# Verifies: REQ-p00061-A+B+C
# Validates REQ-p00050-A, REQ-p00050-C, REQ-p00050-D
# Validates REQ-o00050-A, REQ-o00050-B, REQ-o00050-C, REQ-o00050-D, REQ-o00050-E
"""Tests for Graph Builder - builds TraceGraph from parsed content."""

from pathlib import Path

import pytest

from elspais.graph import NodeKind
from elspais.graph.builder import GraphBuilder
from elspais.graph.parsers import ParsedContent
from elspais.graph.relations import EdgeKind
from tests.core.graph_test_helpers import (
    build_graph,
    children_string,
    incoming_edges_string,
    make_code_ref,
    make_journey,
    make_requirement,
    make_test_ref,
    make_test_result,
    outgoing_edges_string,
    parents_string,
)


@pytest.fixture
def sample_requirements():
    """Sample parsed requirement content."""
    return [
        ParsedContent(
            content_type="requirement",
            start_line=1,
            end_line=10,
            raw_text="...",
            parsed_data={
                "id": "REQ-p00001",
                "title": "User Auth",
                "level": "PRD",
                "status": "Active",
                "implements": [],
                "assertions": [
                    {"label": "A", "text": "Users can log in"},
                    {"label": "B", "text": "Users can reset password"},
                ],
            },
        ),
        ParsedContent(
            content_type="requirement",
            start_line=15,
            end_line=25,
            raw_text="...",
            parsed_data={
                "id": "REQ-o00001",
                "title": "Login Form",
                "level": "OPS",
                "status": "Active",
                "implements": ["REQ-p00001-A"],
                "assertions": [],
            },
        ),
    ]


class TestGraphBuilder:
    """Tests for GraphBuilder class."""

    # Implements: REQ-o00050-A
    def test_build_creates_nodes(self, sample_requirements):
        builder = GraphBuilder()

        for req in sample_requirements:
            builder.add_parsed_content(req)

        graph = builder.build()

        assert graph.find_by_id("REQ-p00001") is not None
        assert graph.find_by_id("REQ-o00001") is not None

    # Implements: REQ-o00050-D
    def test_build_creates_assertion_nodes(self, sample_requirements):
        builder = GraphBuilder()

        for req in sample_requirements:
            builder.add_parsed_content(req)

        graph = builder.build()

        assert graph.find_by_id("REQ-p00001-A") is not None
        assert graph.find_by_id("REQ-p00001-B") is not None

    # Implements: REQ-o00050-D
    def test_build_links_assertions_to_parent(self, sample_requirements):
        builder = GraphBuilder()

        for req in sample_requirements:
            builder.add_parsed_content(req)

        graph = builder.build()

        parent = graph.find_by_id("REQ-p00001")
        assertion_a = graph.find_by_id("REQ-p00001-A")

        assert "REQ-p00001-A" in children_string(parent)
        assert "REQ-p00001" in parents_string(assertion_a)

    # Implements: REQ-o00050-C
    def test_build_creates_implements_edges(self, sample_requirements):
        builder = GraphBuilder()

        for req in sample_requirements:
            builder.add_parsed_content(req)

        graph = builder.build()

        ops_req = graph.find_by_id("REQ-o00001")
        prd_req = graph.find_by_id("REQ-p00001")

        # OPS req should be child of parent REQ (not assertion node)
        # with assertion_targets indicating which assertions it implements
        assert "REQ-p00001" in parents_string(ops_req)

        # Verify the edge has assertion_targets set
        for edge in prd_req.iter_outgoing_edges():
            if edge.target.id == "REQ-o00001":
                assert edge.assertion_targets == ["A"]
                break
        else:
            pytest.fail("Expected edge from REQ-p00001 to REQ-o00001 not found")

    # Implements: REQ-p00050-A
    def test_roots_are_top_level_requirements(self, sample_requirements):
        builder = GraphBuilder()

        for req in sample_requirements:
            builder.add_parsed_content(req)

        graph = builder.build()

        assert graph.has_root("REQ-p00001")
        assert not graph.has_root("REQ-o00001")  # Has parent via implements


class TestBodyTextNotStored:
    """After cleanup, body_text must not be stored in node content."""

    # Implements: REQ-d00131-B
    def test_body_text_not_stored_after_build(self, sample_requirements):
        """After cleanup, body_text must not be stored in node content."""
        builder = GraphBuilder()
        for req in sample_requirements:
            builder.add_parsed_content(req)
        graph = builder.build()

        req = graph.find_by_id("REQ-p00001")
        assert req is not None
        assert req.get_field("body_text") is None


class TestTraceGraph:
    """Tests for TraceGraph container."""

    # Implements: REQ-p00050-A
    def test_find_by_id(self, sample_requirements):
        builder = GraphBuilder()
        for req in sample_requirements:
            builder.add_parsed_content(req)
        graph = builder.build()

        node = graph.find_by_id("REQ-p00001")

        assert node is not None
        assert node.id == "REQ-p00001"

    # Implements: REQ-p00050-A
    def test_find_by_id_not_found(self, sample_requirements):
        builder = GraphBuilder()
        for req in sample_requirements:
            builder.add_parsed_content(req)
        graph = builder.build()

        node = graph.find_by_id("NONEXISTENT")

        assert node is None

    # Implements: REQ-d00051-F
    def test_all_nodes_iterator(self, sample_requirements):
        builder = GraphBuilder()
        for req in sample_requirements:
            builder.add_parsed_content(req)
        graph = builder.build()

        all_nodes = list(graph.all_nodes())

        # 2 reqs + 2 assertions = 4 nodes exactly
        assert len(all_nodes) == 4

    # Implements: REQ-d00130-E
    def test_nodes_by_kind(self, sample_requirements):
        builder = GraphBuilder()
        for req in sample_requirements:
            builder.add_parsed_content(req)
        graph = builder.build()

        assertions = list(graph.nodes_by_kind(NodeKind.ASSERTION))

        assert len(assertions) == 2


class TestBuilderContentTypes:
    """Tests for builder handling different content types."""

    # Implements: REQ-o00050-A
    def test_build_creates_journey_nodes(self):
        """Builder creates USER_JOURNEY nodes from journey content."""
        graph = build_graph(
            make_journey("UJ-001", title="Login Flow", actor="User", goal="Sign in"),
        )

        node = graph.find_by_id("UJ-001")
        assert node is not None
        assert node.kind == NodeKind.USER_JOURNEY
        assert node.get_label() == "Login Flow"

    # Implements: REQ-d00128-D
    def test_build_creates_code_ref_nodes(self):
        """Builder creates CODE nodes from code_ref content."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_code_ref(["REQ-d00001"], source_path="src/auth.py", start_line=10),
        )

        # Code ref creates a node linked to the requirement
        req = graph.find_by_id("REQ-d00001")
        assert "code:src/auth.py:10" in children_string(req)

        code_node = graph.find_by_id("code:src/auth.py:10")
        assert code_node is not None
        assert code_node.kind == NodeKind.CODE

    # Implements: REQ-d00128-D
    def test_build_creates_test_ref_nodes(self):
        """Builder creates TEST nodes from test_ref content."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(["REQ-d00001"], source_path="tests/test_auth.py", start_line=5),
        )

        # Test ref creates a node linked to the requirement
        req = graph.find_by_id("REQ-d00001")
        assert "test:tests/test_auth.py:5" in children_string(req)

        test_node = graph.find_by_id("test:tests/test_auth.py:5")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

    # Implements: REQ-o00050-C
    def test_build_creates_refines_edges(self):
        """Builder creates REFINES edges from refines references."""
        graph = build_graph(
            make_requirement("REQ-p00001", level="PRD"),
            make_requirement("REQ-p00002", level="PRD", refines=["REQ-p00001"]),
        )

        parent = graph.find_by_id("REQ-p00001")

        # Verify refines edge exists
        assert "REQ-p00002" in children_string(parent)
        assert "REQ-p00001->REQ-p00002:refines" in outgoing_edges_string(parent)

    def test_REQ_d00071_B_build_ignores_missing_targets(self):
        """REQ-d00071-B: Builder handles references to non-existent targets gracefully.

        Node with broken implements reference has no parent (broken link).
        As a parentless REQUIREMENT, it becomes a root.
        """
        # This should not raise an error
        graph = build_graph(
            make_requirement("REQ-o00001", level="OPS", implements=["REQ-NONEXISTENT"]),
        )

        req = graph.find_by_id("REQ-o00001")
        assert req is not None
        # Parentless REQUIREMENT → root
        assert graph.has_root("REQ-o00001")
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-o00001" not in orphan_ids

    # Implements: REQ-d00127-C
    def test_node_content_fields_accessible(self):
        """Node content fields are accessible via get_field()."""
        graph = build_graph(
            make_requirement(
                "REQ-p00001",
                title="Authentication",
                level="PRD",
                status="Active",
                hash_value="abc12345",
            ),
        )

        node = graph.find_by_id("REQ-p00001")

        # Content fields accessible via public API
        assert node.get_field("level") == "PRD"
        assert node.get_field("status") == "Active"
        assert node.get_field("hash") == "abc12345"
        # Convenience properties work too
        assert node.level == "PRD"
        assert node.status == "Active"
        assert node.hash == "abc12345"

    # Implements: REQ-d00127-E
    def test_test_result_linked_to_test(self):
        """TEST_RESULT nodes should be children of their parent TEST."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(["REQ-d00001"], source_path="tests/test_module.py", start_line=1),
            make_test_result(
                "result-1",
                status="passed",
                test_id="test:tests/test_module.py:1",
            ),
        )

        test = graph.find_by_id("test:tests/test_module.py:1")
        assert test is not None

        children = list(test.iter_children())
        assert len(children) == 1
        assert children[0].id == "result-1"
        assert children[0].kind == NodeKind.RESULT

    # Implements: REQ-d00127-E
    def test_test_result_without_test_id_not_linked(self):
        """TEST_RESULT without test_id should not be linked to any parent."""
        graph = build_graph(
            make_test_result("orphan-result", status="passed", test_id=None),
        )

        result = graph.find_by_id("orphan-result")
        assert result is not None
        # FILE parent exists from build_graph, but no domain parent (TEST via YIELDS)
        from elspais.graph.GraphNode import NodeKind as NK

        domain_parents = [p for p in result.iter_parents() if p.kind != NK.FILE]
        assert len(domain_parents) == 0


class TestTestToRequirementLinking:
    """Tests for linking tests to requirements via test names."""

    # Implements: REQ-o00050-C
    def test_test_result_without_scanner_test_is_broken_ref(self):
        """TEST_RESULT with test_id but no scanner TEST node creates a broken reference."""
        graph = build_graph(
            make_test_result(
                "result-1",
                status="passed",
                test_id="test:TestAuth::test_login",
                name="test_login",
                classname="TestAuth",
            ),
        )

        # TEST node should NOT be auto-created
        test = graph.find_by_id("test:TestAuth::test_login")
        assert test is None

        # Result should exist but have broken reference to test_id
        result = graph.find_by_id("result-1")
        assert result is not None
        assert graph.has_broken_references()
        broken = [
            br for br in graph.broken_references() if br.target_id == "test:TestAuth::test_login"
        ]
        assert len(broken) == 1

    # Implements: REQ-o00050-C
    def test_test_with_req_in_name_validates_requirement(self):
        """Scanner-created TEST linked to REQ, with TEST_RESULT child."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            # Scanner-created TEST node
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                function_name="test_REQ_d00001_login",
                class_name="TestAuth",
            ),
            make_test_result(
                "result-1",
                status="passed",
                test_id="test:tests/test_auth.py::TestAuth::test_REQ_d00001_login",
                name="test_REQ_d00001_login",
                classname="tests.test_auth.TestAuth",
            ),
            repo_root=Path("."),
        )

        # TEST node should exist and be child of REQ
        test = graph.find_by_id("test:tests/test_auth.py::TestAuth::test_REQ_d00001_login")
        assert test is not None

        # Test should be child of requirement via VALIDATES edge
        req = graph.find_by_id("REQ-d00001")
        assert req is not None
        assert "test:tests/test_auth.py::TestAuth::test_REQ_d00001_login" in children_string(req)

        # Result should be linked to TEST
        result = graph.find_by_id("result-1")
        assert result is not None
        assert "test:tests/test_auth.py::TestAuth::test_REQ_d00001_login" in parents_string(result)

    # Implements: REQ-o00050-C
    def test_test_with_assertion_in_name_validates_assertion(self):
        """Test with REQ-xxx-A in name creates VALIDATES edge with assertion_targets."""
        graph = build_graph(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "User can login"},
                    {"label": "B", "text": "User sees error on failure"},
                ],
            ),
            # Scanner-created TEST node with assertion reference
            make_test_ref(
                ["REQ-p00001-A"],
                source_path="tests/test_auth.py",
                function_name="test_REQ_p00001_A_login",
                class_name="TestAuth",
            ),
            make_test_result(
                "result-1",
                status="passed",
                test_id="test:tests/test_auth.py::TestAuth::test_REQ_p00001_A_login",
                name="test_REQ_p00001_A_login",
                classname="tests.test_auth.TestAuth",
            ),
            repo_root=Path("."),
        )

        # TEST node should exist
        test = graph.find_by_id("test:tests/test_auth.py::TestAuth::test_REQ_p00001_A_login")
        assert test is not None

        # Test should be linked to parent REQ with assertion_targets
        req = graph.find_by_id("REQ-p00001")
        assert "test:tests/test_auth.py::TestAuth::test_REQ_p00001_A_login" in children_string(req)

        # Verify edge has assertion_targets set
        for edge in req.iter_outgoing_edges():
            if edge.target.id == "test:tests/test_auth.py::TestAuth::test_REQ_p00001_A_login":
                assert edge.assertion_targets == ["A"]
                break
        else:
            pytest.fail("Expected edge with assertion_targets not found")

    # Implements: REQ-o00050-C
    def test_test_with_multiple_reqs_validates_all(self):
        """Scanner TEST with multiple REQ refs validates all requirements."""
        test_id = "test:tests/test_auth.py::TestAuth::test_REQ_d00001_REQ_d00002_combined"
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_requirement("REQ-d00002", level="DEV"),
            # Scanner-created TEST node referencing both REQs
            make_test_ref(
                ["REQ-d00001", "REQ-d00002"],
                source_path="tests/test_auth.py",
                function_name="test_REQ_d00001_REQ_d00002_combined",
                class_name="TestAuth",
            ),
            make_test_result(
                "result-1",
                status="passed",
                test_id=test_id,
                name="test_REQ_d00001_REQ_d00002_combined",
                classname="tests.test_auth.TestAuth",
            ),
            repo_root=Path("."),
        )

        test = graph.find_by_id(test_id)
        assert test is not None

        # Test should be child of both requirements
        req1 = graph.find_by_id("REQ-d00001")
        req2 = graph.find_by_id("REQ-d00002")
        assert test_id in children_string(req1)
        assert test_id in children_string(req2)

    # Implements: REQ-d00127-E
    def test_multiple_results_share_same_test_node(self):
        """Multiple TEST_RESULTs with same test_id share one scanner-created TEST node."""
        test_id = "test:tests/test_auth.py::TestAuth::test_REQ_d00001_login"
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            # Scanner-created TEST node
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                function_name="test_REQ_d00001_login",
                class_name="TestAuth",
            ),
            make_test_result(
                "result-run1",
                status="passed",
                test_id=test_id,
                name="test_REQ_d00001_login",
                classname="tests.test_auth.TestAuth",
            ),
            make_test_result(
                "result-run2",
                status="failed",
                test_id=test_id,
                name="test_REQ_d00001_login",
                classname="tests.test_auth.TestAuth",
            ),
            repo_root=Path("."),
        )

        # Should be exactly one TEST node
        test_nodes = list(graph.nodes_by_kind(NodeKind.TEST))
        test_matching = [t for t in test_nodes if t.id == test_id]
        assert len(test_matching) == 1

        # Both results should be children of that TEST
        test = test_matching[0]
        children = children_string(test)
        assert "result-run1" in children
        assert "result-run2" in children

    # Implements: REQ-d00127-E
    def test_test_without_req_in_name_is_orphan(self):
        """Test result without matching scanner TEST creates broken reference."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_result(
                "result-1",
                status="passed",
                test_id="test:tests/test_misc.py::TestMisc::test_something_else",
                name="test_something_else",
                classname="tests.test_misc.TestMisc",
                verifies=[],  # No REQ references
            ),
        )

        # No auto-created TEST node
        test = graph.find_by_id("test:tests/test_misc.py::TestMisc::test_something_else")
        assert test is None

        # Result exists, but its reference to test_id is broken
        result = graph.find_by_id("result-1")
        assert result is not None

        # Requirement should not have any test children
        req = graph.find_by_id("REQ-d00001")
        assert "test:" not in children_string(req)  # No test children at all


class TestGeneralizedOrphanDetection:
    """Tests for orphan detection across all node kinds.

    Validates REQ-d00071-A: Root = parentless with meaningful children.
    Validates REQ-d00071-B: Orphan = parentless with no meaningful children.
    Validates REQ-d00071-C: Satellite kinds (ASSERTION, TEST_RESULT) don't count.
    Validates REQ-d00071-D: Journey nodes follow same root/orphan rules.

    Validates that TEST, TEST_RESULT, and CODE nodes are tracked as
    orphan candidates, alongside the existing REQUIREMENT orphan detection.
    """

    # Implements: REQ-d00071-B
    def test_test_with_broken_validates_is_orphan(self):
        """TEST referencing non-existent REQ is an orphan."""
        graph = build_graph(
            make_test_ref(
                verifies=["REQ-nonexistent"],
                source_path="tests/test_foo.py",
                function_name="test_something",
            ),
        )

        orphans = list(graph.orphaned_nodes())
        orphan_ids = {n.id for n in orphans}
        assert any(n.kind == NodeKind.TEST for n in orphans)
        assert "test:tests/test_foo.py::test_something" in orphan_ids

    # Implements: REQ-d00071-A
    def test_test_with_valid_validates_not_orphan(self):
        """TEST with resolved VALIDATES link is not an orphan."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                verifies=["REQ-d00001"],
                source_path="tests/test_foo.py",
                function_name="test_something",
            ),
        )

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "test:tests/test_foo.py::test_something" not in orphan_ids

    # Implements: REQ-d00071-B
    def test_result_without_parent_test_is_orphan(self):
        """TEST_RESULT whose YIELDS target doesn't exist is an orphan + broken ref."""
        graph = build_graph(
            make_test_result(
                "result-orphan",
                status="passed",
                test_id="test:nonexistent::test_func",
                name="test_func",
            ),
        )

        orphans = list(graph.orphaned_nodes())
        orphan_ids = {n.id for n in orphans}
        assert "result-orphan" in orphan_ids
        assert any(n.kind == NodeKind.RESULT for n in orphans)

        # Also a broken reference
        assert graph.has_broken_references()
        broken_targets = {br.target_id for br in graph.broken_references()}
        assert "test:nonexistent::test_func" in broken_targets

    # Implements: REQ-d00071-B
    def test_result_without_test_id_is_orphan(self):
        """TEST_RESULT with no test_id is an orphan (no link possible)."""
        graph = build_graph(
            make_test_result("result-no-parent", status="passed", test_id=None),
        )

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "result-no-parent" in orphan_ids

    # Implements: REQ-d00071-B
    def test_code_with_broken_implements_is_orphan(self):
        """CODE referencing non-existent REQ is an orphan."""
        graph = build_graph(
            make_code_ref(
                implements=["REQ-nonexistent"],
                source_path="src/module.py",
            ),
        )

        orphans = list(graph.orphaned_nodes())
        orphan_ids = {n.id for n in orphans}
        assert any(n.kind == NodeKind.CODE for n in orphans)
        assert "code:src/module.py:1" in orphan_ids

    def test_REQ_d00071_B_requirement_without_children_is_root(self):
        """REQ-d00071-B: Parentless REQUIREMENT nodes are always roots.

        All parentless REQUIREMENT nodes become roots regardless of whether
        they have meaningful children. This ensures all requirements appear
        in the tree view (e.g. associated repo requirements without children).
        """
        graph = build_graph(
            make_requirement("REQ-p00001", level="PRD"),
            make_requirement("REQ-o00001", level="OPS"),  # No implements, no children
        )

        root_ids = {n.id for n in graph.iter_roots()}
        assert "REQ-p00001" in root_ids  # No children → still a root
        assert "REQ-o00001" in root_ids  # No children → still a root

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-p00001" not in orphan_ids
        assert "REQ-o00001" not in orphan_ids

    # Implements: REQ-d00127-E
    def test_result_linked_to_existing_test_not_orphan(self):
        """TEST_RESULT with resolved YIELDS edge is not an orphan."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                verifies=["REQ-d00001"],
                source_path="tests/test_module.py",
                function_name="test_func",
            ),
            make_test_result(
                "result-linked",
                status="passed",
                test_id="test:tests/test_module.py::test_func",
                name="test_func",
            ),
        )

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "result-linked" not in orphan_ids

    def test_REQ_d00071_C_req_with_only_assertions_is_root(self):
        """REQ-d00071-C: Requirement with only ASSERTION children is a root.

        Even though assertions are satellite kinds and don't count as
        meaningful children for other node types, REQUIREMENT nodes are
        always promoted to roots when parentless. This ensures all
        requirements appear in the tree view.
        """
        graph = build_graph(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First assertion"},
                    {"label": "B", "text": "Second assertion"},
                ],
            ),
        )

        # REQ-p00001 has assertion children but they are satellite kind
        req = graph.find_by_id("REQ-p00001")
        assert req is not None
        # Assertions exist as children
        child_kinds = {c.kind for c in req.iter_children()}
        assert NodeKind.ASSERTION in child_kinds

        # Requirement is a root (all parentless REQs are roots)
        root_ids = {n.id for n in graph.iter_roots()}
        assert "REQ-p00001" in root_ids

        # And NOT an orphan
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-p00001" not in orphan_ids

    def test_REQ_d00071_A_req_with_child_req_is_root(self):
        """REQ-d00071-A: Requirement with a child requirement is a root.

        A parentless PRD requirement that has an OPS child implementing it
        should be classified as a root (the OPS child is a meaningful,
        non-satellite child).
        """
        graph = build_graph(
            make_requirement("REQ-p00001", level="PRD"),
            make_requirement("REQ-o00001", level="OPS", implements=["REQ-p00001"]),
        )

        # PRD has a meaningful child (OPS requirement)
        root_ids = {n.id for n in graph.iter_roots()}
        assert "REQ-p00001" in root_ids

        # PRD is NOT an orphan
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-p00001" not in orphan_ids

    def test_REQ_d00071_C_test_with_only_results_is_orphan(self):
        """REQ-d00071-C: TEST node with only TEST_RESULT children is an orphan.

        TEST_RESULT is a satellite kind. A TEST node that has no validates
        link to a requirement (so it's parentless) but has TEST_RESULT
        children should still be classified as an orphan.
        """
        graph = build_graph(
            # Create a TEST node with no validates (no parent link)
            make_test_ref(
                verifies=[],
                source_path="tests/test_standalone.py",
                function_name="test_standalone_func",
            ),
            # Create a TEST_RESULT that links to the TEST via YIELDS
            make_test_result(
                "result-standalone",
                status="passed",
                test_id="test:tests/test_standalone.py::test_standalone_func",
                name="test_standalone_func",
            ),
        )

        test_node = graph.find_by_id("test:tests/test_standalone.py::test_standalone_func")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

        # TEST_RESULT is a child of the TEST
        child_kinds = {c.kind for c in test_node.iter_children()}
        assert NodeKind.RESULT in child_kinds

        # But TEST is still an orphan (TEST_RESULT is satellite)
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "test:tests/test_standalone.py::test_standalone_func" in orphan_ids

        # And NOT a root
        root_ids = {n.id for n in graph.iter_roots()}
        assert "test:tests/test_standalone.py::test_standalone_func" not in root_ids

    def test_REQ_d00071_D_journey_with_no_children_is_orphan(self):
        """REQ-d00071-D: Standalone USER_JOURNEY with no children is an orphan.

        A journey node that has no children at all should be classified as
        an orphan under the unified root/orphan classification rules.
        """
        graph = build_graph(
            make_journey("UJ-001", title="Login Flow", actor="User", goal="Sign in"),
        )

        journey = graph.find_by_id("UJ-001")
        assert journey is not None
        assert journey.kind == NodeKind.USER_JOURNEY

        # No children at all
        assert journey.child_count() == 0

        # Journey is an orphan
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "UJ-001" in orphan_ids

        # And NOT a root
        root_ids = {n.id for n in graph.iter_roots()}
        assert "UJ-001" not in root_ids

    def test_REQ_d00071_D_journey_with_req_children_is_root(self):
        """REQ-d00071-D: Journey with a requirement child is a root.

        A USER_JOURNEY that has a REQUIREMENT implementing it should be
        classified as a root (the requirement child is non-satellite/meaningful).
        """
        graph = build_graph(
            make_journey("UJ-001", title="Login Flow", actor="User", goal="Sign in"),
            make_requirement(
                "REQ-o00001",
                level="OPS",
                implements=["UJ-001"],
            ),
        )

        journey = graph.find_by_id("UJ-001")
        assert journey is not None
        assert journey.kind == NodeKind.USER_JOURNEY

        # Journey has a meaningful child (requirement)
        assert journey.child_count() > 0
        child_kinds = {c.kind for c in journey.iter_children()}
        assert NodeKind.REQUIREMENT in child_kinds

        # Journey is a root
        root_ids = {n.id for n in graph.iter_roots()}
        assert "UJ-001" in root_ids

        # And NOT an orphan
        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "UJ-001" not in orphan_ids

    def test_REQ_d00071_C_custom_satellite_kinds_override(self):
        """REQ-d00071-C: Custom satellite_kinds adds CODE as satellite.

        When satellite_kinds includes 'code', CODE children count as satellite.
        But parentless REQUIREMENT nodes are always roots regardless of
        satellite configuration.
        """
        builder = GraphBuilder(satellite_kinds=["assertion", "result", "code"])
        builder.add_parsed_content(
            make_requirement("REQ-p00001", level="PRD"),
        )
        builder.add_parsed_content(
            make_code_ref(
                implements=["REQ-p00001"],
                source_path="src/auth.py",
            ),
        )
        graph = builder.build()

        req = graph.find_by_id("REQ-p00001")
        assert req is not None
        # CODE is a child
        child_kinds = {c.kind for c in req.iter_children()}
        assert NodeKind.CODE in child_kinds

        # Parentless REQUIREMENT → always a root
        root_ids = {n.id for n in graph.iter_roots()}
        assert "REQ-p00001" in root_ids

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-p00001" not in orphan_ids

    def test_REQ_d00071_C_default_satellite_kinds_unchanged(self):
        """REQ-d00071-C: Default satellite_kinds still classifies assertions as satellite.

        When no satellite_kinds parameter is passed, the builder defaults
        to ASSERTION and TEST_RESULT as satellite kinds. Assertions are still
        satellite children, but parentless REQUIREMENT nodes are always roots.
        """
        builder = GraphBuilder()
        builder.add_parsed_content(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First assertion"},
                    {"label": "B", "text": "Second assertion"},
                ],
            ),
        )
        graph = builder.build()

        req = graph.find_by_id("REQ-p00001")
        assert req is not None
        # Assertions exist as children
        child_kinds = {c.kind for c in req.iter_children()}
        assert NodeKind.ASSERTION in child_kinds

        # Parentless REQUIREMENT → always a root
        root_ids = {n.id for n in graph.iter_roots()}
        assert "REQ-p00001" in root_ids

        orphan_ids = {n.id for n in graph.orphaned_nodes()}
        assert "REQ-p00001" not in orphan_ids

    def test_REQ_d00071_C_invalid_satellite_kind_raises_error(self):
        """REQ-d00071-C: Invalid satellite kind string raises ValueError.

        Passing an unrecognized string to satellite_kinds should raise
        a ValueError when NodeKind tries to parse it.
        """
        with pytest.raises(ValueError):
            GraphBuilder(satellite_kinds=["bogus"])


class TestCanonicalTestIds:
    """Tests for canonical TEST node ID generation from test_ref content."""

    def test_REQ_d00054_A_canonical_id_with_function_name(self):
        """Canonical ID uses :: separator with function name, no class."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                start_line=5,
                function_name="test_REQ_d00001_validates",
            ),
            repo_root=Path("."),
        )

        test_node = graph.find_by_id("test:tests/test_auth.py::test_REQ_d00001_validates")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

        req = graph.find_by_id("REQ-d00001")
        assert "test:tests/test_auth.py::test_REQ_d00001_validates" in children_string(req)

    def test_REQ_d00054_A_canonical_id_with_class_and_function(self):
        """Canonical ID includes ClassName when class_name is provided."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                start_line=10,
                function_name="test_REQ_d00001_login",
                class_name="TestAuth",
            ),
            repo_root=Path("."),
        )

        test_node = graph.find_by_id("test:tests/test_auth.py::TestAuth::test_REQ_d00001_login")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

        req = graph.find_by_id("REQ-d00001")
        assert "test:tests/test_auth.py::TestAuth::test_REQ_d00001_login" in children_string(req)

    def test_REQ_d00054_A_fallback_to_line_based_id(self):
        """Without function_name, falls back to line-based ID."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                start_line=5,
            ),
            repo_root=Path("."),
        )

        test_node = graph.find_by_id("test:tests/test_auth.py:5")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

        req = graph.find_by_id("REQ-d00001")
        assert "test:tests/test_auth.py:5" in children_string(req)

    def test_REQ_d00054_A_canonical_id_deduplicates(self):
        """Two test_refs with same function/path but different validates create one TEST node."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_requirement("REQ-d00002", level="DEV"),
            make_test_ref(
                ["REQ-d00001"],
                source_path="tests/test_auth.py",
                start_line=5,
                function_name="test_REQ_d00001_REQ_d00002_combined",
            ),
            make_test_ref(
                ["REQ-d00002"],
                source_path="tests/test_auth.py",
                start_line=5,
                function_name="test_REQ_d00001_REQ_d00002_combined",
            ),
            repo_root=Path("."),
        )

        # Only one TEST node should exist for this canonical ID
        test_nodes = [
            n
            for n in graph.nodes_by_kind(NodeKind.TEST)
            if n.id == "test:tests/test_auth.py::test_REQ_d00001_REQ_d00002_combined"
        ]
        assert len(test_nodes) == 1

        # The single TEST node should validate both requirements
        req1 = graph.find_by_id("REQ-d00001")
        req2 = graph.find_by_id("REQ-d00002")
        assert "test:tests/test_auth.py::test_REQ_d00001_REQ_d00002_combined" in children_string(
            req1
        )
        assert "test:tests/test_auth.py::test_REQ_d00001_REQ_d00002_combined" in children_string(
            req2
        )

    def test_REQ_d00054_A_canonical_id_relative_to_repo_root(self):
        """Absolute source_path is converted to relative using repo_root."""
        graph = build_graph(
            make_requirement("REQ-d00001", level="DEV"),
            make_test_ref(
                ["REQ-d00001"],
                source_path="/home/user/project/tests/test_auth.py",
                start_line=10,
                function_name="test_REQ_d00001_validates",
            ),
            repo_root=Path("/home/user/project"),
        )

        # The canonical ID should use the relative path
        test_node = graph.find_by_id("test:tests/test_auth.py::test_REQ_d00001_validates")
        assert test_node is not None
        assert test_node.kind == NodeKind.TEST

        req = graph.find_by_id("REQ-d00001")
        assert "test:tests/test_auth.py::test_REQ_d00001_validates" in children_string(req)


class TestValidatesEdges:
    """Tests for VALIDATES edge creation in GraphBuilder.

    Validates REQ-o00050-C: TraceGraphBuilder SHALL handle all relationship
    linking including validates.
    """

    def test_REQ_o00050_C_journey_validates_creates_edge(self):
        """JNY with Validates: REQ-p00012 creates VALIDATES edge to REQ node."""
        graph = build_graph(
            make_requirement("REQ-p00012", level="PRD", title="Product Requirement"),
            make_journey(
                "JNY-Dev-01",
                title="Dev Workflow",
                actor="Developer",
                goal="Implement feature",
                validates=["REQ-p00012"],
            ),
        )

        jny_node = graph.find_by_id("JNY-Dev-01")
        req_node = graph.find_by_id("REQ-p00012")

        assert jny_node is not None
        assert req_node is not None

        # The REQ node becomes the parent via target.link(source, edge_kind),
        # so the JNY node has incoming VALIDATES edges
        edges = incoming_edges_string(jny_node)
        assert "REQ-p00012->JNY-Dev-01:validates" in edges

        # Verify edge kind is VALIDATES
        found_validates_edge = False
        for edge in jny_node.iter_incoming_edges():
            if edge.source.id == "REQ-p00012" and edge.kind == EdgeKind.VALIDATES:
                found_validates_edge = True
                break
        assert found_validates_edge, "Expected VALIDATES edge from REQ-p00012 to JNY-Dev-01"

    def test_REQ_o00050_C_journey_validates_missing_target_broken_ref(self):
        """JNY with Validates: REQ-NONEXIST records broken reference."""
        graph = build_graph(
            make_journey(
                "JNY-Dev-02",
                title="Broken Ref Journey",
                actor="Developer",
                goal="Test broken ref",
                validates=["REQ-NONEXIST"],
            ),
        )

        jny_node = graph.find_by_id("JNY-Dev-02")
        assert jny_node is not None

        # Should have no domain incoming edges since target doesn't exist
        # (FILE parent exists from build_graph)
        from elspais.graph.GraphNode import NodeKind as NK

        domain_parents = [p for p in jny_node.iter_parents() if p.kind != NK.FILE]
        assert len(domain_parents) == 0

        # Should have a broken reference recorded
        assert graph.has_broken_references()
        broken = [
            br
            for br in graph.broken_references()
            if br.source_id == "JNY-Dev-02" and br.target_id == "REQ-NONEXIST"
        ]
        assert len(broken) == 1
        assert broken[0].edge_kind == "validates"

    def test_REQ_o00050_C_journey_validates_multiple_targets(self):
        """JNY with multiple Validates creates edges to all targets."""
        graph = build_graph(
            make_requirement("REQ-p00012", level="PRD"),
            make_requirement("REQ-d00042", level="DEV"),
            make_journey(
                "JNY-Dev-03",
                title="Multi Validates Journey",
                actor="Developer",
                goal="Test multiple validates",
                validates=["REQ-p00012", "REQ-d00042"],
            ),
        )

        jny_node = graph.find_by_id("JNY-Dev-03")
        assert jny_node is not None

        edges = incoming_edges_string(jny_node)
        assert "REQ-d00042->JNY-Dev-03:validates" in edges
        assert "REQ-p00012->JNY-Dev-03:validates" in edges

    def test_REQ_o00050_C_journey_is_orphan_without_children(self):
        """JNY nodes without meaningful children are orphans (REQ-d00071)."""
        graph = build_graph(
            make_journey(
                "JNY-Dev-04",
                title="Orphan Journey",
                actor="Developer",
                goal="Verify orphan status",
            ),
        )

        # Under unified root/orphan classification (REQ-d00071),
        # parentless nodes need meaningful children to be roots
        assert not graph.has_root("JNY-Dev-04")
        assert graph.find_by_id("JNY-Dev-04") is not None


class TestMultiAssertionExpansion:
    """Tests for multi-assertion expansion in GraphBuilder.

    Validates REQ-d00081-D, REQ-d00081-E, REQ-d00081-F, REQ-d00081-G:
    Multi-assertion compact references (e.g. REQ-p00001-A+B+C) are expanded
    into individual assertion references during link resolution.
    """

    def test_REQ_d00081_D_code_ref_multi_assertion_expands(self):
        """REQ-d00081-D: Code ref with REQ-p00001-A+B+C expands and links to all assertions."""
        builder = GraphBuilder()
        builder.add_parsed_content(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First"},
                    {"label": "B", "text": "Second"},
                    {"label": "C", "text": "Third"},
                ],
            ),
        )
        builder.add_parsed_content(
            make_code_ref(
                implements=["REQ-p00001-A+B+C"],
                source_path="src/auth.py",
                start_line=10,
            ),
        )
        graph = builder.build()

        # Code node should be a child of REQ-p00001 (linked via assertion resolution)
        req = graph.find_by_id("REQ-p00001")
        assert req is not None
        assert "code:src/auth.py:10" in children_string(req)

        # Verify edges have assertion_targets for A, B, and C
        code_node = graph.find_by_id("code:src/auth.py:10")
        assert code_node is not None
        assertion_targets_found = set()
        for edge in req.iter_outgoing_edges():
            if edge.target.id == code_node.id:
                if edge.assertion_targets:
                    for at in edge.assertion_targets:
                        assertion_targets_found.add(at)
        assert {"A", "B", "C"} == assertion_targets_found

    def test_REQ_d00081_G_single_assertion_passes_through(self):
        """REQ-d00081-G: Single assertion ref REQ-p00001-A passes through unchanged."""
        builder = GraphBuilder()
        builder.add_parsed_content(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First"},
                    {"label": "B", "text": "Second"},
                ],
            ),
        )
        builder.add_parsed_content(
            make_code_ref(
                implements=["REQ-p00001-A"],
                source_path="src/auth.py",
                start_line=10,
            ),
        )
        graph = builder.build()

        req = graph.find_by_id("REQ-p00001")
        assert "code:src/auth.py:10" in children_string(req)

        # Only assertion A should be targeted, not B
        for edge in req.iter_outgoing_edges():
            if edge.target.id == "code:src/auth.py:10":
                assert edge.assertion_targets == ["A"]
                break
        else:
            pytest.fail("Expected edge from REQ-p00001 to code:src/auth.py:10 not found")

    def test_REQ_d00081_E_custom_separator_works(self):
        """REQ-d00081-E: Custom separator '&' expands REQ-p00001-A&B&C correctly."""
        builder = GraphBuilder(multi_assertion_separator="&")
        builder.add_parsed_content(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First"},
                    {"label": "B", "text": "Second"},
                    {"label": "C", "text": "Third"},
                ],
            ),
        )
        builder.add_parsed_content(
            make_code_ref(
                implements=["REQ-p00001-A&B&C"],
                source_path="src/auth.py",
                start_line=10,
            ),
        )
        graph = builder.build()

        req = graph.find_by_id("REQ-p00001")
        assert "code:src/auth.py:10" in children_string(req)

        assertion_targets_found = set()
        for edge in req.iter_outgoing_edges():
            if edge.target.id == "code:src/auth.py:10":
                if edge.assertion_targets:
                    for at in edge.assertion_targets:
                        assertion_targets_found.add(at)
        assert {"A", "B", "C"} == assertion_targets_found

    def test_REQ_d00081_F_empty_separator_disables_expansion(self):
        """REQ-d00081-F: Empty separator disables expansion."""
        builder = GraphBuilder(multi_assertion_separator="")
        builder.add_parsed_content(
            make_requirement(
                "REQ-p00001",
                level="PRD",
                assertions=[
                    {"label": "A", "text": "First"},
                    {"label": "B", "text": "Second"},
                ],
            ),
        )
        builder.add_parsed_content(
            make_code_ref(
                implements=["REQ-p00001-A+B"],
                source_path="src/auth.py",
                start_line=10,
            ),
        )
        graph = builder.build()

        # With expansion disabled, "REQ-p00001-A+B" is treated as a literal ID
        # which doesn't exist, so it becomes a broken reference
        assert graph.has_broken_references()
        broken_targets = {br.target_id for br in graph.broken_references()}
        assert "REQ-p00001-A+B" in broken_targets

        # Code node should NOT be linked to the requirement
        req = graph.find_by_id("REQ-p00001")
        assert "code:src/auth.py:10" not in children_string(req)

    def test_REQ_d00081_D_test_ref_multi_assertion_expands(self):
        """REQ-d00081-D: Test ref with multi-assertion also expands, uniform across parser types."""
        builder = GraphBuilder()
        builder.add_parsed_content(
            make_requirement(
                "REQ-d00001",
                level="DEV",
                assertions=[
                    {"label": "A", "text": "First"},
                    {"label": "B", "text": "Second"},
                ],
            ),
        )
        builder.add_parsed_content(
            make_test_ref(
                verifies=["REQ-d00001-A+B"],
                source_path="tests/test_auth.py",
                start_line=5,
            ),
        )
        graph = builder.build()

        req = graph.find_by_id("REQ-d00001")
        assert req is not None
        assert "test:tests/test_auth.py:5" in children_string(req)

        assertion_targets_found = set()
        for edge in req.iter_outgoing_edges():
            if edge.target.id == "test:tests/test_auth.py:5":
                if edge.assertion_targets:
                    for at in edge.assertion_targets:
                        assertion_targets_found.add(at)
        assert {"A", "B"} == assertion_targets_found


class TestParseDirtyFlag:
    """Tests that has_redundant_refs in parsed_data causes parse_dirty on the built node.

    Validates: REQ-p00002-A
    """

    # Implements: REQ-p00002-A
    def test_has_redundant_refs_sets_parse_dirty(self):
        # Verifies: REQ-p00002-A
        """Requirement built from parsed_data with has_redundant_refs=True has parse_dirty=True."""
        content = ParsedContent(
            content_type="requirement",
            start_line=1,
            end_line=5,
            raw_text="",
            parsed_data={
                "id": "REQ-p00001",
                "title": "Redundant Refs Req",
                "level": "PRD",
                "status": "Active",
                "implements": ["REQ-p00002"],
                "refines": [],
                "assertions": [],
                "has_redundant_refs": True,
            },
        )
        builder = GraphBuilder()
        builder.add_parsed_content(content)
        graph = builder.build()

        node = graph.find_by_id("REQ-p00001")
        assert node is not None
        assert node.get_field("parse_dirty") is True

    # Implements: REQ-p00002-A
    def test_no_redundant_refs_does_not_set_parse_dirty(self):
        # Verifies: REQ-p00002-A
        """A requirement without has_redundant_refs does NOT have parse_dirty=True."""
        content = ParsedContent(
            content_type="requirement",
            start_line=1,
            end_line=5,
            raw_text="",
            parsed_data={
                "id": "REQ-p00001",
                "title": "Clean Refs Req",
                "level": "PRD",
                "status": "Active",
                "implements": ["REQ-p00002"],
                "refines": [],
                "assertions": [],
            },
        )
        builder = GraphBuilder()
        builder.add_parsed_content(content)
        graph = builder.build()

        node = graph.find_by_id("REQ-p00001")
        assert node is not None
        assert not node.get_field("parse_dirty")


class TestImplementsEdgeMetadata:
    """Tests for impl_start_line/impl_end_line metadata on IMPLEMENTS edges."""

    # Verifies: REQ-p00061-A
    def test_implements_edge_has_line_metadata(self):
        """IMPLEMENTS edge from CODE node carries impl_start_line and impl_end_line."""
        req = make_requirement(
            req_id="REQ-p00001",
            title="Test Requirement",
            level="PRD",
            assertions=[{"label": "A", "text": "Something"}],
        )
        code = ParsedContent(
            content_type="code_ref",
            start_line=42,
            end_line=42,
            raw_text="# Implements: REQ-p00001",
            parsed_data={
                "implements": ["REQ-p00001"],
                "verifies": [],
                "function_name": "do_something",
                "class_name": None,
                "function_line": 30,
                "function_end_line": 60,
            },
        )
        code.source_context = type("Ctx", (), {"source_id": "src/module.py"})()

        graph = build_graph(req, code)

        # Find the CODE node
        code_node = graph.find_by_id("code:src/module.py:42")
        assert code_node is not None
        assert code_node.get_field("function_line") == 30
        assert code_node.get_field("function_end_line") == 60

        # Check IMPLEMENTS edge metadata
        edges = [e for e in code_node.iter_incoming_edges() if e.kind == EdgeKind.IMPLEMENTS]
        assert len(edges) >= 1
        edge = edges[0]
        assert edge.metadata.get("impl_start_line") == 30
        assert edge.metadata.get("impl_end_line") == 60

    # Verifies: REQ-p00061-A
    def test_implements_edge_falls_back_to_parse_line(self):
        """IMPLEMENTS edge falls back to parse_line when function_line is not set."""
        req = make_requirement(
            req_id="REQ-p00001",
            title="Test Requirement",
            level="PRD",
        )
        code = ParsedContent(
            content_type="code_ref",
            start_line=15,
            end_line=15,
            raw_text="# Implements: REQ-p00001",
            parsed_data={
                "implements": ["REQ-p00001"],
                "verifies": [],
                "function_name": None,
                "class_name": None,
                "function_line": 0,
                "function_end_line": 0,
            },
        )
        code.source_context = type("Ctx", (), {"source_id": "src/module.py"})()

        graph = build_graph(req, code)

        code_node = graph.find_by_id("code:src/module.py:15")
        assert code_node is not None

        edges = [e for e in code_node.iter_incoming_edges() if e.kind == EdgeKind.IMPLEMENTS]
        assert len(edges) >= 1
        edge = edges[0]
        # Falls back to parse_line
        assert edge.metadata.get("impl_start_line") == 15
        # Falls back to parse_end_line
        assert edge.metadata.get("impl_end_line") == 15

    # Verifies: REQ-p00061-A
    def test_implements_edge_to_assertion_has_metadata(self):
        """IMPLEMENTS edge targeting an assertion still gets line metadata."""
        req = make_requirement(
            req_id="REQ-p00001",
            title="Test Requirement",
            level="PRD",
            assertions=[{"label": "A", "text": "Something"}],
        )
        code = ParsedContent(
            content_type="code_ref",
            start_line=10,
            end_line=10,
            raw_text="# Implements: REQ-p00001-A",
            parsed_data={
                "implements": ["REQ-p00001-A"],
                "verifies": [],
                "function_name": "my_func",
                "class_name": "MyClass",
                "function_line": 5,
                "function_end_line": 25,
            },
        )
        code.source_context = type("Ctx", (), {"source_id": "src/module.py"})()

        graph = build_graph(req, code)

        code_node = graph.find_by_id("code:src/module.py:10")
        assert code_node is not None

        edges = [e for e in code_node.iter_incoming_edges() if e.kind == EdgeKind.IMPLEMENTS]
        assert len(edges) >= 1
        edge = edges[0]
        assert edge.metadata.get("impl_start_line") == 5
        assert edge.metadata.get("impl_end_line") == 25


class TestStructuresEdgeRenderOrder:
    """Tests for render_order metadata on STRUCTURES edges."""

    # Implements: REQ-d00131-B
    def test_structures_edges_carry_render_order(self):
        """STRUCTURES edges should have render_order metadata based on line number."""
        req = make_requirement(
            "REQ-p00001",
            title="Requirement with assertions and remainder",
            level="PRD",
            assertions=[
                {"label": "A", "text": "First assertion", "line": 5},
                {"label": "B", "text": "Second assertion", "line": 10},
            ],
            source_path="spec/test.md",
            start_line=1,
            end_line=20,
        )

        graph = build_graph(req)

        req_node = graph.find_by_id("REQ-p00001")
        assert req_node is not None

        # Get STRUCTURES edges from the requirement node
        structures_edges = [
            e for e in req_node.iter_outgoing_edges() if e.kind == EdgeKind.STRUCTURES
        ]

        # Should have at least 2 STRUCTURES children (assertions)
        assert (
            len(structures_edges) >= 2
        ), f"Expected at least 2 STRUCTURES edges, got {len(structures_edges)}"

        # Every STRUCTURES edge must have render_order in metadata
        for edge in structures_edges:
            assert (
                "render_order" in edge.metadata
            ), f"STRUCTURES edge to {edge.target.id} missing render_order metadata"
            assert isinstance(
                edge.metadata["render_order"], float
            ), f"render_order should be float, got {type(edge.metadata['render_order'])}"

        # render_order values must be monotonically increasing
        orders = [e.metadata["render_order"] for e in structures_edges]
        for i in range(1, len(orders)):
            assert orders[i] > orders[i - 1], f"render_order not monotonically increasing: {orders}"


# Implements: REQ-d00131-B
def test_add_assertion_sets_render_order():
    """add_assertion() must set render_order on the new STRUCTURES edge."""
    builder = GraphBuilder()
    builder.add_parsed_content(
        ParsedContent(
            content_type="requirement",
            parsed_data={
                "id": "REQ-p00001",
                "title": "Test Requirement",
                "level": "PRD",
                "status": "Active",
                "assertions": [
                    {"label": "A", "text": "First assertion"},
                    {"label": "B", "text": "Second assertion"},
                ],
                "implements": [],
                "refines": [],
            },
            start_line=1,
            end_line=10,
            raw_text="## REQ-p00001: Test Requirement",
        )
    )
    graph = builder.build()

    # Collect existing render_orders on STRUCTURES edges
    req = graph.find_by_id("REQ-p00001")
    existing_orders = []
    for edge in req.iter_outgoing_edges():
        if edge.kind == EdgeKind.STRUCTURES:
            existing_orders.append(edge.metadata.get("render_order", 0.0))
    max_existing = max(existing_orders) if existing_orders else 0.0

    # Add a new assertion via the public API
    graph.add_assertion("REQ-p00001", "ZZ", "New test assertion")

    # Find the new STRUCTURES edge to the ZZ assertion
    new_edge = None
    for edge in req.iter_outgoing_edges():
        if edge.kind == EdgeKind.STRUCTURES and edge.target.id == "REQ-p00001-ZZ":
            new_edge = edge
            break

    assert new_edge is not None, "New assertion edge not found"
    assert (
        "render_order" in new_edge.metadata
    ), "render_order missing from new STRUCTURES edge metadata"
    assert new_edge.metadata["render_order"] > max_existing, (
        f"New render_order {new_edge.metadata['render_order']} should be > "
        f"max existing {max_existing}"
    )
