"""Tests for prerequisite validation and orchestrator from llm/missions/validation.py.

Tests verify:
- Prerequisite graph validation (cycles, depth, roots)
- Mission orchestrator coordinates all validation passes
- Auto-fix behavior produces valid mission sets
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from hypothesis import given, settings
from hypothesis import strategies as st

from tests.conftest import make_mission_item

if TYPE_CHECKING:
    from pathlib import Path
from unfamiliar.constants import (
    MIN_ROOT_MISSIONS_PER_CATEGORY,
    InvestigationCategory,
)
from unfamiliar.llm.missions.schemas import ChainResponse, MissionFileRefResponse
from unfamiliar.llm.missions.validation import (
    ValidationResult,
    validate_category_missions,
    validate_prerequisites,
)

# --- Helper functions ---


def create_file_with_lines(tmp_path: Path, relative_path: str, line_count: int) -> Path:
    """Create a file with the specified number of lines.

    Args:
        tmp_path: Temporary directory fixture.
        relative_path: Path relative to tmp_path.
        line_count: Number of lines to create.

    Returns:
        Path to the created file.
    """
    file_path = tmp_path / relative_path
    file_path.parent.mkdir(parents=True, exist_ok=True)
    content = "\n".join(f"Line {i + 1}" for i in range(line_count))
    file_path.write_text(content, encoding="utf-8")
    return file_path


def make_chain(
    name_plain: str,
    mission_titles: list[str],
    name: str | None = None,
    description: str = "Test chain",
) -> ChainResponse:
    """Create a ChainResponse for testing."""
    return ChainResponse(
        name=name or name_plain,
        name_plain=name_plain,
        description=description,
        mission_titles=mission_titles,
    )


def make_file_ref(
    file_path: str,
    line_start: int | None = None,
    line_end: int | None = None,
    description: str = "Test file reference",
) -> MissionFileRefResponse:
    """Create a MissionFileRefResponse for testing."""
    return MissionFileRefResponse(
        file_path=file_path,
        line_start=line_start,
        line_end=line_end,
        description=description,
    )


class TestValidatePrerequisitesHappyPath:
    """Tests for validate_prerequisites() happy path scenarios."""

    def test_no_prerequisites_all_root_missions_unchanged(self):
        """All root missions (empty prereqs) pass through unchanged."""
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
            make_mission_item(title_plain="Mission B", prerequisite_titles=[]),
            make_mission_item(title_plain="Mission C", prerequisite_titles=[]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 3
        assert result[0].prerequisite_titles == []
        assert result[1].prerequisite_titles == []
        assert result[2].prerequisite_titles == []
        assert warnings == []
        assert fixes == []

    def test_valid_linear_dag_depth_3_unchanged(self):
        """Valid linear DAG A -> B -> C (depth 3) passes unchanged."""
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
            make_mission_item(
                title_plain="Mission B", prerequisite_titles=["Mission A"]
            ),
            make_mission_item(
                title_plain="Mission C", prerequisite_titles=["Mission B"]
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 3
        assert result[0].prerequisite_titles == []
        assert result[1].prerequisite_titles == ["Mission A"]
        assert result[2].prerequisite_titles == ["Mission B"]
        assert warnings == []
        assert fixes == []

    def test_depth_exactly_at_limit_unchanged(self):
        """Depth exactly at MAX_PREREQUISITE_DEPTH (3) passes unchanged."""
        # Depth 0 -> 1 -> 2 -> 3 (A is root, B depends on A, etc.)
        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="Level1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="Level2", prerequisite_titles=["Level1"]),
            make_mission_item(title_plain="Level3", prerequisite_titles=["Level2"]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 4
        # All prerequisites should be preserved
        assert result[1].prerequisite_titles == ["Root"]
        assert result[2].prerequisite_titles == ["Level1"]
        assert result[3].prerequisite_titles == ["Level2"]
        assert warnings == []
        assert fixes == []

    def test_single_mission_always_valid(self):
        """Single mission is always valid (root by definition), no warnings."""
        missions = [
            make_mission_item(title_plain="Solo Mission", prerequisite_titles=[]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 1
        assert result[0].prerequisite_titles == []
        assert warnings == []
        assert fixes == []

    def test_single_mission_with_self_reference_fixed(self):
        """Single mission with self-referencing prereq has edge removed."""
        missions = [
            make_mission_item(title_plain="Self Ref", prerequisite_titles=["Self Ref"]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 1
        assert result[0].prerequisite_titles == []
        # The cycle is broken and the prereq is removed
        assert len(warnings) >= 1
        assert any("Self Ref" in w for w in warnings)


class TestValidatePrerequisitesCycleDetection:
    """Tests for validate_prerequisites() cycle detection and breaking."""

    def test_simple_cycle_a_to_b_to_a_broken(self):
        """Simple cycle A -> B -> A has one edge removed with warning."""
        missions = [
            make_mission_item(
                title_plain="Mission A", prerequisite_titles=["Mission B"]
            ),
            make_mission_item(
                title_plain="Mission B", prerequisite_titles=["Mission A"]
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 2
        # One of them should have been made a root (prereqs cleared)
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= 1
        assert any("cycle" in w.lower() for w in warnings)
        assert len(fixes) >= 1

    def test_complex_cycle_a_to_b_to_c_to_a_broken(self):
        """Complex cycle A -> B -> C -> A is broken with warning."""
        missions = [
            make_mission_item(
                title_plain="Mission A", prerequisite_titles=["Mission C"]
            ),
            make_mission_item(
                title_plain="Mission B", prerequisite_titles=["Mission A"]
            ),
            make_mission_item(
                title_plain="Mission C", prerequisite_titles=["Mission B"]
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 3
        # Cycle should be broken
        assert any("cycle" in w.lower() for w in warnings)
        # At least one root should exist
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= MIN_ROOT_MISSIONS_PER_CATEGORY

    def test_self_referencing_prerequisite_edge_removed(self):
        """Self-referencing prerequisite A -> A is removed with warning."""
        missions = [
            make_mission_item(
                title_plain="Mission A", prerequisite_titles=["Mission A"]
            ),
            make_mission_item(title_plain="Mission B", prerequisite_titles=[]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 2
        # Mission A should have its self-reference removed
        mission_a = next(m for m in result if m.title_plain == "Mission A")
        assert mission_a.prerequisite_titles == []
        assert any("cycle" in w.lower() or "Mission A" in w for w in warnings)


class TestValidatePrerequisitesDepthEnforcement:
    """Tests for validate_prerequisites() depth enforcement."""

    def test_depth_exceeds_max_trimmed(self):
        """Depth exceeds MAX_PREREQUISITE_DEPTH (4 levels) is trimmed."""
        # Create a chain of 4 levels: Root -> L1 -> L2 -> L3
        # L3 has depth 3, which is at the limit
        # Add L4 which would have depth 4 (exceeds limit of 3)
        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="Level1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="Level2", prerequisite_titles=["Level1"]),
            make_mission_item(title_plain="Level3", prerequisite_titles=["Level2"]),
            # Level4 would have depth 4, exceeding MAX_PREREQUISITE_DEPTH=3
            make_mission_item(title_plain="Level4", prerequisite_titles=["Level3"]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 5
        # Level4 should have its prereq trimmed
        assert any("depth" in w.lower() or "Trimmed" in w for w in warnings)


class TestValidatePrerequisitesRootGuarantee:
    """Tests for validate_prerequisites() root mission guarantee."""

    def test_no_root_missions_promotes_quickest(self):
        """No root missions (all have prereqs) promotes quickest-difficulty mission.

        Uses a linear chain (no cycle) so _ensure_root_missions actually promotes.
        """
        missions = [
            make_mission_item(
                title_plain="Mission A",
                prerequisite_titles=["Mission B"],
                difficulty="medium",
            ),
            make_mission_item(
                title_plain="Mission B",
                prerequisite_titles=["Mission C"],
                difficulty="quick",  # Quickest should be promoted
            ),
            make_mission_item(
                title_plain="Mission C",
                prerequisite_titles=["Mission D"],  # D doesn't exist, will be removed
                difficulty="deep_dive",
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 3
        # After prereq cleanup and root promotion, we should have at least one root
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= MIN_ROOT_MISSIONS_PER_CATEGORY
        # A warning should be generated about the nonexistent prerequisite
        # (which indirectly created a root by removing C's prereq)
        assert len(warnings) >= 1


class TestValidatePrerequisitesEdgeCases:
    """Tests for validate_prerequisites() edge cases."""

    def test_empty_missions_list_returns_empty_with_warning(self):
        """Empty missions list returns empty with warning about no missions."""
        result, warnings, fixes = validate_prerequisites(
            [], InvestigationCategory.ARCHITECTURE
        )

        assert result == []
        assert "Empty" in warnings[0] or "empty" in warnings[0].lower()
        assert fixes == []

    def test_non_existent_prerequisite_title_removed(self):
        """Non-existent prerequisite title is removed with warning."""
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
            make_mission_item(
                title_plain="Mission B",
                prerequisite_titles=["Mission A", "Non Existent Mission"],
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 2
        mission_b = next(m for m in result if m.title_plain == "Mission B")
        assert mission_b.prerequisite_titles == ["Mission A"]
        has_warning = any(
            "Non Existent Mission" in w or "nonexistent" in w.lower() for w in warnings
        )
        assert has_warning

    def test_unreachable_missions_made_roots(self):
        """Unreachable missions (disconnected from DAG) are made roots with warning."""
        # Mission C is unreachable - no one points to it, it doesn't point to anyone
        # Mission A -> B forms a valid chain, C is isolated
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
            make_mission_item(
                title_plain="Mission B", prerequisite_titles=["Mission A"]
            ),
            make_mission_item(
                title_plain="Mission C",
                prerequisite_titles=["Non Existent"],  # Points to nothing real
            ),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 3
        # Mission C should have been made a root after its bad prereq was removed
        mission_c = next(m for m in result if m.title_plain == "Mission C")
        assert mission_c.prerequisite_titles == []


class TestValidatePrerequisitesMultipleIssues:
    """Tests for validate_prerequisites() with multiple combined issues."""

    def test_multiple_issues_combined_all_fixed(self):
        """Multiple issues (cycle + depth + no root) are all fixed in sequence."""
        # Complex scenario:
        # - A <-> B cycle
        # - Chain going too deep
        # - No root initially (all have prereqs in the cycle)
        missions = [
            make_mission_item(title_plain="Cycle A", prerequisite_titles=["Cycle B"]),
            make_mission_item(title_plain="Cycle B", prerequisite_titles=["Cycle A"]),
            make_mission_item(title_plain="Deep1", prerequisite_titles=["Cycle A"]),
            make_mission_item(title_plain="Deep2", prerequisite_titles=["Deep1"]),
            make_mission_item(title_plain="Deep3", prerequisite_titles=["Deep2"]),
            make_mission_item(title_plain="Deep4", prerequisite_titles=["Deep3"]),
        ]

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        assert len(result) == 6
        # Should have at least one root
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= MIN_ROOT_MISSIONS_PER_CATEGORY
        # Should have warnings for cycle and depth
        assert len(warnings) >= 2
        assert len(fixes) >= 2


class TestValidatePrerequisitesPropertyBased:
    """Property-based tests for validate_prerequisites() invariants."""

    @given(
        mission_count=st.integers(min_value=1, max_value=10),
        seed=st.integers(min_value=0, max_value=10000),
    )
    @settings(max_examples=30)
    def test_cycle_detection_invariant(self, mission_count: int, seed: int):
        """Cycle detection invariant: For any mission set, output has no cycles.

        For any set of missions with random prerequisite_titles,
        the output never contains a cycle.
        """
        import random

        random.seed(seed)

        # Generate random missions
        titles = [f"Mission {i}" for i in range(mission_count)]
        missions = []

        for title in titles:
            # Randomly select 0-2 prerequisites from other missions
            other_titles = [t for t in titles if t != title]
            prereq_count = random.randint(0, min(2, len(other_titles)))
            prereqs = (
                random.sample(other_titles, prereq_count) if prereq_count > 0 else []
            )
            missions.append(
                make_mission_item(title_plain=title, prerequisite_titles=prereqs)
            )

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        # Build a graph from result and verify no cycles via DFS
        title_to_prereqs = {m.title_plain: m.prerequisite_titles for m in result}

        def has_cycle_from(start: str, visited: set[str], path: set[str]) -> bool:
            if start in path:
                return True
            if start in visited:
                return False
            visited.add(start)
            path.add(start)
            for prereq in title_to_prereqs.get(start, []):
                if prereq in title_to_prereqs and has_cycle_from(prereq, visited, path):
                    return True
            path.remove(start)
            return False

        for title in title_to_prereqs:
            assert not has_cycle_from(title, set(), set()), (
                f"Cycle detected from {title}"
            )

    @given(
        mission_count=st.integers(min_value=1, max_value=8),
    )
    @settings(max_examples=30)
    def test_root_guarantee_invariant(self, mission_count: int):
        """Root guarantee: non-empty set has >= MIN_ROOT_MISSIONS_PER_CATEGORY roots."""
        import random

        # Create missions with random prereqs (may create cycles, bad refs, etc.)
        titles = [f"M{i}" for i in range(mission_count)]
        missions = []

        for i, title in enumerate(titles):
            # Randomly reference some other missions (may or may not exist)
            possible_refs = titles[:i] + titles[i + 1 :]  # Exclude self
            if possible_refs:
                prereq_count = random.randint(0, min(2, len(possible_refs)))
                prereqs = random.sample(possible_refs, prereq_count)
            else:
                prereqs = []
            missions.append(
                make_mission_item(title_plain=title, prerequisite_titles=prereqs)
            )

        result, warnings, fixes = validate_prerequisites(
            missions, InvestigationCategory.ARCHITECTURE
        )

        # After validation, should have at least MIN_ROOT_MISSIONS_PER_CATEGORY roots
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= MIN_ROOT_MISSIONS_PER_CATEGORY


class TestValidateCategoryMissionsHappyPath:
    """Tests for validate_category_missions() happy path scenarios."""

    def test_valid_response_all_checks_pass(self, tmp_path: Path):
        """Valid response with good files, prereqs, chains passes with valid=True."""
        # Create test files
        create_file_with_lines(tmp_path, "src/main.py", 50)
        create_file_with_lines(tmp_path, "src/utils.py", 30)

        missions = [
            make_mission_item(
                title="Mission A",
                title_plain="Mission A",
                prerequisite_titles=[],
                file_refs=[make_file_ref("src/main.py", 1, 10)],
            ),
            make_mission_item(
                title="Mission B",
                title_plain="Mission B",
                prerequisite_titles=["Mission A"],
                file_refs=[make_file_ref("src/utils.py", 5, 15)],
            ),
        ]

        chains = [
            make_chain("Main Chain", ["Mission A", "Mission B"]),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE, chains
        )

        assert result.valid is True
        assert len(result.missions) == 2
        assert len(result.chains) == 1
        assert result.warnings == []
        assert result.fixes_applied == []

    def test_single_mission_wrapped_to_list(self, tmp_path: Path):
        """Single MissionItemResponse is wrapped to list automatically."""
        create_file_with_lines(tmp_path, "src/solo.py", 20)

        mission = make_mission_item(
            title_plain="Solo",
            file_refs=[make_file_ref("src/solo.py", 1, 10)],
        )

        result = validate_category_missions(
            mission,  # Single mission, not a list
            tmp_path,
            InvestigationCategory.ARCHITECTURE,
        )

        assert result.valid is True
        assert len(result.missions) == 1
        assert result.missions[0].title_plain == "Solo"


class TestValidateCategoryMissionsFileRefs:
    """Tests for validate_category_missions() file reference validation."""

    def test_invalid_file_refs_fixed_with_warnings(self, tmp_path: Path):
        """Response with invalid file refs has them fixed, valid=True, warnings."""
        create_file_with_lines(tmp_path, "src/exists.py", 20)

        missions = [
            make_mission_item(
                title_plain="Mission A",
                prerequisite_titles=[],
                file_refs=[
                    make_file_ref("src/exists.py", 1, 10),
                    make_file_ref("src/missing.py", 1, 10),  # Invalid
                ],
            ),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE
        )

        assert result.valid is True
        assert len(result.missions[0].file_refs) == 1  # Only valid ref kept
        assert len(result.warnings) >= 1
        assert any("missing.py" in w for w in result.warnings)
        assert len(result.fixes_applied) >= 1


class TestValidateCategoryMissionsPrerequisites:
    """Tests for validate_category_missions() prerequisite validation."""

    def test_cycles_in_prerequisites_fixed(self, tmp_path: Path):
        """Response with cycles has them fixed, fixes_applied lists the break."""
        missions = [
            make_mission_item(
                title_plain="Mission A", prerequisite_titles=["Mission B"]
            ),
            make_mission_item(
                title_plain="Mission B", prerequisite_titles=["Mission A"]
            ),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE
        )

        assert result.valid is True
        # One mission should be a root after cycle breaking
        roots = [m for m in result.missions if not m.prerequisite_titles]
        assert len(roots) >= 1
        assert any("cycle" in f.lower() for f in result.fixes_applied)


class TestValidateCategoryMissionsEmptyList:
    """Tests for validate_category_missions() with empty mission list."""

    def test_empty_mission_list_returns_invalid(self, tmp_path: Path):
        """Empty mission list returns valid=False with warning about no missions."""
        result = validate_category_missions(
            [], tmp_path, InvestigationCategory.ARCHITECTURE
        )

        assert result.valid is False
        assert result.missions == []
        assert result.chains == []
        assert len(result.warnings) >= 1
        assert any("No missions" in w for w in result.warnings)


class TestValidateCategoryMissionsChains:
    """Tests for validate_category_missions() chain validation."""

    def test_chain_references_non_existent_mission_dropped(self, tmp_path: Path):
        """Chain referencing non-existent mission title is dropped with warning."""
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
        ]

        chains = [
            make_chain("Bad Chain", ["Mission A", "Non Existent Mission"]),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE, chains
        )

        assert result.valid is True
        # Chain should be dropped (needs 2+ valid missions)
        assert len(result.chains) == 0
        assert any("Bad Chain" in w for w in result.warnings)

    def test_chain_with_only_one_valid_mission_dropped(self, tmp_path: Path):
        """Chain with only 1 valid mission is dropped (needs 2+)."""
        missions = [
            make_mission_item(title_plain="Mission A", prerequisite_titles=[]),
        ]

        chains = [
            make_chain("Solo Chain", ["Mission A"]),  # Only 1 mission
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE, chains
        )

        assert result.valid is True
        assert len(result.chains) == 0
        assert any("Solo Chain" in w or "2+" in w for w in result.warnings)


class TestChainTitleNormalization:
    """Tests for Film Noir title normalization in chains (Bug 2, Layer 1)."""

    def test_film_noir_title_normalized_to_title_plain(self, tmp_path: Path):
        """Chain using Film Noir title is normalized to title_plain."""
        from unfamiliar.llm.missions.validation import _validate_chains

        # Mission with different Film Noir title and title_plain
        missions = [
            make_mission_item(
                title="The Factory Floor",  # Film Noir title
                title_plain="Understand Factory Pattern",
                prerequisite_titles=[],
            ),
            make_mission_item(
                title="Shadows of the Database",
                title_plain="Learn Database Schema",
                prerequisite_titles=[],
            ),
        ]

        # Chain uses Film Noir titles
        chain = make_chain(
            "Learning Path",
            ["The Factory Floor", "Shadows of the Database"],
        )

        valid_chains, warnings = _validate_chains(missions, [chain])

        assert len(valid_chains) == 1
        # Titles should be normalized to title_plain values
        assert valid_chains[0].mission_titles == [
            "Understand Factory Pattern",
            "Learn Database Schema",
        ]
        # Should have normalization warnings
        assert any("Normalized" in w for w in warnings)

    def test_mixed_title_plain_and_film_noir_both_work(self, tmp_path: Path):
        """Chain with mix of title_plain and Film Noir titles normalizes correctly."""
        from unfamiliar.llm.missions.validation import _validate_chains

        missions = [
            make_mission_item(
                title="Film Noir A",
                title_plain="Plain A",
                prerequisite_titles=[],
            ),
            make_mission_item(
                title="Film Noir B",
                title_plain="Plain B",
                prerequisite_titles=[],
            ),
        ]

        # Chain uses one of each type
        chain = make_chain("Mixed Chain", ["Plain A", "Film Noir B"])

        valid_chains, warnings = _validate_chains(missions, [chain])

        assert len(valid_chains) == 1
        assert valid_chains[0].mission_titles == ["Plain A", "Plain B"]
        # Only Film Noir B should generate a normalization warning
        assert sum(1 for w in warnings if "Normalized" in w) == 1

    def test_title_plain_match_takes_priority_over_film_noir(self, tmp_path: Path):
        """When a title matches both title_plain and Film Noir, title_plain wins."""
        from unfamiliar.llm.missions.validation import _validate_chains

        # Mission A's title_plain happens to match Mission B's Film Noir title
        missions = [
            make_mission_item(
                title="Some Title",
                title_plain="Shared Name",  # This is also B's Film Noir title
                prerequisite_titles=[],
            ),
            make_mission_item(
                title="Shared Name",  # This is A's title_plain
                title_plain="Mission B Plain",
                prerequisite_titles=[],
            ),
        ]

        # Chain uses "Shared Name" - should match A's title_plain, not B's Film Noir
        chain = make_chain("Priority Chain", ["Shared Name", "Mission B Plain"])

        valid_chains, warnings = _validate_chains(missions, [chain])

        assert len(valid_chains) == 1
        # Should resolve to the title_plain match (Mission A)
        assert "Shared Name" in valid_chains[0].mission_titles
        # No normalization warning because it matched title_plain directly
        assert not any("Shared Name" in w and "Normalized" in w for w in warnings)

    def test_unknown_title_not_normalized_chain_dropped(self, tmp_path: Path):
        """Unknown title (not in title_plain or Film Noir) causes chain issues."""
        from unfamiliar.llm.missions.validation import _validate_chains

        missions = [
            make_mission_item(
                title="Known Film Noir",
                title_plain="Known Plain",
                prerequisite_titles=[],
            ),
        ]

        # Chain uses completely unknown title
        chain = make_chain("Bad Chain", ["Totally Unknown Title"])

        valid_chains, warnings = _validate_chains(missions, [chain])

        # Chain dropped (no valid missions after filtering)
        assert len(valid_chains) == 0
        assert any(
            "Totally Unknown Title" in w or "missing" in w.lower() for w in warnings
        )


class TestValidateCategoryMissionsFullOrchestration:
    """Tests for validate_category_missions() full orchestration."""

    def test_full_orchestration_multiple_issues_all_fixed(self, tmp_path: Path):
        """Full orchestration: mix of bad file refs + cycle + bad prereq -> fixed."""
        create_file_with_lines(tmp_path, "src/valid.py", 30)

        missions = [
            make_mission_item(
                title_plain="Cycle A",
                prerequisite_titles=["Cycle B"],
                file_refs=[make_file_ref("src/valid.py", 1, 10)],
            ),
            make_mission_item(
                title_plain="Cycle B",
                prerequisite_titles=["Cycle A", "Non Existent"],  # Cycle + bad ref
                file_refs=[
                    make_file_ref("src/valid.py", 5, 15),
                    make_file_ref("src/missing.py", 1, 10),  # Bad file ref
                ],
            ),
        ]

        chains = [
            make_chain(
                "Bad Chain",
                ["Cycle A", "Totally Missing Mission"],  # Bad mission ref
            ),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE, chains
        )

        assert result.valid is True
        assert len(result.missions) == 2

        # Check that multiple types of fixes were applied
        # 1. Cycle broken
        assert any("cycle" in f.lower() for f in result.fixes_applied)
        # 2. File refs fixed
        assert any("file" in f.lower() or "Fixed" in f for f in result.fixes_applied)
        # 3. Bad prereq removed
        assert any("prereq" in f.lower() for f in result.fixes_applied)

        # Warnings should mention various issues
        assert len(result.warnings) >= 3  # Cycle, bad file, bad prereq, chain issue

        # Chain should be dropped (only 1 valid mission after cleanup)
        assert len(result.chains) == 0

    def test_orchestration_preserves_valid_data(self, tmp_path: Path):
        """Orchestration preserves valid data while fixing issues."""
        create_file_with_lines(tmp_path, "src/good.py", 50)

        missions = [
            make_mission_item(
                title_plain="Good Mission",
                prerequisite_titles=[],
                file_refs=[make_file_ref("src/good.py", 10, 20)],
                description="This should be preserved",
                difficulty="medium",
            ),
            make_mission_item(
                title_plain="Bad Mission",
                prerequisite_titles=["Good Mission", "Missing"],
                file_refs=[make_file_ref("src/missing.py", 1, 10)],
            ),
        ]

        result = validate_category_missions(
            missions, tmp_path, InvestigationCategory.ARCHITECTURE
        )

        assert result.valid is True

        # Good mission should be preserved
        good = next(m for m in result.missions if m.title_plain == "Good Mission")
        assert good.description == "This should be preserved"
        assert good.difficulty == "medium"
        assert len(good.file_refs) == 1
        assert good.file_refs[0].file_path == "src/good.py"

        # Bad mission should have its issues fixed
        bad = next(m for m in result.missions if m.title_plain == "Bad Mission")
        assert bad.prerequisite_titles == ["Good Mission"]  # Missing removed
        assert len(bad.file_refs) == 0  # Bad file ref removed


class TestValidationResultDataclass:
    """Tests for ValidationResult dataclass behavior."""

    def test_default_values(self):
        """ValidationResult defaults: warnings=[], fixes_applied=[], valid=True."""

        result = ValidationResult(
            missions=[],
            chains=[],
        )

        assert result.warnings == []
        assert result.fixes_applied == []
        assert result.valid is True

    def test_explicit_invalid_flag(self):
        """ValidationResult can be created with valid=False."""
        result = ValidationResult(
            missions=[],
            chains=[],
            warnings=["Something went wrong"],
            valid=False,
        )

        assert result.valid is False
        assert len(result.warnings) == 1


class TestCycleBreakingEdgeCases:
    """Tests for _break_cycles with various cycle patterns (Card 6.1).

    These tests verify that cycle detection and breaking works correctly
    for various cycle patterns including self-loops, 2-node cycles,
    3-node cycles, and disconnected components with cycles.
    """

    def test_self_loop_single_mission_edge_removed(self):
        """Self-loop (A -> A) on single mission has edge removed."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            make_mission_item(title_plain="Self Ref", prerequisite_titles=["Self Ref"]),
        ]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 1
        assert result[0].prerequisite_titles == []
        assert len(warnings) == 1
        assert "Self Ref" in warnings[0]

    def test_two_node_mutual_cycle_a_to_b_to_broken(self):
        """Two-node mutual cycle A -> B -> A has one edge removed."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            make_mission_item(title_plain="A", prerequisite_titles=["B"]),
            make_mission_item(title_plain="B", prerequisite_titles=["A"]),
        ]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 2
        # At least one should now be a root
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= 1
        assert any("cycle" in w.lower() for w in warnings)

    def test_three_node_cycle_a_to_b_to_c_to_a_broken(self):
        """Three-node cycle A -> B -> C -> A has one edge removed."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            make_mission_item(title_plain="A", prerequisite_titles=["C"]),
            make_mission_item(title_plain="B", prerequisite_titles=["A"]),
            make_mission_item(title_plain="C", prerequisite_titles=["B"]),
        ]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 3
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= 1
        assert any("cycle" in w.lower() for w in warnings)

    def test_four_node_cycle_broken_preserves_three_edges(self):
        """Four-node cycle has one edge removed, preserving three valid edges."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            make_mission_item(title_plain="A", prerequisite_titles=["D"]),
            make_mission_item(title_plain="B", prerequisite_titles=["A"]),
            make_mission_item(title_plain="C", prerequisite_titles=["B"]),
            make_mission_item(title_plain="D", prerequisite_titles=["C"]),
        ]

        result, warnings, fixes = _break_cycles(missions)

        # Count remaining prerequisite edges
        total_prereqs = sum(len(m.prerequisite_titles) for m in result)
        assert total_prereqs == 3  # 4 edges - 1 broken = 3

    def test_disconnected_components_with_separate_cycles_both_broken(self):
        """Two disconnected components with separate cycles both have edges removed."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            # Component 1: A <-> B
            make_mission_item(title_plain="A1", prerequisite_titles=["B1"]),
            make_mission_item(title_plain="B1", prerequisite_titles=["A1"]),
            # Component 2: C <-> D
            make_mission_item(title_plain="C2", prerequisite_titles=["D2"]),
            make_mission_item(title_plain="D2", prerequisite_titles=["C2"]),
        ]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 4
        # Each component should have at least one root
        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= 2
        assert len(warnings) >= 2

    def test_dag_with_no_cycles_unchanged(self):
        """Valid DAG with no cycles passes through unchanged."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="Child1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="Child2", prerequisite_titles=["Root"]),
            make_mission_item(
                title_plain="Grandchild", prerequisite_titles=["Child1", "Child2"]
            ),
        ]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 4
        assert result[0].prerequisite_titles == []
        assert result[1].prerequisite_titles == ["Root"]
        assert result[2].prerequisite_titles == ["Root"]
        assert set(result[3].prerequisite_titles) == {"Child1", "Child2"}
        assert warnings == []
        assert fixes == []

    def test_empty_missions_list_returns_empty(self):
        """Empty missions list returns empty with no warnings."""
        from unfamiliar.llm.missions.validation import _break_cycles

        result, warnings, fixes = _break_cycles([])

        assert result == []
        assert warnings == []
        assert fixes == []

    def test_single_mission_no_cycle_unchanged(self):
        """Single mission with no self-reference passes unchanged."""
        from unfamiliar.llm.missions.validation import _break_cycles

        missions = [make_mission_item(title_plain="Solo", prerequisite_titles=[])]

        result, warnings, fixes = _break_cycles(missions)

        assert len(result) == 1
        assert result[0].prerequisite_titles == []
        assert warnings == []
        assert fixes == []


class TestMaxDepthEnforcementEdgeCases:
    """Tests for _enforce_max_depth with various chain depths (Card 6.2).

    These tests verify that prerequisite chains exceeding MAX_PREREQUISITE_DEPTH
    are correctly trimmed.
    """

    def test_chain_at_exact_max_depth_unchanged(self):
        """Chain exactly at MAX_PREREQUISITE_DEPTH (3) is unchanged."""
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        # Create chain of length MAX_PREREQUISITE_DEPTH + 1 (0 -> 1 -> 2 -> 3)
        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="L1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="L2", prerequisite_titles=["L1"]),
            make_mission_item(title_plain="L3", prerequisite_titles=["L2"]),
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        assert len(result) == 4
        assert result[3].prerequisite_titles == ["L2"]
        assert warnings == []

    def test_chain_one_over_max_depth_has_one_prereq_removed(self):
        """Chain one over max depth has deepest prerequisite removed."""
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        # Create chain: Root -> L1 -> L2 -> L3 -> L4 (depth 4, max is 3)
        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="L1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="L2", prerequisite_titles=["L1"]),
            make_mission_item(title_plain="L3", prerequisite_titles=["L2"]),
            make_mission_item(title_plain="L4", prerequisite_titles=["L3"]),
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        assert len(result) == 5
        assert len(warnings) >= 1
        assert any("depth" in w.lower() for w in warnings)

    def test_chain_way_over_max_depth_trimmed_to_max(self):
        """Chain way over max depth is trimmed until within limit."""
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        # Create a chain of 7 levels (way over max of 3)
        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="L1", prerequisite_titles=["Root"]),
            make_mission_item(title_plain="L2", prerequisite_titles=["L1"]),
            make_mission_item(title_plain="L3", prerequisite_titles=["L2"]),
            make_mission_item(title_plain="L4", prerequisite_titles=["L3"]),
            make_mission_item(title_plain="L5", prerequisite_titles=["L4"]),
            make_mission_item(title_plain="L6", prerequisite_titles=["L5"]),
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        # Should have multiple warnings for multiple trims
        assert len(warnings) >= 1

    def test_root_mission_with_no_prereqs_unchanged(self):
        """Root mission (no prereqs) is never affected by depth enforcement."""
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        missions = [
            make_mission_item(title_plain="Root1", prerequisite_titles=[]),
            make_mission_item(title_plain="Root2", prerequisite_titles=[]),
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        assert len(result) == 2
        assert result[0].prerequisite_titles == []
        assert result[1].prerequisite_titles == []
        assert warnings == []

    def test_multiple_independent_chains_processed_separately(self):
        """Multiple independent chains are processed separately for depth."""
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        # Two independent chains, each at depth 2 (within limit)
        missions = [
            # Chain 1
            make_mission_item(title_plain="Root1", prerequisite_titles=[]),
            make_mission_item(title_plain="Chain1_L1", prerequisite_titles=["Root1"]),
            make_mission_item(
                title_plain="Chain1_L2", prerequisite_titles=["Chain1_L1"]
            ),
            # Chain 2
            make_mission_item(title_plain="Root2", prerequisite_titles=[]),
            make_mission_item(title_plain="Chain2_L1", prerequisite_titles=["Root2"]),
            make_mission_item(
                title_plain="Chain2_L2", prerequisite_titles=["Chain2_L1"]
            ),
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        assert len(result) == 6
        # All should be within depth limit
        assert warnings == []

    def test_diamond_dependency_pattern_handled_correctly(self):
        """Diamond pattern (A->B, A->C, B->D, C->D) doesn't cause false
        depth violation.
        """
        from unfamiliar.llm.missions.validation import _enforce_max_depth

        # Diamond: A is root, B and C depend on A, D depends on B and C
        missions = [
            make_mission_item(title_plain="A", prerequisite_titles=[]),
            make_mission_item(title_plain="B", prerequisite_titles=["A"]),
            make_mission_item(title_plain="C", prerequisite_titles=["A"]),
            make_mission_item(
                title_plain="D", prerequisite_titles=["B", "C"]
            ),  # depth 2
        ]

        result, warnings, fixes = _enforce_max_depth(missions)

        assert len(result) == 4
        # D has depth 2, which is within limit
        assert warnings == []


class TestEnsureRootMissionsEdgeCases:
    """Tests for _ensure_root_missions with various scenarios."""

    def test_already_has_minimum_roots_unchanged(self):
        """When already has MIN_ROOT_MISSIONS_PER_CATEGORY roots, unchanged."""
        from unfamiliar.llm.missions.validation import _ensure_root_missions

        missions = [
            make_mission_item(title_plain="Root1", prerequisite_titles=[]),
            make_mission_item(title_plain="Root2", prerequisite_titles=[]),
            make_mission_item(
                title_plain="Child", prerequisite_titles=["Root1", "Root2"]
            ),
        ]

        result, warnings, fixes = _ensure_root_missions(missions)

        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) == 2
        assert warnings == []
        assert fixes == []

    def test_no_roots_promotes_quickest_difficulty_mission(self):
        """When no roots exist, promotes mission with 'quick' difficulty."""
        from unfamiliar.llm.missions.validation import _ensure_root_missions

        missions = [
            make_mission_item(
                title_plain="Deep1",
                prerequisite_titles=["Deep2"],
                difficulty="deep_dive",
            ),
            make_mission_item(
                title_plain="Deep2", prerequisite_titles=["Quick1"], difficulty="medium"
            ),
            make_mission_item(
                title_plain="Quick1", prerequisite_titles=["Deep1"], difficulty="quick"
            ),
        ]

        result, warnings, fixes = _ensure_root_missions(missions)

        roots = [m for m in result if not m.prerequisite_titles]
        assert len(roots) >= 1
        # Quick1 should be promoted (quickest difficulty)
        quick_root = next((m for m in roots if m.title_plain == "Quick1"), None)
        assert quick_root is not None


class TestEnsureReachabilityEdgeCases:
    """Tests for _ensure_reachability with various graph patterns."""

    def test_all_missions_reachable_unchanged(self):
        """When all missions are reachable from roots, unchanged."""
        from unfamiliar.llm.missions.validation import _ensure_reachability

        missions = [
            make_mission_item(title_plain="Root", prerequisite_titles=[]),
            make_mission_item(title_plain="Child", prerequisite_titles=["Root"]),
        ]

        result, warnings, fixes = _ensure_reachability(missions)

        assert len(result) == 2
        assert warnings == []
        assert fixes == []

    def test_isolated_mission_made_root(self):
        """Isolated mission (not reachable from any root) is made a root."""
        from unfamiliar.llm.missions.validation import _ensure_reachability

        # A -> B is connected, C is isolated with bad prereq
        missions = [
            make_mission_item(title_plain="A", prerequisite_titles=[]),
            make_mission_item(title_plain="B", prerequisite_titles=["A"]),
            make_mission_item(
                title_plain="C", prerequisite_titles=["NonExistent"]
            ),  # Will become root
        ]

        result, warnings, fixes = _ensure_reachability(missions)

        mission_c = next(m for m in result if m.title_plain == "C")
        # C should have had its bad prereq removed by earlier validation
        # and now be reachable (as a root)
        assert mission_c.prerequisite_titles == []

    def test_empty_missions_returns_empty(self):
        """Empty missions list returns empty."""
        from unfamiliar.llm.missions.validation import _ensure_reachability

        result, warnings, fixes = _ensure_reachability([])

        assert result == []
        assert warnings == []
        assert fixes == []
