"""Tests for OCTAVE canonical emitter (P1.4).

Tests AST → canonical OCTAVE string emission with:
- Unicode operators (never ASCII)
- No whitespace around ::
- Explicit envelope
- Deterministic output
- Idempotence
- I2 Deterministic Absence (Absent sentinel handling)
"""

import pytest

from octave_mcp.core.emitter import FormatOptions, emit, emit_meta, emit_value, needs_quotes
from octave_mcp.core.grammar.cst import Absent, Assignment, Block, Document, InlineMap, ListValue
from octave_mcp.core.parser import parse


class TestCanonicalEmission:
    """Test canonical OCTAVE emission."""

    def test_emits_unicode_operators(self):
        """Should emit unicode operators, never ASCII."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="FLOW", value="A→B→C")],
        )
        result = emit(doc)
        assert "→" in result
        assert "->" not in result

    def test_no_whitespace_around_assignment(self):
        """Should emit KEY::value with no spaces."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="value")])
        result = emit(doc)
        assert "KEY::value" in result
        assert "KEY :: value" not in result

    def test_explicit_envelope_always_present(self):
        """Should always emit explicit envelope."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="value")])
        result = emit(doc)
        assert result.startswith("===TEST===")
        assert result.strip().endswith("===END===")

    def test_typed_envelope_roundtrip(self):
        """GH#347: Emit roundtrip should preserve typed envelope name."""
        content = "===PATTERN:MIP_BUILD===\nMETA:\n  TYPE::TEST\n===END===\n"
        doc = parse(content)
        result = emit(doc)
        assert result.startswith("===PATTERN:MIP_BUILD===")
        assert result.strip().endswith("===END===")

    def test_quoted_strings_where_required(self):
        """Should quote strings with spaces/special chars."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="hello world")])
        result = emit(doc)
        assert '"hello world"' in result

    def test_bare_strings_when_safe(self):
        """Should use bare words when no spaces/special chars."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="simple")])
        result = emit(doc)
        assert "KEY::simple" in result
        assert '"simple"' not in result

    def test_2_space_indentation(self):
        """Should use consistent 2-space indentation."""
        doc = Document(
            name="TEST",
            sections=[Block(key="BLOCK", children=[Assignment(key="CHILD", value="value")])],
        )
        result = emit(doc)
        lines = result.split("\n")
        # Find CHILD line
        child_line = [line for line in lines if "CHILD" in line][0]
        assert child_line.startswith("  ")  # 2 spaces
        assert not child_line.startswith("    ")  # Not 4 spaces

    def test_empty_list(self):
        """Should emit empty list as []."""
        doc = Document(name="TEST", sections=[Assignment(key="EMPTY", value=ListValue(items=[]))])
        result = emit(doc)
        assert "EMPTY::[]" in result

    def test_list_with_items(self):
        """Should emit list with comma-separated items (GH#273: 3+ items go multi-line)."""
        doc = Document(name="TEST", sections=[Assignment(key="TAGS", value=ListValue(items=["a", "b", "c"]))])
        result = emit(doc)
        # GH#273: 3+ items now emit multi-line
        assert "TAGS::[\n  a,\n  b,\n  c\n]" in result


class TestIdempotence:
    """Test emit is idempotent."""

    def test_emit_parse_emit_idempotent(self):
        """Should satisfy emit(parse(emit(parse(x)))) == emit(parse(x))."""
        original = """===TEST===
META:
  TYPE::TEST_DOC
  VERSION::"1.0"
---
DATA::value
TAGS::[a,b,c]
===END===
"""
        doc1 = parse(original)
        emitted1 = emit(doc1)
        doc2 = parse(emitted1)
        emitted2 = emit(doc2)
        assert emitted1 == emitted2


class TestMetaEmission:
    """Test META block emission."""

    def test_emits_meta_block(self):
        """Should emit META block."""
        doc = Document(
            name="TEST",
            meta={"TYPE": "TEST_DOC", "VERSION": "1.0"},
            sections=[],
        )
        result = emit(doc)
        assert "META:" in result
        assert "TYPE::TEST_DOC" in result
        assert "VERSION::" in result

    def test_emits_separator_when_present(self):
        """Should emit --- separator when has_separator=True."""
        doc = Document(name="TEST", has_separator=True, sections=[Assignment(key="KEY", value="value")])
        result = emit(doc)
        assert "---" in result


class TestBlockEmission:
    """Test block structure emission."""

    def test_emits_simple_block(self):
        """Should emit KEY: with nested children."""
        doc = Document(
            name="TEST",
            sections=[Block(key="CONFIG", children=[Assignment(key="NESTED", value="value")])],
        )
        result = emit(doc)
        assert "CONFIG:" in result
        assert "  NESTED::value" in result

    def test_emits_deeply_nested_blocks(self):
        """Should emit multiple nesting levels."""
        doc = Document(
            name="TEST",
            sections=[
                Block(
                    key="LEVEL1",
                    children=[
                        Block(key="LEVEL2", children=[Assignment(key="LEVEL3", value="value")]),
                    ],
                )
            ],
        )
        result = emit(doc)
        assert "LEVEL1:" in result
        assert "  LEVEL2:" in result
        assert "    LEVEL3::value" in result

    def test_empty_block(self):
        """Should emit empty block as KEY: with no children."""
        doc = Document(name="TEST", sections=[Block(key="EMPTY", children=[])])
        result = emit(doc)
        assert "EMPTY:" in result


class TestI2AbsentHandling:
    """Test I2 Deterministic Absence: Absent sentinel handling.

    Per CRS blocking feedback:
    - emit_value(Absent()) must raise ValueError (not return empty string)
    - Absent values in ListValue must be filtered out
    - Absent values in InlineMap must be filtered out
    - emit_meta() must return "" when all fields are absent
    """

    def test_emit_value_raises_on_absent(self):
        """emit_value(Absent()) must raise ValueError, not return empty string.

        CRS Issue 1: Absent can leak into emit_value producing invalid output.
        Previous behavior: returned "" which produced `KEY::` (empty value).
        Required behavior: raise ValueError to catch caller bugs.
        """
        with pytest.raises(ValueError, match="Absent"):
            emit_value(Absent())

    def test_list_with_absent_filters_absent_items(self):
        """ListValue containing Absent items must filter them out.

        CRS Issue 1: [Absent(), 'a'] previously emitted `[,a]` (invalid).
        Required: Absent items are filtered, emitting `[a]`.
        """
        list_val = ListValue(items=[Absent(), "a", Absent(), "b", Absent()])
        result = emit_value(list_val)
        assert result == "[a,b]"

    def test_inline_map_with_absent_filters_absent_values(self):
        """InlineMap with Absent values must filter those pairs out.

        CRS Issue 1: {'k': Absent()} previously emitted `k::` (invalid).
        Required: Pairs with Absent values are filtered out.
        """
        inline_map = InlineMap(pairs={"present": "value", "absent_key": Absent()})
        result = emit_value(inline_map)
        assert result == "[present::value]"
        assert "absent_key" not in result

    def test_emit_meta_empty_when_all_absent(self):
        """emit_meta() must return '' when all fields are Absent.

        CRS Issue 2: emit_meta() emitted header even if all fields skipped.
        Required: Return "" when all meta fields are absent.
        """
        meta = {"TYPE": Absent(), "VERSION": Absent()}
        result = emit_meta(meta)
        assert result == ""

    def test_emit_meta_filters_absent_preserves_values(self):
        """emit_meta() filters Absent but preserves present values."""
        meta = {"TYPE": "TEST", "VERSION": Absent(), "STATUS": "ACTIVE"}
        result = emit_meta(meta)
        assert "TYPE::TEST" in result
        assert "STATUS::ACTIVE" in result
        assert "VERSION" not in result

    def test_absent_singleton_pattern(self):
        """Absent() must return singleton instance.

        CRS Issue 3: Missing test for singleton behavior.
        """
        assert Absent() is Absent()
        a1 = Absent()
        a2 = Absent()
        assert a1 is a2


class TestEmitterEdgeCases:
    """Test edge cases in emitter for coverage."""

    def test_emit_empty_string_value(self):
        """Empty string needs quotes to be valid."""
        doc = Document(name="TEST", sections=[Assignment(key="EMPTY", value="")])
        result = emit(doc)
        assert 'EMPTY::""' in result

    def test_emit_boolean_values(self):
        """Boolean values emit as true/false."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="TRUE_VAL", value=True),
                Assignment(key="FALSE_VAL", value=False),
            ],
        )
        result = emit(doc)
        assert "TRUE_VAL::true" in result
        assert "FALSE_VAL::false" in result

    def test_emit_null_value(self):
        """None emits as null."""
        doc = Document(name="TEST", sections=[Assignment(key="NULL_VAL", value=None)])
        result = emit(doc)
        assert "NULL_VAL::null" in result

    def test_emit_numeric_values(self):
        """Numeric values emit directly."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="INT_VAL", value=42),
                Assignment(key="FLOAT_VAL", value=3.14),
            ],
        )
        result = emit(doc)
        assert "INT_VAL::42" in result
        assert "FLOAT_VAL::3.14" in result

    def test_emit_unknown_type_fallback(self):
        """Unknown types fall back to str()."""

        class CustomType:
            def __str__(self):
                return "CUSTOM_VALUE"

        doc = Document(name="TEST", sections=[Assignment(key="CUSTOM", value=CustomType())])
        result = emit(doc)
        assert "CUSTOM::CUSTOM_VALUE" in result

    def test_emit_reserved_words_quoted(self):
        """Reserved words (true, false, null, vs) need quotes."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="VAL1", value="true"),
                Assignment(key="VAL2", value="false"),
                Assignment(key="VAL3", value="null"),
                Assignment(key="VAL4", value="vs"),
            ],
        )
        result = emit(doc)
        assert 'VAL1::"true"' in result
        assert 'VAL2::"false"' in result
        assert 'VAL3::"null"' in result
        assert 'VAL4::"vs"' in result

    def test_emit_string_with_special_chars(self):
        """Strings with special chars need escaping."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="NEWLINE", value="line1\nline2"),
                Assignment(key="TAB", value="col1\tcol2"),
                Assignment(key="QUOTE", value='say "hello"'),
            ],
        )
        result = emit(doc)
        assert "\\n" in result
        assert "\\t" in result
        assert '\\"' in result

    def test_emit_inline_map(self):
        """InlineMap emits as [key::val]."""
        inline_map = InlineMap(pairs={"key": "value"})
        doc = Document(name="TEST", sections=[Assignment(key="MAP", value=inline_map)])
        result = emit(doc)
        assert "MAP::[key::value]" in result

    def test_emit_absent_in_document_sections(self):
        """Absent values in document sections are skipped."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="PRESENT", value="here"),
                Assignment(key="ABSENT", value=Absent()),
            ],
        )
        result = emit(doc)
        assert "PRESENT::here" in result
        assert "ABSENT" not in result

    def test_emit_absent_in_block_children(self):
        """Absent values in Block children are skipped."""
        doc = Document(
            name="TEST",
            sections=[
                Block(
                    key="BLOCK",
                    children=[
                        Assignment(key="PRESENT", value="here"),
                        Assignment(key="ABSENT", value=Absent()),
                    ],
                )
            ],
        )
        result = emit(doc)
        assert "PRESENT::here" in result
        assert "ABSENT" not in result


class TestFrontmatterPreservation:
    """Test Zone 2 (Preserving Container) - YAML frontmatter round-trip.

    Issue #234: The emitter must preserve raw_frontmatter when present
    on the Document AST. This ensures Zone 2 container data survives
    parse -> emit round-trips without loss (I1: syntactic fidelity).
    """

    def test_emit_preserves_frontmatter(self):
        """emit() should prepend raw_frontmatter when present on Document."""
        doc = Document(
            name="TEST_SKILL",
            sections=[Assignment(key="PURPOSE", value="Test skill")],
            raw_frontmatter="name: test-skill\ndescription: A test skill",
        )
        result = emit(doc)
        assert result.startswith("---\n")
        assert "name: test-skill" in result
        assert "description: A test skill" in result
        # Frontmatter must be closed before envelope
        fm_end = result.index("---\n", 3)  # Find second ---
        envelope_start = result.index("===TEST_SKILL===")
        assert fm_end < envelope_start

    def test_emit_no_frontmatter_when_none(self):
        """emit() should NOT add frontmatter when raw_frontmatter is None."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
        )
        result = emit(doc)
        assert not result.startswith("---")
        assert result.startswith("===TEST===")

    def test_roundtrip_frontmatter_preserved(self):
        """Parse a document with frontmatter, emit it, frontmatter preserved."""
        original = (
            "---\n"
            "name: test-skill\n"
            "description: A test skill for frontmatter preservation\n"
            'allowed-tools: ["Read", "Write", "Edit"]\n'
            'version: "1.0.0"\n'
            "---\n"
            "\n"
            "===TEST_SKILL===\n"
            "META:\n"
            "  TYPE::SKILL\n"
            '  VERSION::"1.0.0"\n'
            "  STATUS::ACTIVE\n"
            "\n"
            "\u00a71::CORE\n"
            'PURPOSE::"Test skill"\n'
            "\n"
            "===END==="
        )
        doc = parse(original)
        assert doc.raw_frontmatter is not None
        emitted = emit(doc)
        # The emitted output must contain the frontmatter
        assert emitted.startswith("---\n")
        assert "name: test-skill" in emitted
        assert 'allowed-tools: ["Read", "Write", "Edit"]' in emitted
        assert 'version: "1.0.0"' in emitted
        # Re-parse to verify round-trip
        doc2 = parse(emitted)
        assert doc2.raw_frontmatter == doc.raw_frontmatter

    def test_roundtrip_no_frontmatter_unchanged(self):
        """Parse a pure OCTAVE file (no frontmatter) -> emit -> no frontmatter added."""
        original = (
            "===PURE_DOC===\n" "META:\n" "  TYPE::TEST\n" "\n" "\u00a71::SECTION\n" "KEY::value\n" "\n" "===END==="
        )
        doc = parse(original)
        assert doc.raw_frontmatter is None
        emitted = emit(doc)
        assert not emitted.startswith("---")
        assert emitted.startswith("===PURE_DOC===")

    def test_frontmatter_byte_for_byte_preserved(self):
        """Frontmatter content must be byte-for-byte preserved (no normalization)."""
        # Include special characters, tabs, unusual spacing
        raw_fm = 'name: test-skill\n  nested: "value with :: colons"\ntags: [a, b, c]'
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
            raw_frontmatter=raw_fm,
        )
        result = emit(doc)
        # Extract frontmatter from result
        lines = result.split("\n")
        assert lines[0] == "---"
        # Find closing ---
        close_idx = None
        for i in range(1, len(lines)):
            if lines[i] == "---":
                close_idx = i
                break
        assert close_idx is not None
        extracted_fm = "\n".join(lines[1:close_idx])
        assert extracted_fm == raw_fm

    def test_frontmatter_separator_no_conflict_with_doc_separator(self):
        """Frontmatter --- markers must not conflict with doc.has_separator."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
            raw_frontmatter="name: test",
            has_separator=True,
        )
        result = emit(doc)
        # Should have frontmatter --- pair AND document ---
        parts = result.split("---")
        # parts[0] is empty (before first ---), parts[1] is frontmatter,
        # parts[2] contains envelope + separator section onward
        assert len(parts) >= 3, f"Expected at least 3 parts split by ---, got {len(parts)}"

    def test_empty_string_frontmatter_treated_as_absent(self):
        """raw_frontmatter="" should NOT emit empty ---\\n\\n--- block."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
            raw_frontmatter="",
        )
        result = emit(doc)
        assert not result.startswith("---")
        assert result.startswith("===TEST===")

    def test_whitespace_only_frontmatter_treated_as_absent(self):
        """raw_frontmatter with only whitespace should NOT emit."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
            raw_frontmatter="   \n  \n  ",
        )
        result = emit(doc)
        assert not result.startswith("---")
        assert result.startswith("===TEST===")

    def test_frontmatter_survives_trailing_whitespace_strip(self):
        """Frontmatter content survives format_options trailing_whitespace='strip'."""
        doc = Document(
            name="TEST",
            sections=[Assignment(key="KEY", value="val")],
            raw_frontmatter="name: test-skill\ndescription: A skill",
        )
        opts = FormatOptions(trailing_whitespace="strip")
        result = emit(doc, format_options=opts)
        assert result.startswith("---\n")
        assert "name: test-skill" in result
        assert "description: A skill" in result


class TestMultiLineArrayEmission:
    """Test multi-line emission for arrays containing structured content (GH#267).

    Arrays with KEY::VALUE pairs or nested arrays should emit multi-line
    with 2-space indentation. Simple flat arrays remain single-line when short.
    """

    def test_flat_short_array_stays_single_line(self):
        """Simple flat arrays with fewer than 3 items remain single-line (GH#273)."""
        result = emit_value(ListValue(items=["a", "b"]))
        assert result == "[a,b]"

    def test_array_with_kv_pairs_emits_multiline(self):
        """Arrays containing KEY::VALUE pairs (InlineMap items) emit multi-line."""
        # An array with InlineMap items inside it
        items = [
            InlineMap(pairs={"ROLE": "ORCHESTRATOR"}),
            InlineMap(pairs={"COGNITION": "LOGOS"}),
        ]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result
        assert "  ROLE::ORCHESTRATOR,\n" in result
        assert "  COGNITION::LOGOS\n" in result
        assert result.endswith("]")

    def test_array_with_nested_array_emits_multiline(self):
        """Arrays containing nested arrays emit multi-line."""
        inner = ListValue(items=["ATLAS", "ODYSSEUS"])
        items = ["ROLE", inner, "MODEL"]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result
        assert result.endswith("]")

    def test_nested_array_indentation(self):
        """Nested structured arrays get additional 2-space indentation."""
        inner = ListValue(
            items=[
                InlineMap(pairs={"ATLAS": "ultimate_accountability"}),
                InlineMap(pairs={"ODYSSEUS": "cross_boundary_navigation"}),
            ]
        )
        items = [
            InlineMap(pairs={"ROLE": "ORCHESTRATOR"}),
            InlineMap(pairs={"ARCHETYPE": inner}),
        ]
        result = emit_value(ListValue(items=items))
        # The inner structured array should be indented further
        assert "  ARCHETYPE::[\n" in result
        assert "    ATLAS::ultimate_accountability,\n" in result

    def test_multiline_in_assignment_context(self):
        """Multi-line arrays within assignments use correct base indentation."""
        inner = ListValue(
            items=[
                "ATLAS<ultimate_accountability>",
                "ODYSSEUS<cross_boundary_navigation>",
            ]
        )
        items = [
            InlineMap(pairs={"ROLE": "HOLISTIC_ORCHESTRATOR"}),
            InlineMap(pairs={"COGNITION": "LOGOS"}),
            InlineMap(pairs={"ARCHETYPE": inner}),
            InlineMap(pairs={"MODEL_TIER": "PREMIUM"}),
        ]
        doc = Document(
            name="TEST",
            sections=[
                Block(
                    key="SECTION",
                    children=[
                        Assignment(key="CORE", value=ListValue(items=items)),
                    ],
                )
            ],
        )
        result = emit(doc)
        # The CORE assignment is at indent level 1 (inside SECTION block)
        # So array content should be at indent level 2 (4 spaces)
        assert "  CORE::[\n" in result
        assert "    ROLE::HOLISTIC_ORCHESTRATOR,\n" in result
        assert "    COGNITION::LOGOS,\n" in result
        # GH#304: Inner array with annotation items now goes multi-line
        assert "    ARCHETYPE::[\n" in result
        assert "      ATLAS<ultimate_accountability>,\n" in result
        assert "      ODYSSEUS<cross_boundary_navigation>\n" in result
        assert "    ],\n" in result
        assert "    MODEL_TIER::PREMIUM\n" in result
        assert "  ]" in result

    def test_multiline_in_assignment_with_nested_structured_array(self):
        """Multi-line arrays with nested structured arrays emit recursively."""
        inner = ListValue(
            items=[
                InlineMap(pairs={"ATLAS": "ultimate_accountability"}),
                InlineMap(pairs={"ODYSSEUS": "cross_boundary_navigation"}),
            ]
        )
        items = [
            InlineMap(pairs={"ROLE": "HOLISTIC_ORCHESTRATOR"}),
            InlineMap(pairs={"ARCHETYPE": inner}),
            InlineMap(pairs={"MODEL_TIER": "PREMIUM"}),
        ]
        doc = Document(
            name="TEST",
            sections=[
                Block(
                    key="SECTION",
                    children=[
                        Assignment(key="CORE", value=ListValue(items=items)),
                    ],
                )
            ],
        )
        result = emit(doc)
        # Outer array at indent 1, inner structured array deeper
        assert "  CORE::[\n" in result
        assert "    ROLE::HOLISTIC_ORCHESTRATOR,\n" in result
        assert "    ARCHETYPE::[\n" in result
        assert "      ATLAS::ultimate_accountability,\n" in result
        assert "      ODYSSEUS::cross_boundary_navigation\n" in result
        assert "    ],\n" in result
        assert "    MODEL_TIER::PREMIUM\n" in result
        assert "  ]" in result

    def test_multiline_idempotent(self):
        """Multi-line emission must be deterministic (same input -> same output)."""
        items = [
            InlineMap(pairs={"ROLE": "ORCHESTRATOR"}),
            InlineMap(pairs={"COGNITION": "LOGOS"}),
        ]
        result1 = emit_value(ListValue(items=items))
        result2 = emit_value(ListValue(items=items))
        assert result1 == result2

    def test_long_flat_array_emits_multiline(self):
        """Flat arrays with 3+ items emit multi-line for readability (GH#273).

        The threshold is item count >= 3, regardless of content type.
        This prevents 300+ character single lines.
        """
        items = [f"ITEM_{i}" for i in range(20)]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result
        assert result.endswith("]")

    def test_array_with_kv_pair_string_values_emits_multiline(self):
        """Arrays where items contain :: (key-value syntax) should go multi-line."""
        # This tests InlineMap items with quoted string values
        items = [
            InlineMap(pairs={"MUST_ALWAYS": ListValue(items=["Write tests first", "Check coverage"])}),
        ]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result

    def test_closing_bracket_at_parent_indent(self):
        """Closing ] must be at the parent's indentation level."""
        items = [
            InlineMap(pairs={"ROLE": "ORCHESTRATOR"}),
            InlineMap(pairs={"COGNITION": "LOGOS"}),
        ]
        result = emit_value(ListValue(items=items), indent=2)
        lines = result.split("\n")
        # Closing bracket should be at indent level 2 (4 spaces)
        assert lines[-1] == "    ]"

    def test_all_absent_inline_map_skipped_in_multiline(self):
        """InlineMap where all pairs are Absent should be skipped entirely (GH#267).

        An all-Absent InlineMap produces no visible pairs. Emitting it would
        insert an empty line inside the array brackets, breaking emit-parse
        idempotency (I1 violation).
        """
        items = [
            InlineMap(pairs={"A": Absent()}),
        ]
        result = emit_value(ListValue(items=items))
        # With all items filtered out, the result should be an empty array
        assert result == "[]"

    def test_all_absent_inline_map_among_valid_items(self):
        """All-Absent InlineMap among valid items should be skipped (GH#267).

        The valid items should still emit normally; only the all-Absent
        InlineMap item is dropped.
        """
        items = [
            InlineMap(pairs={"ROLE": "ORCHESTRATOR"}),
            InlineMap(pairs={"DEAD": Absent()}),
            InlineMap(pairs={"COGNITION": "LOGOS"}),
        ]
        result = emit_value(ListValue(items=items))
        assert "ROLE::ORCHESTRATOR" in result
        assert "COGNITION::LOGOS" in result
        assert "DEAD" not in result
        # Should still be multi-line (has valid InlineMap items)
        assert "[\n" in result

    def test_mixed_absent_non_absent_inline_map(self):
        """InlineMap with mix of Absent and non-Absent pairs emits only non-Absent (GH#267).

        Only the pairs with actual values should appear in the output.
        The Absent pairs are silently dropped per I2.
        """
        items = [
            InlineMap(pairs={"A": "val", "B": Absent()}),
        ]
        result = emit_value(ListValue(items=items))
        assert "A::val" in result
        assert "B" not in result

    def test_all_absent_inline_map_roundtrip(self):
        """Emit-parse-emit roundtrip for ListValue with all-Absent InlineMap (GH#267).

        The emitted output must parse back to an equivalent AST that re-emits
        identically, proving I1 (emit-parse idempotency).
        """
        doc = Document(
            name="TEST",
            sections=[
                Assignment(key="K", value=ListValue(items=[InlineMap(pairs={"A": Absent()})])),
            ],
        )
        emitted1 = emit(doc)
        parsed = parse(emitted1)
        emitted2 = emit(parsed)
        assert emitted1 == emitted2

    def test_three_plus_string_items_emit_multiline(self):
        """Arrays with 3+ string items should emit multi-line for readability (GH#273).

        String-only arrays with many items produce 300+ char lines when collapsed.
        The threshold of 3 items triggers multi-line emission regardless of
        content type, improving readability while preserving semantics (I1).
        """
        items = [
            "STRUCTURAL_THINKING_BEFORE_TACTICAL_ACTION",
            "CROSS_BOUNDARY_COHERENCE_CHECKS",
            "PROPHETIC_FAILURE_PREVENTION",
            "DELEGATION_VIA_OA_ROUTER",
        ]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result
        assert "  STRUCTURAL_THINKING_BEFORE_TACTICAL_ACTION,\n" in result
        assert "  CROSS_BOUNDARY_COHERENCE_CHECKS,\n" in result
        assert "  PROPHETIC_FAILURE_PREVENTION,\n" in result
        assert "  DELEGATION_VIA_OA_ROUTER\n" in result
        assert result.endswith("]")

    def test_two_item_string_array_stays_single_line(self):
        """Arrays with only 1-2 items should remain single-line for compactness (GH#273).

        The 3-item threshold means short arrays stay compact.
        """
        result = emit_value(ListValue(items=["alpha", "beta"]))
        assert "\n" not in result
        assert result == "[alpha,beta]"

    def test_three_plus_string_items_roundtrip(self):
        """Emit-parse-emit roundtrip for 3+ item string arrays (GH#273, I1).

        Multi-line emission must produce output that parses back to an
        equivalent AST, proving I1 (syntactic fidelity).
        """
        doc = Document(
            name="TEST",
            sections=[
                Assignment(
                    key="SKILLS",
                    value=ListValue(items=["build_execution", "build_philosophy", "clarification_gate"]),
                ),
            ],
        )
        emitted1 = emit(doc)
        parsed = parse(emitted1)
        emitted2 = emit(parsed)
        assert emitted1 == emitted2

    def test_three_plus_string_items_in_assignment_context(self):
        """3+ item string arrays in nested assignments use correct indentation (GH#273)."""
        doc = Document(
            name="TEST",
            sections=[
                Block(
                    key="SECTION",
                    children=[
                        Assignment(
                            key="MUST_ALWAYS",
                            value=ListValue(
                                items=[
                                    "Write tests first",
                                    "Check coverage",
                                    "Run quality gates",
                                ]
                            ),
                        ),
                    ],
                )
            ],
        )
        result = emit(doc)
        # Assignment at indent 1, array items at indent 2
        assert "  MUST_ALWAYS::[\n" in result
        assert '    "Write tests first",\n' in result
        assert '    "Check coverage",\n' in result
        assert '    "Run quality gates"\n' in result
        assert "  ]" in result


class TestTrailingNewline:
    """GH#284: emit() must always return content ending with a newline."""

    def test_emit_ends_with_newline(self):
        """emit() output must end with exactly one newline (POSIX text file)."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="value")])
        result = emit(doc)
        assert result.endswith("\n"), "emit() must end with trailing newline"
        assert not result.endswith("\n\n"), "emit() must not end with double newline"

    def test_emit_ends_with_newline_empty_doc(self):
        """Even an empty document should end with a newline."""
        doc = Document(name="EMPTY", sections=[])
        result = emit(doc)
        assert result.endswith("\n"), "emit() must end with trailing newline for empty doc"

    def test_emit_trailing_newline_with_format_options(self):
        """Trailing newline must survive format_options application."""
        doc = Document(name="TEST", sections=[Assignment(key="KEY", value="value")])
        opts = FormatOptions(strip_comments=True)
        result = emit(doc, format_options=opts)
        assert result.endswith("\n"), "emit() with format_options must end with trailing newline"


class TestBug299HyphenatedIdentifiers:
    """GH#299: Hyphenated identifiers must NOT be quoted.

    The lexer (_is_valid_identifier_char) explicitly allows hyphens in identifiers,
    but the emitter's IDENTIFIER_PATTERN excludes hyphens, causing parser-emitter
    asymmetry. Values like 'ho-mode' and 'prophetic-intelligence' get spuriously quoted.
    """

    def test_hyphenated_identifier_not_quoted(self):
        """Simple hyphenated identifier should not be quoted."""
        assert not needs_quotes("ho-mode"), "ho-mode should not need quotes"

    def test_hyphenated_identifier_in_list_not_quoted(self):
        """Hyphenated identifiers in a list should emit unquoted."""
        result = emit_value(ListValue(items=["ho-mode", "prophetic-intelligence"]))
        assert result == "[ho-mode,prophetic-intelligence]"
        assert '"' not in result

    def test_multi_segment_hyphenated_not_quoted(self):
        """Multi-segment hyphenated identifiers should not be quoted."""
        assert not needs_quotes("build-anti-patterns")

    def test_trailing_hyphen_still_quoted(self):
        """Trailing hyphen is not a valid identifier -- should be quoted."""
        assert needs_quotes("bad-"), "Trailing hyphen should need quotes"

    def test_leading_hyphen_still_quoted(self):
        """Leading hyphen is not a valid identifier start -- should be quoted."""
        assert needs_quotes("-bad"), "Leading hyphen should need quotes"

    def test_hyphenated_round_trip(self):
        """Parse -> emit -> parse -> emit must be idempotent for hyphenated identifiers."""
        original = "===TEST===\nSKILLS::[ho-mode,prophetic-intelligence]\n===END==="
        doc1 = parse(original)
        emitted1 = emit(doc1)
        doc2 = parse(emitted1)
        emitted2 = emit(doc2)
        assert emitted1 == emitted2
        assert '"ho-mode"' not in emitted1
        assert '"prophetic-intelligence"' not in emitted1

    def test_dotted_hyphenated_not_quoted(self):
        """Identifiers with both dots and hyphens should not be quoted."""
        assert not needs_quotes("module.sub-component")


class TestBug300MultiArgAnnotation:
    """GH#300: Multi-arg constructor annotations must NOT be quoted.

    The emitter's ANNOTATION_PATTERN only accepts a single qualifier.
    Multi-arg qualifiers like NEVER<PEDANTIC,DISMISSIVE,VAGUE> fail the
    pattern and get quoted.
    """

    def test_multi_arg_annotation_not_quoted(self):
        """Multi-arg annotation NEVER<A,B,C> should not need quotes."""
        assert not needs_quotes("NEVER<PEDANTIC,DISMISSIVE,VAGUE>")

    def test_single_arg_annotation_still_works(self):
        """Single-arg annotation should still work."""
        assert not needs_quotes("ATHENA<strategic_wisdom>")

    def test_multi_arg_annotation_in_list(self):
        """Multi-arg annotations in a list should emit unquoted."""
        result = emit_value(
            ListValue(
                items=[
                    "NEVER<PEDANTIC,DISMISSIVE,VAGUE>",
                    "ALWAYS<CONSTRUCTIVE,EDUCATIONAL,SPECIFIC>",
                ]
            )
        )
        assert '"NEVER<PEDANTIC,DISMISSIVE,VAGUE>"' not in result
        assert '"ALWAYS<CONSTRUCTIVE,EDUCATIONAL,SPECIFIC>"' not in result

    def test_multi_arg_annotation_first_emit_unquoted(self):
        """Parse constructor -> emit must produce unquoted multi-arg annotations.

        Note: full round-trip (emit -> re-parse -> emit) requires lexer support for
        commas inside <> which is a separate lexer issue. This test verifies the
        emitter side: the first emission must not quote multi-arg annotations.
        """
        original = (
            "===TEST===\nGATES::NEVER[PEDANTIC,DISMISSIVE,VAGUE] ALWAYS[CONSTRUCTIVE,EDUCATIONAL,SPECIFIC]\n===END==="
        )
        doc1 = parse(original)
        emitted1 = emit(doc1)
        assert "NEVER<PEDANTIC,DISMISSIVE,VAGUE>" in emitted1
        assert "ALWAYS<CONSTRUCTIVE,EDUCATIONAL,SPECIFIC>" in emitted1
        assert '"NEVER<' not in emitted1

    def test_two_arg_annotation_not_quoted(self):
        """Two-arg annotation should also not be quoted."""
        assert not needs_quotes("NEVER<X,Y>")

    def test_empty_annotation_not_quoted(self):
        """Empty annotation FOO<> should not be quoted."""
        assert not needs_quotes("FOO<>")


class TestBug301UnicodeOperators:
    """GH#301: Values with Unicode semantic operators must NOT be quoted.

    The spec (section 3b::QUOTING_RULES) exempts defined operators in expressions.
    Unicode operators like A->B, X|Y, P&Q normalize to their Unicode forms
    and should NOT be quoted. The emitter's needs_quotes() is ASCII-only.
    """

    def test_synthesis_operator_not_quoted(self):
        """Value with synthesis operator should not be quoted."""
        assert not needs_quotes("PREVENT_CHAOS\u2295ELEVATE_EXCELLENCE")

    def test_flow_operator_not_quoted(self):
        """Value with flow arrow should not be quoted."""
        assert not needs_quotes("A\u2192B\u2192C")

    def test_constraint_operator_not_quoted(self):
        """Value with constraint operator should not be quoted."""
        assert not needs_quotes("P\u2227Q")

    def test_alternative_operator_not_quoted(self):
        """Value with alternative operator should not be quoted."""
        assert not needs_quotes("X\u2228Y")

    def test_tension_operator_not_quoted(self):
        """Value with tension operator should not be quoted."""
        assert not needs_quotes("Speed\u21ccQuality")

    def test_concat_operator_not_quoted(self):
        """Value with concat operator should not be quoted."""
        assert not needs_quotes("A\u29faB")

    def test_complex_expression_not_quoted(self):
        """Complex expression with multiple operators should not be quoted."""
        assert not needs_quotes("I1\u2295I4\u2227I5")

    def test_unicode_operator_round_trip(self):
        """Parse -> emit -> parse -> emit for expressions with Unicode operators."""
        original = "===TEST===\nMISSION::PREVENT_CHAOS\u2295ELEVATE_EXCELLENCE\n===END==="
        doc1 = parse(original)
        emitted1 = emit(doc1)
        assert '"PREVENT_CHAOS' not in emitted1
        doc2 = parse(emitted1)
        emitted2 = emit(doc2)
        assert emitted1 == emitted2

    def test_unicode_operator_in_emit_value(self):
        """emit_value should not quote expression values with Unicode operators."""
        result = emit_value("PREVENT_CHAOS\u2295ELEVATE_EXCELLENCE")
        assert result == "PREVENT_CHAOS\u2295ELEVATE_EXCELLENCE"
        assert '"' not in result


class TestBug304AnnotationMultiline:
    """GH#304: 2-item annotation lists should go multi-line.

    The hard-coded threshold `non_absent_count >= 3` means 2-item annotation
    lists stay single-line while 3-item ones go multi-line. Annotation items
    (NAME<qualifier>) are structured content that should always trigger
    multi-line emission regardless of count.
    """

    def test_two_annotation_items_go_multiline(self):
        """A list of 2 annotation items should emit multi-line."""
        items = ["MNEMOSYNE<pattern_recall>", "HERMES<elegant_translation>"]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result, f"Expected multi-line for 2 annotation items, got: {result}"

    def test_three_annotation_items_still_multiline(self):
        """A list of 3 annotation items should also emit multi-line (regression guard)."""
        items = [
            "ATHENA<strategic_wisdom>",
            "PROMETHEUS<creative_fire>",
            "HEPHAESTUS<implementation_craft>",
        ]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result

    def test_two_plain_items_stay_single_line(self):
        """Plain 2-item lists should still stay single-line (GH#273 behavior preserved)."""
        result = emit_value(ListValue(items=["alpha", "beta"]))
        assert "\n" not in result
        assert result == "[alpha,beta]"

    def test_annotation_multiline_format(self):
        """Verify exact multi-line format for 2-item annotation list."""
        items = ["MNEMOSYNE<pattern_recall>", "HERMES<elegant_translation>"]
        result = emit_value(ListValue(items=items))
        assert "  MNEMOSYNE<pattern_recall>,\n" in result
        assert "  HERMES<elegant_translation>\n" in result

    def test_mixed_annotation_and_plain_multiline(self):
        """A list mixing annotation and plain items should go multi-line if any annotation present."""
        items = ["plain_item", "ATHENA<strategic_wisdom>"]
        result = emit_value(ListValue(items=items))
        assert "[\n" in result

    def test_annotation_multiline_round_trip(self):
        """Parse -> emit -> parse -> emit for 2-item annotation lists must be idempotent."""
        doc = Document(
            name="TEST",
            sections=[
                Assignment(
                    key="ARCHETYPE",
                    value=ListValue(
                        items=[
                            "MNEMOSYNE<pattern_recall>",
                            "HERMES<elegant_translation>",
                        ]
                    ),
                ),
            ],
        )
        emitted1 = emit(doc)
        doc2 = parse(emitted1)
        emitted2 = emit(doc2)
        assert emitted1 == emitted2
