"""Tests for mission status engine.

Tests verify:
- complete_mission transitions AVAILABLE/IN_PROGRESS to COMPLETED
- complete_mission unlocks dependents when all prerequisites met
- complete_mission raises MissionStatusError for invalid states
- unlock_mission forces LOCKED to AVAILABLE (escape hatch)
- unlock_mission is no-op for AVAILABLE/IN_PROGRESS
- start_mission transitions AVAILABLE to IN_PROGRESS
- All error conditions covered with exact error attributes
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import MagicMock

import pytest

from unfamiliar.campaign.missions import (
    CampaignStatusError,
    MissionStatusError,
    complete_mission,
    reopen_campaign,
    start_mission,
    unlock_mission,
)
from unfamiliar.campaign.types import CompletionResult
from unfamiliar.constants import (
    CampaignStatus,
    CompletionMethod,
    InvestigationCategory,
    MissionDifficulty,
    MissionStatus,
)
from unfamiliar.db import open_campaign_db
from unfamiliar.models import Campaign, Mission

if TYPE_CHECKING:
    from pathlib import Path


def make_mission(
    mission_id: int,
    status: MissionStatus,
    title: str = "Test Mission",
) -> Mission:
    """Create a Mission with minimal required fields for status testing."""
    return Mission(
        id=mission_id,
        campaign_id=1,
        title=title,
        title_plain=title,
        description="Test description",
        category=InvestigationCategory.ARCHITECTURE,
        difficulty=MissionDifficulty.QUICK,
        time_estimate_minutes=15,
        completion_condition="Done",
        status=status,
    )


class TestCompleteMission:
    """Tests for complete_mission function."""

    def test_available_mission_transitions_to_completed(self):
        """AVAILABLE mission marked COMPLETED, returns empty unlocked_ids."""
        db = MagicMock()
        mission = make_mission(mission_id=42, status=MissionStatus.AVAILABLE)
        db.mission_repo.get.return_value = mission
        db.mission_file_ref_repo.get_dependents.return_value = []

        result = complete_mission(db, mission_id=42)

        assert result == CompletionResult(mission_id=42, unlocked_ids=[])
        db.mission_repo.update_status.assert_called_once_with(
            42, MissionStatus.COMPLETED, CompletionMethod.SELF_REPORT
        )

    def test_in_progress_mission_transitions_to_completed(self):
        """IN_PROGRESS mission marked COMPLETED, returns CompletionResult."""
        db = MagicMock()
        mission = make_mission(mission_id=99, status=MissionStatus.IN_PROGRESS)
        db.mission_repo.get.return_value = mission
        db.mission_file_ref_repo.get_dependents.return_value = []

        result = complete_mission(db, mission_id=99)

        assert result == CompletionResult(mission_id=99, unlocked_ids=[])
        db.mission_repo.update_status.assert_called_once_with(
            99, MissionStatus.COMPLETED, CompletionMethod.SELF_REPORT
        )

    def test_completing_unlocks_dependent_with_all_prerequisites_met(self):
        """Completing mission unlocks dependent when all prerequisites done."""
        db = MagicMock()
        mission = make_mission(mission_id=1, status=MissionStatus.AVAILABLE)
        db.mission_repo.get.return_value = mission

        dependent = make_mission(mission_id=2, status=MissionStatus.LOCKED)
        prereq_3_completed = make_mission(mission_id=3, status=MissionStatus.COMPLETED)

        mission_lookup = {1: mission, 2: dependent, 3: prereq_3_completed}

        def get_mission_side_effect(mid):
            return mission_lookup.get(mid)

        db.mission_repo.get.side_effect = get_mission_side_effect
        # Mission 2 depends on mission 1
        db.mission_file_ref_repo.get_dependents.return_value = [2]
        # Mission 2 needs both 1 and 3 as prerequisites
        db.mission_file_ref_repo.get_prerequisites.return_value = [1, 3]

        # Track status changes to simulate mission 1 becoming COMPLETED
        status_changes = {}

        def update_status_side_effect(mid, status, method=None):
            status_changes[mid] = status
            if mid in mission_lookup:
                mission_lookup[mid] = mission_lookup[mid].model_copy(
                    update={"status": status}
                )

        db.mission_repo.update_status.side_effect = update_status_side_effect

        result = complete_mission(db, mission_id=1)

        assert result.mission_id == 1
        assert result.unlocked_ids == [2]
        # Verify dependent was updated to AVAILABLE
        assert status_changes.get(2) == MissionStatus.AVAILABLE

    def test_completing_does_not_unlock_dependent_with_unmet_prerequisites(self):
        """Completing mission does NOT unlock dependent with unmet prereqs."""
        db = MagicMock()
        mission = make_mission(mission_id=1, status=MissionStatus.AVAILABLE)
        db.mission_repo.get.return_value = mission

        dependent = make_mission(mission_id=2, status=MissionStatus.LOCKED)
        prereq_3_locked = make_mission(mission_id=3, status=MissionStatus.LOCKED)

        call_count = [0]

        def get_mission_side_effect(mid):
            call_count[0] += 1
            if mid == 1:
                # First call returns AVAILABLE, after completion returns COMPLETED
                if call_count[0] == 1:
                    return mission
                return mission.model_copy(update={"status": MissionStatus.COMPLETED})
            if mid == 2:
                return dependent
            if mid == 3:
                return prereq_3_locked
            return None

        db.mission_repo.get.side_effect = get_mission_side_effect
        db.mission_file_ref_repo.get_dependents.return_value = [2]
        db.mission_file_ref_repo.get_prerequisites.return_value = [1, 3]

        result = complete_mission(db, mission_id=1)

        assert result.mission_id == 1
        assert result.unlocked_ids == []
        # Only the completion call, no unlock call for dependent
        db.mission_repo.update_status.assert_called_once()

    def test_completing_locked_mission_raises_status_error(self):
        """LOCKED mission cannot be completed directly, raises error."""
        db = MagicMock()
        mission = make_mission(mission_id=5, status=MissionStatus.LOCKED)
        db.mission_repo.get.return_value = mission

        with pytest.raises(MissionStatusError) as exc_info:
            complete_mission(db, mission_id=5)

        error = exc_info.value
        assert error.mission_id == 5
        assert error.current_status == MissionStatus.LOCKED
        db.mission_repo.update_status.assert_not_called()

    def test_completing_already_completed_mission_raises_status_error(self):
        """COMPLETED mission cannot be completed again, raises error."""
        db = MagicMock()
        mission = make_mission(mission_id=10, status=MissionStatus.COMPLETED)
        db.mission_repo.get.return_value = mission

        with pytest.raises(MissionStatusError) as exc_info:
            complete_mission(db, mission_id=10)

        error = exc_info.value
        assert error.mission_id == 10
        assert error.current_status == MissionStatus.COMPLETED
        db.mission_repo.update_status.assert_not_called()

    def test_completing_nonexistent_mission_raises_error(self):
        """Nonexistent mission ID raises MissionStatusError."""
        db = MagicMock()
        db.mission_repo.get.return_value = None

        with pytest.raises(MissionStatusError) as exc_info:
            complete_mission(db, mission_id=999)

        error = exc_info.value
        assert error.mission_id == 999
        assert error.current_status is None
        db.mission_repo.update_status.assert_not_called()


class TestUnlockMission:
    """Tests for unlock_mission function (escape hatch)."""

    def test_locked_mission_becomes_available(self):
        """LOCKED mission force-unlocked to AVAILABLE status."""
        db = MagicMock()
        mission = make_mission(mission_id=7, status=MissionStatus.LOCKED)
        db.mission_repo.get.return_value = mission

        unlock_mission(db, mission_id=7)

        db.mission_repo.update_status.assert_called_once_with(
            7, MissionStatus.AVAILABLE
        )

    def test_available_mission_is_noop(self):
        """AVAILABLE mission remains unchanged, no error raised."""
        db = MagicMock()
        mission = make_mission(mission_id=8, status=MissionStatus.AVAILABLE)
        db.mission_repo.get.return_value = mission

        unlock_mission(db, mission_id=8)

        db.mission_repo.update_status.assert_not_called()

    def test_in_progress_mission_is_noop(self):
        """IN_PROGRESS mission remains unchanged, no error raised."""
        db = MagicMock()
        mission = make_mission(mission_id=9, status=MissionStatus.IN_PROGRESS)
        db.mission_repo.get.return_value = mission

        unlock_mission(db, mission_id=9)

        db.mission_repo.update_status.assert_not_called()

    def test_completed_mission_raises_status_error(self):
        """COMPLETED mission cannot be unlocked, raises MissionStatusError."""
        db = MagicMock()
        mission = make_mission(mission_id=11, status=MissionStatus.COMPLETED)
        db.mission_repo.get.return_value = mission

        with pytest.raises(MissionStatusError) as exc_info:
            unlock_mission(db, mission_id=11)

        error = exc_info.value
        assert error.mission_id == 11
        assert error.current_status == MissionStatus.COMPLETED
        db.mission_repo.update_status.assert_not_called()

    def test_nonexistent_mission_raises_error(self):
        """Nonexistent mission ID raises MissionStatusError."""
        db = MagicMock()
        db.mission_repo.get.return_value = None

        with pytest.raises(MissionStatusError) as exc_info:
            unlock_mission(db, mission_id=888)

        error = exc_info.value
        assert error.mission_id == 888
        assert error.current_status is None
        db.mission_repo.update_status.assert_not_called()


class TestStartMission:
    """Tests for start_mission function."""

    def test_available_mission_transitions_to_in_progress(self):
        """AVAILABLE mission transitions to IN_PROGRESS status."""
        db = MagicMock()
        mission = make_mission(mission_id=20, status=MissionStatus.AVAILABLE)
        db.mission_repo.get.return_value = mission

        start_mission(db, mission_id=20)

        db.mission_repo.update_status.assert_called_once_with(
            20, MissionStatus.IN_PROGRESS
        )

    def test_in_progress_mission_succeeds_without_error(self):
        """IN_PROGRESS mission can be started again (no error per contract)."""
        db = MagicMock()
        mission = make_mission(mission_id=21, status=MissionStatus.IN_PROGRESS)
        db.mission_repo.get.return_value = mission

        start_mission(db, mission_id=21)

        db.mission_repo.update_status.assert_called_once_with(
            21, MissionStatus.IN_PROGRESS
        )

    def test_locked_mission_raises_status_error(self):
        """LOCKED mission cannot be started, raises MissionStatusError."""
        db = MagicMock()
        mission = make_mission(mission_id=22, status=MissionStatus.LOCKED)
        db.mission_repo.get.return_value = mission

        with pytest.raises(MissionStatusError) as exc_info:
            start_mission(db, mission_id=22)

        error = exc_info.value
        assert error.mission_id == 22
        assert error.current_status == MissionStatus.LOCKED
        db.mission_repo.update_status.assert_not_called()

    def test_completed_mission_raises_status_error(self):
        """COMPLETED mission cannot be started, raises MissionStatusError."""
        db = MagicMock()
        mission = make_mission(mission_id=23, status=MissionStatus.COMPLETED)
        db.mission_repo.get.return_value = mission

        with pytest.raises(MissionStatusError) as exc_info:
            start_mission(db, mission_id=23)

        error = exc_info.value
        assert error.mission_id == 23
        assert error.current_status == MissionStatus.COMPLETED
        db.mission_repo.update_status.assert_not_called()

    def test_nonexistent_mission_raises_error(self):
        """Nonexistent mission ID raises MissionStatusError."""
        db = MagicMock()
        db.mission_repo.get.return_value = None

        with pytest.raises(MissionStatusError) as exc_info:
            start_mission(db, mission_id=777)

        error = exc_info.value
        assert error.mission_id == 777
        assert error.current_status is None
        db.mission_repo.update_status.assert_not_called()


class TestReopenCampaign:
    """Tests for reopen_campaign function."""

    def test_closed_campaign_reopens_to_active(self, tmp_path: Path):
        """CLOSED campaign successfully reopens to ACTIVE."""
        db = open_campaign_db(tmp_path)

        # Create a closed campaign
        campaign = db.campaign_repo.create(
            Campaign(
                repo_path=str(tmp_path),
                status=CampaignStatus.CLOSED,
            )
        )

        # Reopen it
        reopen_campaign(db, campaign.id)

        # Verify status is now ACTIVE
        result = db.campaign_repo.get(campaign.id)
        assert result is not None
        assert result.status == CampaignStatus.ACTIVE

    def test_active_campaign_raises_error(self, tmp_path: Path):
        """ACTIVE campaign cannot be reopened."""
        db = open_campaign_db(tmp_path)

        campaign = db.campaign_repo.create(
            Campaign(
                repo_path=str(tmp_path),
                status=CampaignStatus.ACTIVE,
            )
        )

        with pytest.raises(CampaignStatusError) as exc_info:
            reopen_campaign(db, campaign.id)

        assert "not CLOSED" in str(exc_info.value)
        assert exc_info.value.campaign_id == campaign.id

    def test_initializing_campaign_raises_error(self, tmp_path: Path):
        """INITIALIZING campaign cannot be reopened."""
        db = open_campaign_db(tmp_path)

        campaign = db.campaign_repo.create(
            Campaign(
                repo_path=str(tmp_path),
                status=CampaignStatus.INITIALIZING,
            )
        )

        with pytest.raises(CampaignStatusError) as exc_info:
            reopen_campaign(db, campaign.id)

        assert "not CLOSED" in str(exc_info.value)

    def test_nonexistent_campaign_raises_error(self, tmp_path: Path):
        """Nonexistent campaign ID raises error."""
        db = open_campaign_db(tmp_path)

        with pytest.raises(CampaignStatusError) as exc_info:
            reopen_campaign(db, 99999)

        assert "not found" in str(exc_info.value)
