from typing import List, Tuple
import unittest

from io import BytesIO

from bdflib import model, reader

# This comes from the X11 BDF spec.
SAMPLE_FONT = b"""
STARTFONT 2.1
COMMENT This is a sample font in 2.1 format.
FONT -Adobe-Helvetica-Bold-R-Normal--24-240-75-75-P-65-ISO8859-1
SIZE 24 75 75
FONTBOUNDINGBOX 9 24 -2 -6
STARTPROPERTIES 19
FOUNDRY "Adobe"
FAMILY "Helvetica"
WEIGHT_NAME "Bold"
SLANT "R"
SETWIDTH_NAME "Normal"
ADD_STYLE_NAME ""
PIXEL_SIZE 24
POINT_SIZE 240
RESOLUTION_X 75
RESOLUTION_Y 75
SPACING "P"
AVERAGE_WIDTH 65
CHARSET_REGISTRY "ISO8859"
CHARSET_ENCODING "1"
MIN_SPACE 4
FONT_ASCENT 21
FONT_DESCENT 7
COPYRIGHT "Copyright (c) 1987 Adobe Systems, Inc."
NOTICE "Helvetica is a registered trademark of Linotype Inc."
ENDPROPERTIES
CHARS 2
STARTCHAR j
ENCODING 106
SWIDTH 355 0
DWIDTH 8 0
BBX 9 22 -2 -6
BITMAP
0380
0380
0380
0380
0000
0700
0700
0700
0700
0E00
0E00
0E00
0E00
0E00
1C00
1C00
1C00
1C00
3C00
7800
F000
E000
ENDCHAR
STARTCHAR quoteright
ENCODING 39
SWIDTH 223 0
DWIDTH 5 0
BBX 4 6 2 12
ATTRIBUTES 01C0
BITMAP
70
70
70
60
E0
C0
ENDCHAR
ENDFONT
""".strip()


class TestReadFont(unittest.TestCase):
    def _check_font(self, font: model.Font) -> None:
        """
        Checks that the given font is a representation of the sample font.
        """
        self.assertEqual(
            font.name,
            b"-Adobe-Helvetica-Bold-R-Normal--24-240-75-75-P-65-ISO8859-1",
        )
        self.assertEqual(font.ptSize, 24.0)
        self.assertEqual(font.xdpi, 75)
        self.assertEqual(font.ydpi, 75)
        self.assertEqual(
            font.get_comments(), [b"This is a sample font in 2.1 format."]
        )
        self.assertEqual(len(font.glyphs), 2)
        # Our code ignores PIXEL_SIZE but adds FACE_NAME, so the total is still
        # 19.
        self.assertEqual(len(font.properties), 19)

    def test_basic_operation(self) -> None:
        testFontData = BytesIO(SAMPLE_FONT)
        testFont = reader.read_bdf(testFontData)

        self._check_font(testFont)

    def test_extra_blank_lines(self) -> None:
        """
        We should ignore any extra lines in the input.
        """
        testFontData = BytesIO(SAMPLE_FONT.replace(b"\n", b"\n\n"))
        testFont = reader.read_bdf(testFontData)

        self._check_font(testFont)

    def test_reject_illegible_font(self) -> None:
        input = [b"blah blah blah"]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 1: Expected b'STARTFONT', not b'blah'"
        )

    def test_reject_unsupported_bdf_version(self) -> None:
        input = [b"STARTFONT 1.9"]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 1: Only BDF versions 2.0 and 2.1 are supported, not b'1.9'",
        )
        input = [b"STARTFONT 2.2"]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 1: Only BDF versions 2.0 and 2.1 are supported, not b'2.2'",
        )

    def test_reject_font_with_no_metadata(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"ENDFONT\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 3: Expected b'SIZE', not b'ENDFONT'"
        )

    def test_accept_font_with_only_metadata(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 0\n",
            b"ENDFONT\n",
        ]
        f = reader.read_bdf(input)

        self.assertEqual(f.name, b"Awesome Font")
        self.assertEqual(f.ptSize, 12)
        self.assertEqual(f.xdpi, 72)
        self.assertEqual(f.ydpi, 72)
        self.assertEqual(f.glyphs, [])
        self.assertEqual(list(f.property_names()), [])
        self.assertEqual(list(f.codepoints()), [])

    def test_accept_font_with_empty_property_list(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 0\n",
            b"ENDPROPERTIES\n",
            b"CHARS 0\n",
            b"ENDFONT\n",
        ]
        f = reader.read_bdf(input)

        self.assertEqual(f.name, b"Awesome Font")
        self.assertEqual(f.ptSize, 12)
        self.assertEqual(f.xdpi, 72)
        self.assertEqual(f.ydpi, 72)
        self.assertEqual(f.glyphs, [])
        self.assertEqual(list(f.property_names()), [])
        self.assertEqual(list(f.codepoints()), [])

    def test_reject_unnamed_font(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT\n",
            b"ENDFONT\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 2: Font name must not be empty"
        )

    def test_reject_font_with_missing_size(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72\n",
            b"ENDFONT\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 3: SIZE must have three values"
        )

    def test_reject_font_with_corrupt_size(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 blah\n",
            b"ENDFONT\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 3: Vertical DPI must be an integer, not b'blah'",
        )

    def test_warn_font_with_negative_size(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE -12 72 72\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(
            str(cm.exception),
            "Line 4: Expected b'FONTBOUNDINGBOX', not b'ENDFONT'",
        )

        expected = (3, "Point size should be > 0, not -12")
        self.assertIn(expected, warnings)

    def test_warn_font_with_negative_xdpi(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 -72 72\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(
            str(cm.exception),
            "Line 4: Expected b'FONTBOUNDINGBOX', not b'ENDFONT'",
        )

        expected = (3, "Horizontal DPI should be > 0, not -72")
        self.assertIn(expected, warnings)

    def test_warn_font_with_negative_ydpi(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 -72\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(
            str(cm.exception),
            "Line 4: Expected b'FONTBOUNDINGBOX', not b'ENDFONT'",
        )

        expected = (3, "Vertical DPI should be > 0, not -72")
        self.assertIn(expected, warnings)

    def test_reject_font_missing_required_metadata(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"STARTPROPERTIES 4\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 2: Expected b'FONT', not b'STARTPROPERTIES'",
        )

        input = [
            b"STARTFONT 2.1\n",
            b"CHARS 4\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 2: Expected b'FONT', not b'CHARS'",
        )

    def test_reject_font_with_unexpected_metadata(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"BLAH\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            (
                "Line 5: Metadata must be followed by properties or chars, "
                + "not b'BLAH'"
            ),
        )

    def test_reject_font_missing_property_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 5: Property count must be an integer, not b''",
        )

    def test_reject_font_with_corrupt_property_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES blah\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 5: Property count must be an integer, not b'blah'",
        )

    def test_warn_font_with_missing_property_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 5\n",
            b"AVERAGE_WIDTH\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (
            6,
            (
                "Property b'AVERAGE_WIDTH' value must be int or quoted string, "
                + "not b''"
            ),
        )
        self.assertIn(expected, warnings)

    def test_warn_font_with_corrupt_property_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 5\n",
            b"AVERAGE_WIDTH blah\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (
            6,
            (
                "Property b'AVERAGE_WIDTH' value must be int or quoted string, "
                + "not b'blah'"
            ),
        )
        self.assertIn(expected, warnings)

    def test_warn_font_with_duplicate_property_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 5\n",
            b"AVERAGE_WIDTH 7\n",
            b"AVERAGE_WIDTH 8\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (
            7,
            "Property b'AVERAGE_WIDTH' already set to 7, ignoring new value 8",
        )
        self.assertIn(expected, warnings)

    def test_reject_font_with_wrong_property_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 2\n",
            b"AVERAGE_WIDTH 10\n",
            b"AVERAGE_RAINFALL 20\n",
            b"AVERAGE_SALARY 30\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 8: Expected b'ENDPROPERTIES', not b'AVERAGE_SALARY'",
        )

    def test_warn_font_with_endproperties_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"STARTPROPERTIES 1\n",
            b"AVERAGE_WIDTH 7\n",
            b"ENDPROPERTIES blah\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (7, "ENDPROPERTIES expects no value, got b'blah'")
        self.assertIn(expected, warnings)

    def test_reject_font_missing_char_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 5: Glyph count must be an integer, not b''"
        )

    def test_reject_font_with_corrupt_char_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS blah\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 5: Glyph count must be an integer, not b'blah'",
        )

    def test_warn_glyph_with_missing_char_name(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (6, "Character name shouldn't be empty")
        self.assertIn(expected, warnings)

    def test_warn_glyph_with_char_name_with_spaces(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR o circumflex\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (
            6,
            "Character name should not contain spaces: b'o circumflex'",
        )
        self.assertIn(expected, warnings)

    def test_reject_glyph_with_missing_encoding_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 7: Glyph encoding must be an integer, not b''",
        )

    def test_reject_glyph_with_corrupt_encoding_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING blah\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 7: Glyph encoding must be an integer, not b'blah'",
        )

    def test_reject_glyph_with_invalid_encoding_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING -2\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (7, "Glyph encoding must be >= -1, not -2")
        self.assertIn(expected, warnings)

    def test_accept_glyph_with_no_encoding(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING -1\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 2 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR\n",
            b"ENDFONT\n",
        ]
        f = reader.read_bdf(input)

        # Make sure this glyph is unencoded
        self.assertNotIn(-1, f)
        self.assertEqual(len(f.glyphs), 1)
        g = f.glyphs[0]
        self.assertEqual(g.name, b"j")
        self.assertEqual(g.codepoint, -1)

    def test_warn_glyph_with_duplicate_encoding(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 2\n",
            b"STARTCHAR j1\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR\n",
            b"STARTCHAR j2\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 2 2 3 4\n",
            b"BITMAP\n",
            b"10\n",
            b"01\n",
            b"ENDCHAR\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        f = reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        # One glyph gets a codepoint
        self.assertEqual(list(f.codepoints()), [106])
        # But there should be two glyphs
        self.assertEqual(len(f.glyphs), 2)

        # Let's get the names and codepoints of glyphs in the font
        actual = set((g.name, g.codepoint) for g in f.glyphs)

        # The first glyph should be fine,
        # the second glyph should be de-encoded
        # (but still exist)
        expected = set(
            [
                (b"j1", 106),
                (b"j2", -1),
            ]
        )

        self.assertEqual(actual, expected)

        # There should be a warning about it
        self.assertIn(
            (16, "Font already has a glyph with encoding 106"), warnings
        )

    def test_reject_glyph_with_missing_dwidth_values(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 9: DWIDTH must have two values, got b''"
        )

    def test_reject_glyph_with_missing_dwidth_y_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 12\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 9: DWIDTH must have two values, got b'12'"
        )

    def test_warn_glyph_with_non_zero_dwidth_y_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 12 1\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (9, "Non-zero vertical DWIDTH not supported, got 1")
        self.assertIn(expected, warnings)

    def test_reject_glyph_with_corrupt_dwidth_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH blah 0\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 9: Horizontal DWIDTH must be an integer, not b'blah'",
        )

    def test_warn_glyph_with_negative_advance(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH -3 0\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(cm.exception.lineno, 0)
        self.assertEqual(cm.exception.message, "Unexpected EOF")

        expected = (9, "Horizontal DWIDTH should be >= 0, not -3")
        self.assertIn(expected, warnings)

    def test_reject_glyph_with_missing_bbx_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception), "Line 10: BBX must have four values"
        )

    def test_reject_glyph_with_corrupt_bbx_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 blah\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 10: Bounding box Y must be an integer, not b'blah'",
        )

    def test_reject_glyph_with_invalid_bb_width(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX -1 2 3 4\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 10: Bounding box width should be >= 0, not -1",
        )

    def test_reject_glyph_with_invalid_bb_height(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 -2 3 4\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 10: Bounding box width should be >= 0, not -2",
        )

    def test_reject_glyph_with_unexpected_metadata(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BLAH\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 11: Expected ATTRIBUTES or BITMAP, not b'BLAH'",
        )

    def test_warn_glyph_with_bitmap_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP blah\n",
        ]
        warnings: List[Tuple[int, str]] = []
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertEqual(
            str(cm.exception),
            "Line 0: Unexpected EOF",
        )

        self.assertIn((11, "BITMAP expects no value, got b'blah'"), warnings)

    def test_reject_glyph_with_not_enough_rows(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"00\n",
            b"ENDCHAR\n",
            b"STARTCHAR k\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 13: Expected hexadecimal digits, not b'ENDCHAR'",
        )

    def test_reject_glyph_with_too_many_rows(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"00\n",
            b"00\n",
            b"00\n",
            b"ENDCHAR\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 14: Expected b'ENDCHAR', not b'00'",
        )

    def test_accept_glyph_with_zero_height(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR space\n",
            b"ENCODING 32\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 0 0 3 4\n",
            b"BITMAP\n",
            b"ENDCHAR\n",
            b"ENDFONT\n",
        ]
        f = reader.read_bdf(input)

        self.assertIn(32, f.codepoints())

        g = f[32]
        self.assertEqual(g.name, b"space")
        self.assertEqual(g.codepoint, 32)
        self.assertEqual(g.bbW, 0)
        self.assertEqual(g.bbH, 0)
        self.assertEqual(g.advance, 8)

    def test_reject_glyph_with_corrupt_rows(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"00\n",
            b"blah\n",
            b"ENDCHAR\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 13: Expected hexadecimal digits, not b'blah'",
        )

    def test_warn_glyph_with_extra_row_data(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10 blah\n",
            b"ENDCHAR\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertIn(
            (13, "Bitmap row should not contain spaces, got b'10 blah'"),
            warnings,
        )

    def test_reject_font_with_non_character_data(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR\n",
            b"BLAH\n",
        ]
        with self.assertRaises(reader.ParseError) as cm:
            reader.read_bdf(input)

        self.assertEqual(
            str(cm.exception),
            "Line 15: Expected STARTCHAR or ENDFONT, not b'BLAH'",
        )

    def test_warn_font_with_endchar_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR blah\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertIn((14, "ENDCHAR expects no value, got b'blah'"), warnings)

    def test_warn_font_with_endfont_value(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 1\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR\n",
            b"ENDFONT blah\n",
        ]
        warnings: List[Tuple[int, str]] = []
        reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertIn((15, "ENDFONT expects no value, got b'blah'"), warnings)

    def test_warn_font_with_wrong_char_count(self) -> None:
        input = [
            b"STARTFONT 2.1\n",
            b"FONT Awesome Font\n",
            b"SIZE 12 72 72\n",
            b"FONTBOUNDINGBOX 9 24 -2 -6\n",
            b"CHARS 2\n",
            b"STARTCHAR j\n",
            b"ENCODING 106\n",
            b"SWIDTH 355 0\n",
            b"DWIDTH 8 0\n",
            b"BBX 1 2 3 4\n",
            b"BITMAP\n",
            b"01\n",
            b"10\n",
            b"ENDCHAR\n",
            b"ENDFONT\n",
        ]
        warnings: List[Tuple[int, str]] = []
        reader.read_bdf(input, lambda n, m: warnings.append((n, m)))

        self.assertIn((15, "Font should have 2 glyphs, found 1"), warnings)
