"""Tests for mission pipeline.

Tests verify:
- run_mission_pipeline() processes all 7 categories in order
- Error handling: auth stops immediately, other errors continue
- Previous mission titles accumulate across categories
- Checkpoint fallback behavior
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

from tests.conftest import (
    make_analysis_result,
    make_briefing_result,
    make_category_response,
    make_mission_item,
)
from unfamiliar.constants import InvestigationCategory
from unfamiliar.llm.exceptions import AuthenticationError, MissionGenerationError
from unfamiliar.llm.missions.pipeline import (
    CATEGORY_ORDER,
    run_mission_pipeline,
)
from unfamiliar.llm.missions.validation import ValidationResult


class ExceptionWithStatusCode(Exception):
    """Test exception with status_code attribute."""

    def __init__(self, message: str, status_code: int) -> None:
        super().__init__(message)
        self.status_code = status_code


def make_validation_result(missions=None, warnings=None) -> ValidationResult:
    """Create a minimal valid ValidationResult."""
    return ValidationResult(
        missions=missions or [make_mission_item()],
        chains=[],
        warnings=warnings or [],
        fixes_applied=[],
        valid=True,
    )


class TestRunMissionPipelineHappyPath:
    """Tests for run_mission_pipeline() happy path scenarios."""

    def test_full_pipeline_all_categories_succeed(self, mock_checkpointer):
        """All 7 categories succeed -> 7 completed, 0 errors."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            # Setup mocks to return valid responses for each category
            mission_resp = make_category_response()
            validation_resp = make_validation_result()

            mock_gen.return_value = mission_resp
            mock_val.return_value = validation_resp

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.completed_categories) == 7
        assert len(result.errors) == 0
        assert len(result.category_missions) == 7

    def test_category_missions_has_seven_keys_for_completed_categories(
        self, mock_checkpointer
    ):
        """PipelineResult.category_missions has 7 keys for completed categories."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mission_resp = make_category_response()
            validation_resp = make_validation_result()

            mock_gen.return_value = mission_resp
            mock_val.return_value = validation_resp

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        expected_keys = {cat.value for cat in CATEGORY_ORDER}
        assert set(result.category_missions.keys()) == expected_keys

    def test_categories_processed_in_category_order(self, mock_checkpointer):
        """Categories processed in CATEGORY_ORDER (verify call order)."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_order = []

        def track_gen_call(*args, **kwargs):
            category = kwargs.get("category")
            if category:
                call_order.append(category.value)
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = track_gen_call
            mock_val.return_value = make_validation_result()

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        expected_order = [cat.value for cat in CATEGORY_ORDER]
        assert call_order == expected_order


class TestRunMissionPipelineCategoryFilter:
    """Tests for the categories filter parameter."""

    def test_single_category_processes_only_that_category(self, mock_checkpointer):
        """Passing categories=[DEPENDENCIES] processes only dependencies."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_order: list[str] = []

        def track_gen(*args, **kwargs):
            category = kwargs.get("category")
            if category:
                call_order.append(category.value)
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = track_gen
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
                categories=[InvestigationCategory.DEPENDENCIES],
            )

        assert call_order == ["dependencies"]
        assert len(result.completed_categories) == 1
        assert result.completed_categories[0] == "dependencies"

    def test_subset_preserves_category_order(self, mock_checkpointer):
        """Passing out-of-order subset still processes in CATEGORY_ORDER sequence."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_order: list[str] = []

        def track_gen(*args, **kwargs):
            category = kwargs.get("category")
            if category:
                call_order.append(category.value)
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = track_gen
            mock_val.return_value = make_validation_result()

            # Pass in reverse order — should still run in canonical order
            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
                categories=[
                    InvestigationCategory.CONVENTIONS,
                    InvestigationCategory.ARCHITECTURE,
                ],
            )

        assert call_order == ["architecture", "conventions"]
        assert len(result.completed_categories) == 2

    def test_none_categories_processes_all_seven(self, mock_checkpointer):
        """Passing categories=None (default) processes all 7."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.return_value = make_category_response()
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
                categories=None,
            )

        assert len(result.completed_categories) == 7


class TestRunMissionPipelinePreviousMissions:
    """Tests for previous_missions accumulation across categories."""

    def test_first_category_gets_empty_previous_missions(self, mock_checkpointer):
        """First category (Architecture) gets [] for previous_missions."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        captured_calls = []

        def capture_gen(*args, **kwargs):
            prev = kwargs.get("previous_missions", [])
            captured_calls.append(list(prev))
            mission = make_mission_item(title_plain=f"Mission {len(captured_calls)}")
            return make_category_response(missions=[mission])

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = capture_gen
            mock_val.return_value = make_validation_result()

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        # First call should have empty list
        assert captured_calls[0] == []

    def test_previous_mission_titles_accumulate(self, mock_checkpointer):
        """Previous mission titles accumulate: first gets [], second gets first."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        mission_titles = ["Alpha Mission", "Beta Mission", "Gamma Mission"]
        title_index = [0]

        def gen_with_titles(*args, **kwargs):
            idx = title_index[0]
            if idx < len(mission_titles):
                title = mission_titles[idx]
            else:
                title = f"Mission {idx}"
            title_index[0] += 1
            return make_category_response(
                missions=[make_mission_item(title_plain=title)]
            )

        def val_with_titles(*args, **kwargs):
            missions = kwargs.get("missions_response", args[0] if args else [])
            if isinstance(missions, list):
                return make_validation_result(missions=missions)
            return make_validation_result()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_with_titles
            mock_val.side_effect = val_with_titles

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.completed_categories) == 7


class TestRunMissionPipelineSingleCategoryFailure:
    """Tests for handling single category failures."""

    def test_single_category_fails_non_auth_six_completed_one_error(
        self, mock_checkpointer
    ):
        """Single category fails (non-auth) -> 6 completed, 1 error."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_with_one_failure(*args, **kwargs):
            call_count[0] += 1
            # Third category (DOMAIN_BUSINESS_LOGIC) fails
            if call_count[0] == 3:
                raise MissionGenerationError("Simulated failure")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_with_one_failure
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.completed_categories) == 6
        assert len(result.errors) == 1

    def test_first_category_fails_still_tries_remaining_six(self, mock_checkpointer):
        """First category fails -> still tries remaining 6 categories."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_first_fails(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                raise MissionGenerationError("First category failure")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_first_fails
            mock_val.return_value = make_validation_result()

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert call_count[0] == 7  # All 7 categories attempted


class TestRunMissionPipelineAuthError:
    """Tests for authentication error handling."""

    def test_auth_error_stops_pipeline_immediately(self, mock_checkpointer):
        """Auth error -> pipeline stops immediately, returns partial results."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_with_auth_error(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 3:
                raise AuthenticationError("Invalid API key")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_with_auth_error
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        # Should have stopped after third category
        assert call_count[0] == 3
        # Two succeeded before auth error
        assert len(result.completed_categories) == 2
        assert len(result.errors) == 1

    def test_auth_error_403_in_message_stops_pipeline(self, mock_checkpointer):
        """Auth error with 403 in message stops pipeline."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_with_403(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 2:
                raise AuthenticationError("403 Forbidden")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_with_403
            mock_val.return_value = make_validation_result()

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert call_count[0] == 2  # Stopped after second category


class TestRunMissionPipelineInvariants:
    """Tests for pipeline result invariants."""

    def test_no_errors_when_all_categories_succeed(self, mock_checkpointer):
        """len(result.errors) == 0 when no categories fail."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.return_value = make_category_response()
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.errors) == 0

    def test_one_error_when_categories_fail(self, mock_checkpointer):
        """len(result.errors) == 1 when categories fail."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_one_failure(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 4:
                raise MissionGenerationError("Failure")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_one_failure
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.errors) == 1

    def test_invariant_completed_plus_errors_at_most_seven(self, mock_checkpointer):
        """Pipeline result invariant: len(completed) + len(errors) <= 7."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        call_count = [0]

        def gen_with_failures(*args, **kwargs):
            call_count[0] += 1
            # Fail on categories 3, 5, 6
            if call_count[0] in [3, 5, 6]:
                raise MissionGenerationError(f"Failure on category {call_count[0]}")
            return make_category_response()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_with_failures
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        assert len(result.completed_categories) + len(result.errors) <= 7


class TestRunMissionPipelineValidation:
    """Tests for validate_category_missions integration."""

    def test_validate_category_missions_called_with_correct_args(
        self, mock_checkpointer
    ):
        """validate_category_missions is called with correct arguments."""
        analysis = make_analysis_result(repo_path="/test/repo")
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        validation_calls = []

        def capture_validation(*args, **kwargs):
            validation_calls.append({"args": args, "kwargs": kwargs})
            return make_validation_result()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.return_value = make_category_response()
            mock_val.side_effect = capture_validation

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        # Verify all 7 categories had validation called
        assert len(validation_calls) == 7

        # Check first call has correct structure
        first_call = validation_calls[0]
        assert "repo_path" in first_call["kwargs"]
        assert "category" in first_call["kwargs"]
        assert "chains" in first_call["kwargs"]

    def test_validation_warnings_logged(self, mock_checkpointer):
        """Validation warnings are logged."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        validation_with_warnings = make_validation_result(
            warnings=["Test warning 1", "Test warning 2"]
        )

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
            patch("unfamiliar.llm.missions.pipeline.logger") as mock_logger,
        ):
            mock_gen.return_value = make_category_response()
            mock_val.return_value = validation_with_warnings

            run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

            # Logger should have been called for warnings
            mock_logger.warning.assert_called()


class TestRunMissionPipelineValidatedDataWired:
    """Tests for validated data being wired into pipeline result (Bug 1 fix)."""

    def test_validated_missions_replace_raw_in_category_missions(
        self, mock_checkpointer
    ):
        """Validated missions replace raw LLM output in category_missions."""
        from unfamiliar.llm.missions.schemas import MissionFileRefResponse

        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        # Create a raw mission with out-of-range line numbers
        raw_mission = make_mission_item(
            title_plain="Test Mission",
            file_refs=[
                MissionFileRefResponse(
                    file_path="src/main.py",
                    line_start=9999,  # Out of range
                    line_end=10000,
                    description="Test ref",
                )
            ],
            prerequisite_titles=["Nonexistent Prereq"],  # Invalid
        )

        raw_response = make_category_response(missions=[raw_mission])

        def gen_raw(*args, **kwargs):
            return raw_response

        def val_and_fix(*args, **kwargs):
            # Simulate validation: return fixed mission
            fixed_mission = make_mission_item(
                title_plain="Test Mission",
                file_refs=[],  # File refs removed (file doesn't exist in mock)
                prerequisite_titles=[],  # Invalid prereq removed
            )
            return make_validation_result(missions=[fixed_mission])

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_raw
            mock_val.side_effect = val_and_fix

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
                categories=[InvestigationCategory.ARCHITECTURE],
            )

        # The stored mission should be the validated (fixed) one, not the raw
        stored = result.category_missions["architecture"]
        assert len(stored.missions) == 1
        # Invalid prerequisite should have been removed
        assert stored.missions[0].prerequisite_titles == []
        # File refs should have been cleaned (empty because file doesn't exist)
        assert stored.missions[0].file_refs == []

    def test_validated_chains_replace_raw_in_category_missions(self, mock_checkpointer):
        """Validated chains replace raw LLM output in category_missions."""
        from unfamiliar.llm.missions.schemas import ChainResponse

        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        # Create raw missions and a chain referencing a nonexistent mission
        raw_mission = make_mission_item(title_plain="Real Mission")
        raw_chain = ChainResponse(
            name="Test Chain",
            name_plain="Test Chain",
            description="A chain",
            mission_titles=["Real Mission", "Fake Mission"],  # One invalid
        )

        raw_response = make_category_response(
            missions=[raw_mission], chains=[raw_chain]
        )

        def gen_raw(*args, **kwargs):
            return raw_response

        # Return a ValidationResult with empty chains (chain was dropped)
        def val_and_fix(*args, **kwargs):
            return ValidationResult(
                missions=[raw_mission],
                chains=[],  # Chain dropped due to invalid reference
                warnings=["Chain 'Test Chain' references missing missions"],
                fixes_applied=[],
                valid=True,
            )

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.side_effect = gen_raw
            mock_val.side_effect = val_and_fix

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
                categories=[InvestigationCategory.ARCHITECTURE],
            )

        # The stored chain should be the validated one (empty, dropped)
        stored = result.category_missions["architecture"]
        assert stored.chains == []


class TestRunMissionPipelineResultStructure:
    """Tests for PipelineResult structure."""

    def test_category_validations_has_seven_keys(self, mock_checkpointer):
        """PipelineResult.category_validations has 7 keys for completed categories."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.return_value = make_category_response()
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        expected_keys = {cat.value for cat in CATEGORY_ORDER}
        assert set(result.category_validations.keys()) == expected_keys

    def test_completed_categories_matches_category_order_values(
        self, mock_checkpointer
    ):
        """Completed categories list contains all CATEGORY_ORDER values."""
        analysis = make_analysis_result()
        briefing_result = make_briefing_result()
        mock_model = MagicMock()

        with (
            patch(
                "unfamiliar.llm.missions.pipeline.generate_category_missions_multi"
            ) as mock_gen,
            patch(
                "unfamiliar.llm.missions.pipeline.validate_category_missions"
            ) as mock_val,
            patch("unfamiliar.llm.missions.pipeline._save_checkpoint"),
        ):
            mock_gen.return_value = make_category_response()
            mock_val.return_value = make_validation_result()

            result = run_mission_pipeline(
                analysis=analysis,
                briefing_result=briefing_result,
                model=mock_model,
                campaign_dir=Path("/tmp/test-campaign"),
            )

        expected = [cat.value for cat in CATEGORY_ORDER]
        assert result.completed_categories == expected
