"""Tests for TUI data queries.

Tests verify:
- load_header_data returns HeaderData with all 7 fields
- load_status_data returns StatusData with category breakdown and chains
- load_available_missions returns only AVAILABLE missions
- load_actionable_missions returns IN_PROGRESS then AVAILABLE in order
- load_mission_detail returns full detail with file_refs and prerequisites
- load_mission_detail raises ValueError for nonexistent mission
- load_briefing_data returns briefing fields or placeholder
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from unfamiliar.campaign.types import CampaignProgress, CategoryProgress
from unfamiliar.constants import (
    InvestigationCategory,
    MissionDifficulty,
    MissionStatus,
)
from unfamiliar.models import (
    Campaign,
    ChainMissionLink,
    Mission,
    MissionChain,
    MissionFileRef,
)
from unfamiliar.models.campaign import Briefing
from unfamiliar.tui.queries import (
    load_actionable_missions,
    load_available_missions,
    load_briefing_data,
    load_chain_summaries,
    load_header_data,
    load_in_progress_missions,
    load_mission_detail,
    load_status_data,
)
from unfamiliar.tui.views import (
    HeaderData,
)


def make_campaign(
    campaign_id: int = 1, repo_path: str = "/repos/test-project"
) -> Campaign:
    """Create a Campaign with minimal required fields for testing."""
    return Campaign(
        id=campaign_id,
        repo_path=repo_path,
    )


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


def make_category_progress(
    category: InvestigationCategory,
    total: int = 0,
    completed: int = 0,
    available: int = 0,
    locked: int = 0,
    percent: int = 0,
) -> CategoryProgress:
    """Create a CategoryProgress with specified values."""
    return CategoryProgress(
        category=category,
        total=total,
        completed=completed,
        available=available,
        locked=locked,
        percent=percent,
    )


def make_chain(
    chain_id: int,
    name: str = "Test Chain",
    name_plain: str = "Test Chain",
    category: InvestigationCategory = InvestigationCategory.ARCHITECTURE,
) -> MissionChain:
    """Create a MissionChain with minimal required fields."""
    return MissionChain(
        id=chain_id,
        campaign_id=1,
        name=name,
        name_plain=name_plain,
        description="Test chain description",
        category=category,
    )


def make_chain_link(chain_id: int, mission_id: int, position: int) -> ChainMissionLink:
    """Create a ChainMissionLink."""
    return ChainMissionLink(
        chain_id=chain_id,
        mission_id=mission_id,
        position=position,
    )


def make_file_ref(
    mission_id: int,
    file_path: str = "src/test.py",
    line_start: int | None = None,
    line_end: int | None = None,
    description: str = "Test file reference",
) -> MissionFileRef:
    """Create a MissionFileRef with specified values."""
    return MissionFileRef(
        id=1,
        mission_id=mission_id,
        file_path=file_path,
        line_start=line_start,
        line_end=line_end,
        description=description,
    )


class TestLoadHeaderData:
    """Tests for load_header_data function."""

    def test_unpersisted_campaign_raises_assertion_error(self):
        """Campaign with id=None raises AssertionError.

        This verifies the contract: campaign must be persisted before
        querying. The assertion catches programming errors early.
        """
        # Create campaign without an ID (not persisted)
        campaign = Campaign(
            id=None,  # Explicitly None to test the guard
            repo_path="/repos/test-project",
        )

        db = MagicMock()

        with pytest.raises(AssertionError) as exc_info:
            load_header_data(db, campaign)

        assert "Campaign must be persisted" in str(exc_info.value)

    def test_returns_header_with_all_fields_populated(self):
        """Campaign with missions returns HeaderData with all 7 fields."""
        campaign = make_campaign(campaign_id=1, repo_path="/repos/my-project")

        progress = CampaignProgress(
            category_progress=[],
            total_missions=10,
            completed_missions=3,
            available_missions=4,
            locked_missions=2,
            in_progress_missions=1,
            overall_percent=30,
            security_total=2,
            security_completed=1,
        )

        db = MagicMock()
        db.campaign_repo.get.return_value = campaign
        db.mission_repo.get_by_campaign.return_value = []

        with patch(
            "unfamiliar.tui.queries.get_campaign_progress", return_value=progress
        ):
            result = load_header_data(db, campaign)

        assert result == HeaderData(
            campaign_name="Investigation: my-project",
            repo_name="my-project",
            theme_name="Film Noir",
            completed=3,
            total=10,
            available=4,
        )

    def test_zero_missions_has_zero_counts(self):
        """Campaign with no missions returns HeaderData with zero counts."""
        campaign = make_campaign(campaign_id=1, repo_path="/repos/empty-repo")

        progress = CampaignProgress(
            category_progress=[],
            total_missions=0,
            completed_missions=0,
            available_missions=0,
            locked_missions=0,
            in_progress_missions=0,
            overall_percent=0,
            security_total=0,
            security_completed=0,
        )

        db = MagicMock()
        db.campaign_repo.get.return_value = campaign
        db.mission_repo.get_by_campaign.return_value = []

        with patch(
            "unfamiliar.tui.queries.get_campaign_progress", return_value=progress
        ):
            result = load_header_data(db, campaign)

        assert result.completed == 0
        assert result.total == 0
        assert result.available == 0


class TestLoadStatusData:
    """Tests for load_status_data function."""

    def test_returns_status_with_category_breakdown(self):
        """Missions across categories returns StatusData with correct
        per-category counts.
        """
        campaign = make_campaign(campaign_id=1, repo_path="/repos/my-project")

        category_progress = [
            make_category_progress(
                category=InvestigationCategory.ARCHITECTURE,
                total=5,
                completed=2,
                available=2,
                locked=1,
                percent=40,
            ),
            make_category_progress(
                category=InvestigationCategory.TESTING,
                total=3,
                completed=1,
                available=1,
                locked=1,
                percent=33,
            ),
        ]

        progress = CampaignProgress(
            category_progress=category_progress,
            total_missions=8,
            completed_missions=3,
            available_missions=3,
            locked_missions=2,
            in_progress_missions=0,
            overall_percent=37,
            security_total=1,
            security_completed=0,
        )

        db = MagicMock()
        db.chain_repo.get_by_campaign.return_value = []
        db.campaign_repo.get.return_value = campaign
        db.mission_repo.get_by_campaign.return_value = []

        with patch(
            "unfamiliar.tui.queries.get_campaign_progress", return_value=progress
        ):
            result = load_status_data(db, campaign)

        assert result.campaign_name == "Investigation: my-project"
        assert result.overall_percent == 37
        assert result.completed == 3
        assert result.total == 8
        assert result.available == 3
        assert result.locked == 2
        assert result.in_progress == 0
        assert result.security_total == 1
        assert result.security_completed == 0
        assert len(result.categories) == 2

        arch_cat = result.categories[0]
        assert arch_cat.name == "architecture"
        assert arch_cat.display_name == "Architecture"
        assert arch_cat.total == 5
        assert arch_cat.completed == 2
        assert arch_cat.available == 2
        assert arch_cat.locked == 1
        assert arch_cat.percent == 40

        test_cat = result.categories[1]
        assert test_cat.name == "testing"
        assert test_cat.display_name == "Testing"
        assert test_cat.total == 3
        assert test_cat.completed == 1
        assert test_cat.available == 1
        assert test_cat.locked == 1
        assert test_cat.percent == 33

    def test_includes_chain_summaries(self):
        """Chains exist returns ChainSummary objects in status.chains."""
        campaign = make_campaign(campaign_id=1, repo_path="/repos/my-project")

        chain = make_chain(
            chain_id=10,
            name="The Mystery Chain",
            name_plain="The Mystery Chain",
            category=InvestigationCategory.ARCHITECTURE,
        )

        mission_1 = make_mission(
            mission_id=101,
            status=MissionStatus.COMPLETED,
            category=InvestigationCategory.ARCHITECTURE,
        )
        mission_2 = make_mission(
            mission_id=102,
            status=MissionStatus.AVAILABLE,
            category=InvestigationCategory.ARCHITECTURE,
        )

        links = [
            make_chain_link(chain_id=10, mission_id=101, position=0),
            make_chain_link(chain_id=10, mission_id=102, position=1),
        ]

        progress = CampaignProgress(
            category_progress=[],
            total_missions=2,
            completed_missions=1,
            available_missions=1,
            locked_missions=0,
            in_progress_missions=0,
            overall_percent=50,
            security_total=0,
            security_completed=0,
        )

        db = MagicMock()
        db.chain_repo.get_by_campaign.return_value = [chain]
        db.chain_repo.get_chain_missions.return_value = links
        db.mission_repo.get.side_effect = lambda mid: {
            101: mission_1,
            102: mission_2,
        }.get(mid)
        db.campaign_repo.get.return_value = campaign
        db.mission_repo.get_by_campaign.return_value = [mission_1, mission_2]

        with patch(
            "unfamiliar.tui.queries.get_campaign_progress", return_value=progress
        ):
            result = load_status_data(db, campaign)

        assert len(result.chains) == 1
        chain_summary = result.chains[0]
        assert chain_summary.name == "The Mystery Chain"
        assert chain_summary.name_plain == "The Mystery Chain"
        assert chain_summary.category == "Architecture"
        assert chain_summary.mission_count == 2
        assert chain_summary.completed_count == 1


class TestLoadAvailableMissions:
    """Tests for load_available_missions function."""

    def test_returns_only_available_missions(self):
        """Mix of statuses returns only AVAILABLE missions in result."""
        available_1 = make_mission(
            mission_id=1,
            status=MissionStatus.AVAILABLE,
            category=InvestigationCategory.ARCHITECTURE,
            title="Available Mission 1",
        )
        available_2 = make_mission(
            mission_id=2,
            status=MissionStatus.AVAILABLE,
            category=InvestigationCategory.TESTING,
            title="Available Mission 2",
        )

        db = MagicMock()
        db.mission_repo.get_by_status.return_value = [available_1, available_2]

        result = load_available_missions(db, campaign_id=1)

        assert len(result) == 2
        assert result[0].id == 1
        assert result[0].title == "Available Mission 1"
        assert result[0].status == "available"
        assert result[1].id == 2
        assert result[1].title == "Available Mission 2"
        assert result[1].status == "available"

        db.mission_repo.get_by_status.assert_called_once_with(
            1, MissionStatus.AVAILABLE
        )

    def test_returns_empty_list_when_none_available(self):
        """All LOCKED missions returns empty list."""
        db = MagicMock()
        db.mission_repo.get_by_status.return_value = []

        result = load_available_missions(db, campaign_id=1)

        assert result == []


class TestLoadActionableMissions:
    """Tests for load_actionable_missions function."""

    def test_returns_in_progress_and_available(self):
        """Mix of all statuses returns only IN_PROGRESS and AVAILABLE."""
        in_progress_1 = make_mission(
            mission_id=10,
            status=MissionStatus.IN_PROGRESS,
            title="In Progress Mission",
        )
        available_1 = make_mission(
            mission_id=20,
            status=MissionStatus.AVAILABLE,
            title="Available Mission",
        )

        db = MagicMock()
        db.mission_repo.get_by_status.side_effect = lambda cid, status: {
            (1, MissionStatus.IN_PROGRESS): [in_progress_1],
            (1, MissionStatus.AVAILABLE): [available_1],
        }.get((cid, status), [])

        result = load_actionable_missions(db, campaign_id=1)

        assert len(result) == 2
        ids = [m.id for m in result]
        assert 10 in ids
        assert 20 in ids

    def test_in_progress_missions_come_first(self):
        """IN_PROGRESS missions sorted before AVAILABLE in result list."""
        in_progress_1 = make_mission(
            mission_id=100,
            status=MissionStatus.IN_PROGRESS,
            title="In Progress 1",
        )
        in_progress_2 = make_mission(
            mission_id=101,
            status=MissionStatus.IN_PROGRESS,
            title="In Progress 2",
        )
        available_1 = make_mission(
            mission_id=200,
            status=MissionStatus.AVAILABLE,
            title="Available 1",
        )
        available_2 = make_mission(
            mission_id=201,
            status=MissionStatus.AVAILABLE,
            title="Available 2",
        )

        db = MagicMock()
        db.mission_repo.get_by_status.side_effect = lambda cid, status: {
            (1, MissionStatus.IN_PROGRESS): [in_progress_1, in_progress_2],
            (1, MissionStatus.AVAILABLE): [available_1, available_2],
        }.get((cid, status), [])

        result = load_actionable_missions(db, campaign_id=1)

        assert len(result) == 4
        assert result[0].id == 100
        assert result[1].id == 101
        assert result[2].id == 200
        assert result[3].id == 201
        assert result[0].status == "in_progress"
        assert result[2].status == "available"

    def test_returns_empty_when_none_actionable(self):
        """All LOCKED or COMPLETED returns empty list."""
        db = MagicMock()
        db.mission_repo.get_by_status.side_effect = lambda cid, status: []

        result = load_actionable_missions(db, campaign_id=1)

        assert result == []


class TestLoadMissionDetail:
    """Tests for load_mission_detail function."""

    def test_returns_full_detail_with_file_refs(self):
        """Mission with file_refs and prerequisites returns MissionDetail
        with all 15 fields.
        """
        mission = make_mission(
            mission_id=42,
            status=MissionStatus.IN_PROGRESS,
            category=InvestigationCategory.DOMAIN_BUSINESS_LOGIC,
            difficulty=MissionDifficulty.DEEP_DIVE,
            title="Investigate the Domain",
            title_plain="Investigate the Domain",
            description="Look deep into the business logic",
            completion_condition="You understand the domain model",
            time_estimate_minutes=45,
            security_tag=True,
            narrative_intro="The shadows hide secrets...",
            narrative_outro="The truth is revealed.",
        )

        file_ref_1 = make_file_ref(
            mission_id=42,
            file_path="src/domain/model.py",
            line_start=10,
            line_end=50,
            description="Core domain model",
        )
        file_ref_2 = make_file_ref(
            mission_id=42,
            file_path="src/domain/service.py",
            line_start=None,
            line_end=None,
            description="Domain service layer",
        )

        prereq_mission = make_mission(
            mission_id=41,
            status=MissionStatus.COMPLETED,
            title="Architecture Overview",
            title_plain="Architecture Overview",
        )

        db = MagicMock()
        db.mission_repo.get.side_effect = lambda mid: {
            42: mission,
            41: prereq_mission,
        }.get(mid)
        db.mission_file_ref_repo.get_file_refs.return_value = [file_ref_1, file_ref_2]
        db.mission_file_ref_repo.get_prerequisites.return_value = [41]

        result = load_mission_detail(db, mission_id=42)

        assert result.id == 42
        assert result.title == "Investigate the Domain"
        assert result.title_plain == "Investigate the Domain"
        assert result.description == "Look deep into the business logic"
        assert result.category == "Domain & Business Logic"
        assert result.difficulty == "Deep Dive"
        assert result.time_minutes == 45
        assert result.status == "in_progress"
        assert result.completion_condition == "You understand the domain model"
        assert result.security_tag is True
        assert result.narrative_intro == "The shadows hide secrets..."
        assert result.narrative_outro == "The truth is revealed."
        assert len(result.file_refs) == 2
        assert result.file_refs[0].file_path == "src/domain/model.py"
        assert result.file_refs[0].line_start == 10
        assert result.file_refs[0].line_end == 50
        assert result.file_refs[0].description == "Core domain model"
        assert result.file_refs[1].file_path == "src/domain/service.py"
        assert result.file_refs[1].line_start is None
        assert result.file_refs[1].line_end is None
        assert result.file_refs[1].description == "Domain service layer"
        assert result.prerequisites == ["Architecture Overview"]

    def test_nonexistent_mission_raises_value_error(self):
        """Invalid mission ID raises ValueError."""
        db = MagicMock()
        db.mission_repo.get.return_value = None

        with pytest.raises(ValueError) as exc_info:
            load_mission_detail(db, mission_id=999)

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


class TestLoadBriefingData:
    """Tests for load_briefing_data function."""

    def test_returns_briefing_fields(self):
        """Briefing exists returns BriefingData with summary, tech_stack,
        security_posture.
        """
        briefing = Briefing(
            id=1,
            campaign_id=1,
            summary="This is a Python project using FastAPI and SQLAlchemy.",
            tech_stack=["Python", "FastAPI", "SQLAlchemy", "SQLite"],
            security_posture=(
                "No critical vulnerabilities detected. Uses parameterized queries."
            ),
        )

        db = MagicMock()
        db.campaign_repo.get_briefing.return_value = briefing

        result = load_briefing_data(db, campaign_id=1)

        assert (
            result.summary == "This is a Python project using FastAPI and SQLAlchemy."
        )
        assert result.tech_stack == ["Python", "FastAPI", "SQLAlchemy", "SQLite"]
        assert (
            result.security_posture
            == "No critical vulnerabilities detected. Uses parameterized queries."
        )

    def test_no_briefing_returns_placeholder(self):
        """No briefing record returns BriefingData with default text."""
        db = MagicMock()
        db.campaign_repo.get_briefing.return_value = None

        result = load_briefing_data(db, campaign_id=1)

        assert (
            result.summary
            == "No briefing generated yet. Start an investigation to generate one."
        )
        assert result.tech_stack == []
        assert result.security_posture == ""


class TestLoadInProgressMissions:
    """Tests for load_in_progress_missions function."""

    def test_returns_only_in_progress_missions(self):
        """Only IN_PROGRESS missions returned in result."""
        in_progress_1 = make_mission(
            mission_id=50,
            status=MissionStatus.IN_PROGRESS,
            title="Active Mission 1",
        )
        in_progress_2 = make_mission(
            mission_id=51,
            status=MissionStatus.IN_PROGRESS,
            title="Active Mission 2",
        )

        db = MagicMock()
        db.mission_repo.get_by_status.return_value = [in_progress_1, in_progress_2]

        result = load_in_progress_missions(db, campaign_id=1)

        assert len(result) == 2
        assert result[0].id == 50
        assert result[0].status == "in_progress"
        assert result[1].id == 51
        assert result[1].status == "in_progress"

        db.mission_repo.get_by_status.assert_called_once_with(
            1, MissionStatus.IN_PROGRESS
        )

    def test_returns_empty_list_when_none_in_progress(self):
        """No IN_PROGRESS missions returns empty list."""
        db = MagicMock()
        db.mission_repo.get_by_status.return_value = []

        result = load_in_progress_missions(db, campaign_id=1)

        assert result == []


class TestLoadChainSummaries:
    """Tests for load_chain_summaries function."""

    def test_returns_chain_summaries_with_completion_counts(self):
        """Chains with missions returns ChainSummary with correct completion counts."""
        chain_1 = make_chain(
            chain_id=100,
            name="The Architecture Trail",
            name_plain="The Architecture Trail",
            category=InvestigationCategory.ARCHITECTURE,
        )
        chain_2 = make_chain(
            chain_id=200,
            name="Testing the Waters",
            name_plain="Testing the Waters",
            category=InvestigationCategory.TESTING,
        )

        mission_completed = make_mission(
            mission_id=1001,
            status=MissionStatus.COMPLETED,
        )
        mission_available = make_mission(
            mission_id=1002,
            status=MissionStatus.AVAILABLE,
        )
        mission_in_progress = make_mission(
            mission_id=2001,
            status=MissionStatus.IN_PROGRESS,
        )

        links_1 = [
            make_chain_link(chain_id=100, mission_id=1001, position=0),
            make_chain_link(chain_id=100, mission_id=1002, position=1),
        ]
        links_2 = [
            make_chain_link(chain_id=200, mission_id=2001, position=0),
        ]

        db = MagicMock()
        db.chain_repo.get_by_campaign.return_value = [chain_1, chain_2]
        db.chain_repo.get_chain_missions.side_effect = lambda chain_id: {
            100: links_1,
            200: links_2,
        }.get(chain_id, [])
        db.mission_repo.get.side_effect = lambda mid: {
            1001: mission_completed,
            1002: mission_available,
            2001: mission_in_progress,
        }.get(mid)

        result = load_chain_summaries(db, campaign_id=1)

        assert len(result) == 2

        first_chain = result[0]
        assert first_chain.name == "The Architecture Trail"
        assert first_chain.name_plain == "The Architecture Trail"
        assert first_chain.category == "Architecture"
        assert first_chain.mission_count == 2
        assert first_chain.completed_count == 1

        second_chain = result[1]
        assert second_chain.name == "Testing the Waters"
        assert second_chain.name_plain == "Testing the Waters"
        assert second_chain.category == "Testing"
        assert second_chain.mission_count == 1
        assert second_chain.completed_count == 0

    def test_returns_empty_list_when_no_chains(self):
        """No chains returns empty list."""
        db = MagicMock()
        db.chain_repo.get_by_campaign.return_value = []

        result = load_chain_summaries(db, campaign_id=1)

        assert result == []
