"""
TOON Specification v3.0 Compliance Test Suite

This test suite validates compliance with the TOON Specification v3.0 (2025-11-24).
Tests are organized by specification section for clear traceability.

Test Coverage Summary:
- 62 tests covering all testable specification sections
- File I/O operations (dump/load)
- Round-trip fidelity tests
- Integration with 128 total tests in test suite

Key sections tested:
- Section 2: Data Model (ordering, number precision, -0.0 normalization)
- Section 5: Root Form (arrays, objects, primitives)
- Section 6: Header Syntax (optional, comma/tab/pipe delimiters)
- Section 7: Strings and Keys (escaping, quoting rules)
- Section 8: Objects (spacing, nesting, empty objects)
- Section 9: Arrays (inline, expanded, tabular forms)
- Section 10: Objects as List Items (hyphen syntax)
- Section 11: Delimiters (comma default, tab, pipe)
- Section 12: Indentation and Whitespace (2-space indent, trailing whitespace)
- Section 14: Strict Mode Errors (invalid syntax, count mismatches)
- Section 15: Security (injection prevention, escaping)

Non-testable sections (reference/definitions only):
- Section 1: Terminology and Conventions
- Section 3: Encoding Normalization (implementation theory)
- Section 4: Decoding Interpretation (implementation theory)
- Section 13: Conformance (general requirements)
- Section 16: Internationalization (UTF-8 is standard, covered in Section 7)
"""

import pytest

import toons


class TestSection2DataModel:
    """Section 2: Data Model"""

    def test_minus_zero_normalization(self):
        """Section 2: -0 MUST be normalized to 0"""
        result = toons.dumps({"value": -0.0})
        assert result == "value: 0"

    def test_array_order_preserved(self):
        """Section 2: Array order MUST be preserved"""
        data = {"items": [3, 1, 4, 1, 5, 9, 2, 6]}
        result = toons.loads(toons.dumps(data))
        assert result["items"] == [3, 1, 4, 1, 5, 9, 2, 6]

    def test_object_key_order_preserved(self):
        """Section 2: Object key order MUST be preserved"""
        data = {"z": 1, "a": 2, "m": 3, "b": 4}
        result = toons.dumps(data)
        lines = result.split("\n")
        # Keys should appear in the order: z, a, m, b
        assert lines == ["z: 1", "a: 2", "m: 3", "b: 4"]

    def test_number_serialization_canonical_form(self):
        """Section 2: No exponent notation"""
        # 1e6 -> 1000000
        assert toons.dumps({"val": 1000000.0}) == "val: 1000000"
        # 1e-6 -> 0.000001
        assert toons.dumps({"val": 0.000001}) == "val: 0.000001"


class TestSection4DecodingInterpretation:
    """Section 4: Decoding Interpretation"""

    def test_number_decoding_leading_zeros(self):
        """Section 4: Tokens with forbidden leading zeros are strings"""
        # "05" -> string "05"
        assert toons.loads("val: 05") == {"val": "05"}
        # "007" -> string "007"
        assert toons.loads("val: 007") == {"val": "007"}
        # "-05" -> string "-05" (v3.0)
        assert toons.loads("val: -05") == {"val": "-05"}
        # "-001" -> string "-001" (v3.0)
        assert toons.loads("val: -001") == {"val": "-001"}

        # Valid numbers
        assert toons.loads("val: 0.5") == {"val": 0.5}
        assert toons.loads("val: -0.5") == {"val": -0.5}
        assert toons.loads("val: 0e1") == {"val": 0.0}
        assert toons.loads("val: -0e1") == {"val": -0.0}


class TestSection5RootForm:
    """Section 5: Concrete Syntax and Root Form"""

    def test_root_array_detection(self):
        """Section 5: Root array header detection"""
        # Array header at root
        toon = "[3]: 1,2,3"
        result = toons.loads(toon)
        assert result == [1, 2, 3]

    def test_root_primitive_single_line(self):
        """Section 5: Single non-empty line without colon is primitive"""
        result = toons.loads("hello")
        assert result == "hello"

        result = toons.loads("42")
        assert result == 42

        result = toons.loads("true")
        assert result is True

    def test_root_object_default(self):
        """Section 5: Otherwise decode as object"""
        toon = "key: value\nother: 123"
        result = toons.loads(toon)
        assert result == {"key": "value", "other": 123}

    def test_empty_document_decodes_to_object(self):
        """Section 5: Empty document decodes to empty object"""
        assert toons.loads("") == {}


class TestSection6HeaderSyntax:
    """Section 6: Header Syntax"""

    def test_header_length_marker_rejected(self):
        """Section 6: Legacy [#N] headers were removed in v2.0"""
        result = toons.loads("items[3]: 1,2,3")
        assert result == {"items": [1, 2, 3]}

        with pytest.raises(ValueError):
            toons.loads("items[#3]: 1,2,3")

    def test_header_comma_default(self):
        """Section 6: Absence of delimiter symbol ALWAYS means comma"""
        toon = "items[2]: a,b"
        result = toons.loads(toon)
        assert result == {"items": ["a", "b"]}

    def test_header_tab_delimiter(self):
        """Section 6: Tab delimiter in header"""
        toon = "items[2\t]: a\tb"
        result = toons.loads(toon)
        assert result == {"items": ["a", "b"]}

    def test_header_pipe_delimiter(self):
        """Section 6: Pipe delimiter in header"""
        toon = "items[2|]: a|b"
        result = toons.loads(toon)
        assert result == {"items": ["a", "b"]}

    def test_header_requires_colon(self):
        """Section 6: Header MUST end with colon"""
        # Parser should reject header without colon
        # Note: Single line "items[3] 1,2,3" is a valid primitive string per Section 5
        # We use multi-line to force object parsing
        with pytest.raises(ValueError):
            toons.loads("items[3] 1,2,3\nother: 1")

    def test_header_space_after_colon_inline(self):
        """Section 6: Exactly one space after colon for inline values"""
        result = toons.dumps({"items": [1, 2, 3]})
        assert result == "items[3]: 1,2,3"  # Note: exactly one space after :


class TestSection7StringsAndKeys:
    """Section 7: Strings and Keys"""

    def test_escape_sequences_encoding(self):
        """Section 7.1: Required escape sequences"""
        data = {"text": 'line1\nline2\ttab\r\n"quoted"\\backslash'}
        result = toons.dumps(data)
        assert '"line1\\nline2\\ttab\\r\\n\\"quoted\\"\\\\backslash"' in result

    def test_escape_sequences_decoding(self):
        """Section 7.1: Escape sequence decoding"""
        toon = r'text: "a\\b\"c\nd\re\tf"'
        result = toons.loads(toon)
        assert result["text"] == 'a\\b"c\nd\re\tf'

    def test_invalid_escape_rejected(self):
        """Section 7.1: Invalid escapes MUST error"""
        with pytest.raises(ValueError):  # Any ValueError is fine
            toons.loads(r'text: "bad\xescape"')

    def test_quoting_empty_string(self):
        """Section 7.2: Empty string MUST be quoted"""
        result = toons.dumps({"empty": ""})
        assert result == 'empty: ""'

    def test_quoting_whitespace(self):
        """Section 7.2: Leading/trailing whitespace requires quoting"""
        result = toons.dumps({"text": " space "})
        assert result == 'text: " space "'

    def test_quoting_boolean_like(self):
        """Section 7.2: true/false/null strings MUST be quoted"""
        result = toons.dumps({"t": "true", "f": "false", "n": "null"})
        assert '"true"' in result
        assert '"false"' in result
        assert '"null"' in result

    def test_quoting_numeric_like(self):
        """Section 7.2: Numeric-like strings MUST be quoted"""
        result = toons.dumps({"num": "42", "float": "3.14", "exp": "1e-6"})
        assert '"42"' in result
        assert '"3.14"' in result
        assert '"1e-6"' in result

    def test_quoting_leading_zero(self):
        """Section 7.2: Leading-zero strings MUST be quoted"""
        result = toons.dumps({"val": "05"})
        assert '"05"' in result

    def test_quoting_special_chars(self):
        """Section 7.2: Colon, quotes, backslash require quoting"""
        result = toons.dumps(
            {"a": "has:colon", "b": 'has"quote', "c": "has\\slash"}
        )
        assert '":"' not in result or '"has:colon"' in result
        assert '"has\\"quote"' in result
        assert '"has\\\\slash"' in result

    def test_quoting_hyphen(self):
        """Section 7.2: Single hyphen or starting with hyphen requires quoting"""
        result = toons.dumps({"a": "-", "b": "-start"})
        assert '"-"' in result
        assert '"-start"' in result

    def test_quoting_delimiter_in_array(self):
        """Section 7.2: Active delimiter in array scope requires quoting"""
        data = {"items": ["a,b", "c", "d,e"]}  # commas in values
        result = toons.dumps(data)
        # Values with commas should be quoted
        assert '"a,b"' in result
        assert '"d,e"' in result

    def test_key_encoding_alphanumeric(self):
        """Section 7.3: Keys matching ^[A-Za-z_][\\w.]*$ MAY be unquoted"""
        data = {"simple_key": 1, "Key123": 2, "with.dot": 3}
        result = toons.dumps(data)
        assert "simple_key: 1" in result
        assert "Key123: 2" in result
        assert "with.dot: 3" in result

    def test_key_encoding_special_chars(self):
        """Section 7.3: Keys with special chars MUST be quoted"""
        data = {"key with space": 1, "key:colon": 2, "key-dash": 3}
        result = toons.dumps(data)
        assert '"key with space":' in result
        assert '"key:colon":' in result
        assert '"key-dash":' in result


class TestSection8Objects:
    """Section 8: Objects"""

    def test_primitive_field_spacing(self):
        """Section 8: Primitive fields have single space after colon"""
        result = toons.dumps({"key": "value"})
        assert result == "key: value"

    def test_nested_object_indentation(self):
        """Section 8: Nested objects appear at depth +1"""
        data = {"parent": {"child": "value"}}
        result = toons.dumps(data)
        lines = result.split("\n")
        assert lines == ["parent:", "  child: value"]

    def test_empty_object_root(self):
        """Section 8: Empty object at root yields empty document and decodes back"""
        result = toons.dumps({})
        assert result == ""

        # loads("") should return an empty object per v3.0 Section 5
        assert toons.loads("") == {}


class TestSection9Arrays:
    """Section 9: Arrays"""

    def test_inline_primitive_array(self):
        """Section 9.1: Non-empty primitive arrays are inline"""
        data = {"tags": ["admin", "user", "guest"]}
        result = toons.dumps(data)
        assert result == "tags[3]: admin,user,guest"

    def test_empty_array(self):
        """Section 9.1: Empty arrays"""
        data = {"empty": []}
        result = toons.dumps(data)
        assert result == "empty[0]:"

    def test_arrays_of_arrays_expanded(self):
        """Section 9.2: Arrays of primitive arrays use expanded list"""
        data = {"pairs": [[1, 2], [3, 4]]}
        result = toons.dumps(data)
        lines = result.split("\n")
        assert lines[0] == "pairs[2]:"
        assert lines[1] == "  - [2]: 1,2"
        assert lines[2] == "  - [2]: 3,4"

    def test_tabular_detection_uniform(self):
        """Section 9.3: Uniform object arrays use tabular format"""
        data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
        result = toons.dumps(data)
        lines = result.split("\n")
        assert lines[0] == "[2]{id,name}:"
        assert lines[1] == "  1,Alice"
        assert lines[2] == "  2,Bob"

    def test_tabular_field_order(self):
        """Section 9.3: Field order is first object's key encounter order"""
        data = [
            {"z": 1, "a": 2, "m": 3},
            {"a": 4, "m": 5, "z": 6},  # Different order
        ]
        result = toons.dumps(data)
        assert "[2]{z,a,m}:" in result

    def test_non_uniform_expanded_list(self):
        """Section 9.4: Non-uniform arrays use expanded list"""
        data = {"mixed": [1, "text", {"key": "value"}]}
        result = toons.dumps(data)
        lines = result.split("\n")
        assert lines[0] == "mixed[3]:"
        assert lines[1] == "  - 1"
        assert lines[2] == "  - text"
        # Object in list - can be empty hyphen line followed by indented fields
        # or first field on hyphen line (Section 10)
        assert "key: value" in result


class TestSection10ObjectsAsListItems:
    """Section 10: Objects as List Items"""

    def test_first_field_on_hyphen_line(self):
        """Section 10: First field can be on hyphen line"""
        data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
        result = toons.dumps(data)
        # This is uniform, so should use tabular format per Section 9.3
        assert "[2]{id,name}:" in result
        assert "1,Alice" in result
        assert "2,Bob" in result

    def test_tabular_array_in_list_item(self):
        """
        Section 10: List-item object with tabular array as first field.
        Encoders MUST emit tabular header on hyphen line, rows at depth +2.
        """
        data = {
            "items": [
                {
                    "users": [
                        {"id": 1, "name": "Ada"},
                        {"id": 2, "name": "Bob"},
                    ],
                    "status": "active",
                }
            ]
        }

        encoded = toons.dumps(data)
        lines = encoded.strip().split("\n")

        # Verify structure
        # items[1]:
        #   - users[2]{id,name}:
        #       1,Ada
        #       2,Bob
        #     status: active

        assert "items[1]:" in lines[0]
        assert "  - users[2]{id,name}:" in lines[1]
        assert lines[2].startswith("      ")  # 6 spaces (depth 3)
        assert "1,Ada" in lines[2]
        assert "status: active" in lines[4]

        # Verify round-trip
        decoded = toons.loads(encoded)
        assert decoded == data


class TestSection11Delimiters:
    """Section 11: Delimiters"""

    def test_comma_default(self):
        """Section 11: Comma is default delimiter"""
        result = toons.dumps({"items": [1, 2, 3]})
        assert result == "items[3]: 1,2,3"

    def test_tab_delimiter_encoding(self):
        """Section 11: Tab delimiter support"""
        # Encode with tab delimiter
        data = {"items": ["a", "b", "c"]}
        result = toons.dumps(data, delimiter="\t")
        assert result == "items[3\t]: a\tb\tc"

        # Parse tab-delimited correctly
        toon = "items[3\t]: a\tb\tc"
        result = toons.loads(toon)
        assert result == {"items": ["a", "b", "c"]}

    def test_pipe_delimiter_encoding(self):
        """Section 11: Pipe delimiter support"""
        # Encode with pipe delimiter
        data = {"items": ["a", "b", "c"]}
        result = toons.dumps(data, delimiter="|")
        assert result == "items[3|]: a|b|c"

        # Parse pipe-delimited correctly
        toon = "items[3|]: a|b|c"
        result = toons.loads(toon)
        assert result == {"items": ["a", "b", "c"]}

    def test_delimiter_scoping(self):
        """Section 11: Nested headers can change delimiter"""
        # Outer uses pipe, inner uses comma
        toon = """
items[2|]:
  - [2]: a,b
  - [2]: c,d
"""
        result = toons.loads(toon)
        assert result == {"items": [["a", "b"], ["c", "d"]]}

    def test_document_delimiter_quoting(self):
        """
        Section 11.1: Object field values (key: value) use the document delimiter
        to decide delimiter-aware quoting.
        """
        # Value containing comma (default document delimiter) MUST be quoted
        data = {"key": "value,with,comma"}
        encoded = toons.dumps(data)
        assert '"value,with,comma"' in encoded

        # Value containing pipe SHOULD NOT be quoted if document delimiter is comma
        # (and no other quoting rules apply)
        data_pipe = {"key": "value|with|pipe"}
        encoded_pipe = toons.dumps(data_pipe)
        assert "value|with|pipe" in encoded_pipe
        assert '"value|with|pipe"' not in encoded_pipe


class TestSection12IndentationWhitespace:
    """Section 12: Indentation and Whitespace"""

    def test_default_indent_two_spaces(self):
        """Section 12: Default indent is 2 spaces"""
        data = {"parent": {"child": "value"}}
        result = toons.dumps(data)  # Uses default indent=2
        lines = result.split("\n")
        assert lines[1].startswith("  ")  # 2 spaces
        assert not lines[1].startswith("   ")  # not 3

    def test_no_trailing_spaces(self):
        """Section 12: No trailing spaces at end of lines"""
        result = toons.dumps({"key": "value", "nested": {"a": 1}})
        for line in result.split("\n"):
            assert not line.endswith(" "), (
                f"Line has trailing space: {repr(line)}"
            )

    def test_no_trailing_newline(self):
        """Section 12: No trailing newline at end of document"""
        result = toons.dumps({"key": "value"})
        assert not result.endswith("\n")

    def test_one_space_after_colon_primitive(self):
        """Section 12: Exactly one space after : in key: value"""
        result = toons.dumps({"key": "value"})
        assert "key: value" in result
        assert "key:  value" not in result  # not two spaces

    def test_one_space_after_header_inline(self):
        """Section 12: Exactly one space after header for inline values"""
        result = toons.dumps({"items": [1, 2, 3]})
        assert "items[3]: 1,2,3" in result
        assert "items[3]:  " not in result  # not two spaces


class TestSection14StrictModeErrors:
    """Section 14: Strict Mode Errors"""

    def test_array_count_mismatch(self):
        """Section 14.1: Array count mismatch MUST error"""
        # Inline arrays - declared 3, but only 2 values
        with pytest.raises(ValueError):
            toons.loads("items[3]: 1,2")

    def test_tabular_row_count_mismatch(self):
        """Section 14.1: Tabular row count mismatch MUST error"""
        toon = """items[3]{id,name}:
  1,Alice
  2,Bob"""
        with pytest.raises(ValueError):  # Any ValueError is fine
            toons.loads(toon)

    def test_tabular_width_mismatch(self):
        """Section 14.1: Tabular width mismatch MUST error"""
        toon = """items[2]{id,name}:
  1,Alice
  2"""  # Missing name
        with pytest.raises(ValueError):
            toons.loads(toon)

    def test_missing_colon_error(self):
        """Section 14.2: Missing colon MUST error"""
        # Note: Single line "key value" is a valid primitive string per Section 5
        # We use multi-line to force object parsing
        with pytest.raises(ValueError):
            toons.loads("key: value\nkey2 value")

    def test_invalid_escape_error(self):
        """Section 14.2: Invalid escape MUST error"""
        with pytest.raises(ValueError):  # Any ValueError is fine
            toons.loads(r'text: "bad\xescape"')

    def test_indentation_modulo_check(self):
        """Section 14.3: Leading spaces must be multiple of indentSize"""
        # Establish indent=2 then violate it
        toon = """
parent:
  child1: 1
   child2: 2
"""
        with pytest.raises(ValueError, match="Indentation"):
            toons.loads(toon)

    def test_indentation_no_tabs(self):
        """Section 14.3: Tabs not allowed in indentation"""
        toon = "parent:\n\tchild: 1"
        with pytest.raises(ValueError, match="Tabs"):
            toons.loads(toon)

    def test_blank_lines_in_arrays(self):
        """Section 14.4: Blank lines inside arrays must error"""
        toon = """
items[2]:
  - 1

  - 2
"""
        with pytest.raises(ValueError, match="Blank line"):
            toons.loads(toon)

    def test_blank_lines_in_tabular(self):
        """Section 14.4: Blank lines inside tabular rows must error"""
        toon = """
items[2]{id}:
  1

  2
"""
        with pytest.raises(ValueError, match="Blank line"):
            toons.loads(toon)


class TestSection15Security:
    """Section 15: Security Considerations"""

    def test_injection_prevention_colon(self):
        """Section 15: Strings with colon MUST be quoted to prevent injection"""
        result = toons.dumps({"safe": "key:value"})
        assert '"key:value"' in result

    def test_injection_prevention_hyphen(self):
        """Section 15: Hyphen marker cases MUST be quoted"""
        result = toons.dumps({"items": ["-", "-list"]})
        assert '"-"' in result
        assert '"-list"' in result


class TestFileOperations:
    """Tests for file-based dump() and load() operations

    Tests Section 5 (Root Form) and general I/O functionality.
    """

    def test_dump_to_file(self, tmp_path):
        """Test dumping data to a file"""
        file_path = tmp_path / "test.toon"
        data = {"name": "Alice", "age": 30, "scores": [95, 87, 92]}

        with open(file_path, "w") as f:
            toons.dump(data, f)

        # Verify file was created and contains valid TOON
        assert file_path.exists()
        with open(file_path, "r") as f:
            content = f.read()
        result = toons.loads(content)
        assert result == data

    def test_load_from_file(self, tmp_path):
        """Test loading data from a file"""
        file_path = tmp_path / "test.toon"
        # Use TOON native syntax: key: value
        toon_content = "name: Bob\nactive: true\nitems[3]: 1,2,3"

        with open(file_path, "w") as f:
            f.write(toon_content)

        with open(file_path, "r") as f:
            result = toons.load(f)

        assert result == {"name": "Bob", "active": True, "items": [1, 2, 3]}

    def test_dump_and_load_roundtrip(self, tmp_path):
        """Test dump() followed by load() preserves data"""
        file_path = tmp_path / "roundtrip.toon"
        original_data = {
            "string": "hello",
            "number": 42,
            "float": 3.14,
            "bool": True,
            "null": None,
            "array": [1, 2, 3],
            "nested": {"key": "value"},
        }

        with open(file_path, "w") as f:
            toons.dump(original_data, f)

        with open(file_path, "r") as f:
            loaded_data = toons.load(f)

        assert loaded_data == original_data


class TestRoundTripFidelity:
    """Comprehensive round-trip tests

    Tests fidelity across all supported data types and structures.
    Validates Section 2 (Data Model) preservation.
    """

    @pytest.mark.parametrize(
        "data",
        [
            {"simple": "value"},
            {"nested": {"level2": {"level3": "deep"}}},
            {"array": [1, 2, 3, 4, 5]},
            # {"mixed": [1, "text", {"key": "value"}, [1, 2]]},  # Nested arrays in mixed - skip for now
            [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
            {"unicode": "Hello 世界 🌍"},
            {"numbers": [0, -42, 3.14, -99.99, 1e-6]},
            {"bools": [True, False, None]},
            {"empty": {"nested": {}}},
        ],
    )
    def test_round_trip(self, data):
        """All data structures maintain fidelity through encode/decode"""
        encoded = toons.dumps(data)
        decoded = toons.loads(encoded)
        assert decoded == data
