"""Tests for campaign progress computation.

Tests verify:
- Empty campaign returns all zeros with all 7 categories present
- Mixed mission statuses compute correct counts per category and overall
- All completed missions yields 100 percent
- Partial completion computes correct percentage
- Categories with no missions still appear with zero values
- Security-tagged missions counted separately
- CategoryProgress has all 6 fields populated
- Overall totals equal sum of category totals
- Nonexistent campaign raises ValueError
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from unfamiliar.campaign.progress import ALL_CATEGORIES, get_campaign_progress
from unfamiliar.campaign.types import CampaignProgress
from unfamiliar.constants import InvestigationCategory, MissionDifficulty, MissionStatus
from unfamiliar.models import Mission


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


class TestGetCampaignProgress:
    """Tests for get_campaign_progress function."""

    def test_campaign_with_no_missions_has_all_zeros(self):
        """Empty mission list returns all counts 0, all 7 categories present."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        db.mission_repo.get_by_campaign.return_value = []

        result = get_campaign_progress(db, campaign_id=1)

        assert isinstance(result, CampaignProgress)
        assert result.total_missions == 0
        assert result.completed_missions == 0
        assert result.available_missions == 0
        assert result.locked_missions == 0
        assert result.in_progress_missions == 0
        assert result.overall_percent == 0
        assert result.security_total == 0
        assert result.security_completed == 0
        assert len(result.category_progress) == 7
        for cp in result.category_progress:
            assert cp.total == 0
            assert cp.completed == 0
            assert cp.available == 0
            assert cp.locked == 0
            assert cp.percent == 0

    def test_campaign_with_mixed_statuses_computes_correctly(self):
        """10 missions across categories with varied statuses compute correct counts."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            # ARCHITECTURE: 2 completed, 1 available, 1 locked
            make_mission(
                1, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(
                2, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(
                3, InvestigationCategory.ARCHITECTURE, MissionStatus.AVAILABLE
            ),
            make_mission(4, InvestigationCategory.ARCHITECTURE, MissionStatus.LOCKED),
            # DATA_FLOW: 1 completed, 1 in_progress
            make_mission(5, InvestigationCategory.DATA_FLOW, MissionStatus.COMPLETED),
            make_mission(6, InvestigationCategory.DATA_FLOW, MissionStatus.IN_PROGRESS),
            # TESTING: 1 available, 1 locked
            make_mission(7, InvestigationCategory.TESTING, MissionStatus.AVAILABLE),
            make_mission(8, InvestigationCategory.TESTING, MissionStatus.LOCKED),
            # CONVENTIONS: 1 locked
            make_mission(9, InvestigationCategory.CONVENTIONS, MissionStatus.LOCKED),
            # TECHNICAL_DEBT: 1 completed
            make_mission(
                10, InvestigationCategory.TECHNICAL_DEBT, MissionStatus.COMPLETED
            ),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        assert result.total_missions == 10
        assert result.completed_missions == 4
        assert result.available_missions == 2
        assert result.locked_missions == 3
        assert result.in_progress_missions == 1
        assert result.overall_percent == 40
        # Verify specific category counts
        arch = next(
            cp
            for cp in result.category_progress
            if cp.category == InvestigationCategory.ARCHITECTURE
        )
        assert arch.total == 4
        assert arch.completed == 2
        assert arch.available == 1
        assert arch.locked == 1
        assert arch.percent == 50
        data_flow = next(
            cp
            for cp in result.category_progress
            if cp.category == InvestigationCategory.DATA_FLOW
        )
        assert data_flow.total == 2
        assert data_flow.completed == 1
        assert data_flow.available == 0
        assert data_flow.locked == 0
        assert data_flow.percent == 50
        testing = next(
            cp
            for cp in result.category_progress
            if cp.category == InvestigationCategory.TESTING
        )
        assert testing.total == 2
        assert testing.completed == 0
        assert testing.available == 1
        assert testing.locked == 1
        assert testing.percent == 0

    def test_all_missions_completed_gives_100_percent(self):
        """All missions COMPLETED yields overall_percent of 100."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            make_mission(
                1, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(2, InvestigationCategory.DATA_FLOW, MissionStatus.COMPLETED),
            make_mission(3, InvestigationCategory.TESTING, MissionStatus.COMPLETED),
            make_mission(4, InvestigationCategory.CONVENTIONS, MissionStatus.COMPLETED),
            make_mission(
                5, InvestigationCategory.TECHNICAL_DEBT, MissionStatus.COMPLETED
            ),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        assert result.overall_percent == 100
        assert result.completed_missions == 5
        assert result.total_missions == 5

    def test_partial_completion_computes_percentage(self):
        """3 of 10 completed yields overall_percent of 30."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            make_mission(i, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED)
            for i in range(1, 4)
        ] + [
            make_mission(i, InvestigationCategory.ARCHITECTURE, MissionStatus.AVAILABLE)
            for i in range(4, 11)
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        assert result.overall_percent == 30
        assert result.completed_missions == 3
        assert result.total_missions == 10

    def test_categories_with_no_missions_still_appear(self):
        """Missions in only 3 of 7 categories yields 4 categories with all zeros."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            # Only ARCHITECTURE, DATA_FLOW, and TESTING have missions
            make_mission(
                1, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(2, InvestigationCategory.DATA_FLOW, MissionStatus.AVAILABLE),
            make_mission(3, InvestigationCategory.TESTING, MissionStatus.LOCKED),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        assert len(result.category_progress) == 7
        categories_with_missions = {
            InvestigationCategory.ARCHITECTURE,
            InvestigationCategory.DATA_FLOW,
            InvestigationCategory.TESTING,
        }
        empty_categories = [
            cp
            for cp in result.category_progress
            if cp.category not in categories_with_missions
        ]
        assert len(empty_categories) == 4
        for cp in empty_categories:
            assert cp.total == 0
            assert cp.completed == 0
            assert cp.available == 0
            assert cp.locked == 0
            assert cp.percent == 0

    def test_security_tagged_missions_counted_separately(self):
        """Missions with security_tag=True counted in security_total and
        security_completed.
        """
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            # Security missions: 4 total, 2 completed
            make_mission(
                1,
                InvestigationCategory.ARCHITECTURE,
                MissionStatus.COMPLETED,
                security_tag=True,
            ),
            make_mission(
                2,
                InvestigationCategory.ARCHITECTURE,
                MissionStatus.COMPLETED,
                security_tag=True,
            ),
            make_mission(
                3,
                InvestigationCategory.DATA_FLOW,
                MissionStatus.AVAILABLE,
                security_tag=True,
            ),
            make_mission(
                4,
                InvestigationCategory.TESTING,
                MissionStatus.LOCKED,
                security_tag=True,
            ),
            # Non-security missions: 2 total, 1 completed
            make_mission(
                5,
                InvestigationCategory.ARCHITECTURE,
                MissionStatus.COMPLETED,
                security_tag=False,
            ),
            make_mission(
                6,
                InvestigationCategory.DATA_FLOW,
                MissionStatus.AVAILABLE,
                security_tag=False,
            ),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        assert result.security_total == 4
        assert result.security_completed == 2
        # Total completed includes both security and non-security
        assert result.completed_missions == 3

    def test_category_progress_fields_all_populated(self):
        """Each CategoryProgress has all 6 fields: category, total, completed,
        available, locked, percent.
        """
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            make_mission(
                1, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(
                2, InvestigationCategory.ARCHITECTURE, MissionStatus.AVAILABLE
            ),
            make_mission(3, InvestigationCategory.ARCHITECTURE, MissionStatus.LOCKED),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        arch = next(
            cp
            for cp in result.category_progress
            if cp.category == InvestigationCategory.ARCHITECTURE
        )
        assert arch.category == InvestigationCategory.ARCHITECTURE
        assert arch.total == 3
        assert arch.completed == 1
        assert arch.available == 1
        assert arch.locked == 1
        assert arch.percent == 33

    def test_overall_totals_equal_sum_of_categories(self):
        """total_missions equals sum of all category totals."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        missions = [
            make_mission(
                1, InvestigationCategory.ARCHITECTURE, MissionStatus.COMPLETED
            ),
            make_mission(
                2, InvestigationCategory.ARCHITECTURE, MissionStatus.AVAILABLE
            ),
            make_mission(3, InvestigationCategory.DATA_FLOW, MissionStatus.COMPLETED),
            make_mission(4, InvestigationCategory.TESTING, MissionStatus.LOCKED),
            make_mission(5, InvestigationCategory.CONVENTIONS, MissionStatus.AVAILABLE),
        ]
        db.mission_repo.get_by_campaign.return_value = missions

        result = get_campaign_progress(db, campaign_id=1)

        sum_of_category_totals = sum(cp.total for cp in result.category_progress)
        assert result.total_missions == sum_of_category_totals
        assert result.total_missions == 5

    def test_nonexistent_campaign_raises_value_error(self):
        """campaign_repo.get returning None raises ValueError."""
        db = MagicMock()
        db.campaign_repo.get.return_value = None

        with pytest.raises(ValueError) as exc_info:
            get_campaign_progress(db, campaign_id=999)

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


class TestAllCategories:
    """Tests for ALL_CATEGORIES constant."""

    def test_all_categories_has_seven_items(self):
        """ALL_CATEGORIES contains exactly 7 InvestigationCategory values."""
        assert len(ALL_CATEGORIES) == 7

    def test_all_categories_contains_all_enum_values(self):
        """ALL_CATEGORIES contains every InvestigationCategory enum member."""
        expected = set(InvestigationCategory)
        actual = set(ALL_CATEGORIES)
        assert actual == expected

    def test_all_categories_matches_category_progress_order(self):
        """Category progress preserves ALL_CATEGORIES order."""
        db = MagicMock()
        db.campaign_repo.get.return_value = MagicMock()
        db.mission_repo.get_by_campaign.return_value = []

        result = get_campaign_progress(db, campaign_id=1)

        result_categories = [cp.category for cp in result.category_progress]
        assert result_categories == list(ALL_CATEGORIES)
