"""Tests for Pydantic model validation from models/ package.

Tests all models from Phase 2 interface contracts:
- Campaign, Briefing, Module, Mission, MissionChain, FileSnapshot
- Junction models: MissionFileRef, ModuleMissionLink, ChainMissionLink

All fields asserted on returned objects. Minimum 1 error test per 2 happy path.
"""

from __future__ import annotations

import json
from datetime import datetime

import pytest
from hypothesis import given, settings
from hypothesis import strategies as st
from pydantic import ValidationError

from unfamiliar.constants import (
    CampaignMode,
    CampaignStatus,
    CharacterArchetype,
    CompletionMethod,
    InvestigationCategory,
    MissionDifficulty,
    MissionStatus,
    Theme,
)
from unfamiliar.models import (
    Briefing,
    Campaign,
    ChainMissionLink,
    FileSnapshot,
    Mission,
    MissionChain,
    MissionFileRef,
    Module,
    ModuleMissionLink,
)


class TestCampaignModel:
    """Tests for Campaign model validation."""

    def test_valid_construction_with_all_defaults(self):
        """Campaign with only required fields uses all defaults correctly."""
        campaign = Campaign(repo_path="/tmp/test-repo")

        assert campaign.repo_path == "/tmp/test-repo"
        assert campaign.schema_version == "1.0"
        assert campaign.mode == CampaignMode.ONBOARDING
        assert campaign.character == CharacterArchetype.DETECTIVE
        assert campaign.status == CampaignStatus.INITIALIZING
        assert campaign.theme == Theme.FILM_NOIR
        assert campaign.id is None
        assert campaign.created_at is None
        assert campaign.updated_at is None

    def test_valid_construction_with_all_fields(self):
        """Campaign with all fields explicitly set preserves values."""
        now = datetime.now()
        campaign = Campaign(
            id=1,
            repo_path="/tmp/test-repo",
            schema_version="1.1",
            mode=CampaignMode.ASSESSMENT,
            character=CharacterArchetype.FORENSIC_ANALYST,
            status=CampaignStatus.ACTIVE,
            theme=Theme.FILM_NOIR,
            created_at=now,
            updated_at=now,
        )

        assert campaign.id == 1
        assert campaign.repo_path == "/tmp/test-repo"
        assert campaign.schema_version == "1.1"
        assert campaign.mode == CampaignMode.ASSESSMENT
        assert campaign.character == CharacterArchetype.FORENSIC_ANALYST
        assert campaign.status == CampaignStatus.ACTIVE
        assert campaign.theme == Theme.FILM_NOIR
        assert campaign.created_at == now
        assert campaign.updated_at == now

    def test_validation_error_on_invalid_status(self):
        """Invalid status enum value raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            Campaign(repo_path="/tmp/test", status="invalid_status")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("status",)
        assert errors[0]["type"] == "enum"

    def test_validation_error_on_missing_repo_path(self):
        """Missing repo_path raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            Campaign()

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("repo_path",)
        assert errors[0]["type"] == "missing"

    def test_validation_error_on_invalid_mode(self):
        """Invalid mode enum value raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            Campaign(repo_path="/tmp/test", mode="invalid_mode")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("mode",)


class TestBriefingModel:
    """Tests for Briefing model validation."""

    def test_valid_construction(self):
        """Briefing with required fields constructs correctly."""
        briefing = Briefing(
            campaign_id=1,
            summary="Test summary",
            tech_stack=["Python", "TypeScript"],
            security_posture="Good",
        )

        assert briefing.campaign_id == 1
        assert briefing.summary == "Test summary"
        assert briefing.tech_stack == ["Python", "TypeScript"]
        assert briefing.security_posture == "Good"
        assert briefing.id is None
        assert briefing.created_at is None
        assert briefing.updated_at is None

    def test_validation_error_on_missing_campaign_id(self):
        """Missing campaign_id raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            Briefing(summary="Test", tech_stack=[], security_posture="Good")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("campaign_id",)

    def test_tech_stack_empty_list(self):
        """Briefing with empty tech_stack list is valid."""
        briefing = Briefing(
            campaign_id=1,
            summary="Test",
            tech_stack=[],
            security_posture="Good",
        )
        assert briefing.tech_stack == []

    def test_tech_stack_multiple_entries(self):
        """Briefing with multiple tech_stack entries preserves all."""
        tech_stack = ["Python", "TypeScript", "React", "SQLite"]
        briefing = Briefing(
            campaign_id=1,
            summary="Test",
            tech_stack=tech_stack,
            security_posture="Good",
        )
        assert briefing.tech_stack == tech_stack


class TestModuleModel:
    """Tests for Module model validation."""

    def test_valid_with_empty_category_relevance(self):
        """Module with empty category_relevance is valid."""
        module = Module(
            campaign_id=1,
            name="test_module",
            path="src/test.py",
            description="Test module",
        )

        assert module.campaign_id == 1
        assert module.name == "test_module"
        assert module.path == "src/test.py"
        assert module.description == "Test module"
        assert module.category_relevance == []
        assert module.file_count == 0
        assert module.security_relevant is False

    def test_valid_with_all_categories(self):
        """Module with all 7 categories preserves all."""
        all_cats = list(InvestigationCategory)
        module = Module(
            campaign_id=1,
            name="test",
            path="test.py",
            description="Test",
            category_relevance=all_cats,
        )
        assert module.category_relevance == all_cats
        assert len(module.category_relevance) == 7

    def test_validation_error_on_invalid_category(self):
        """Invalid category enum value raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            Module(
                campaign_id=1,
                name="test",
                path="test.py",
                description="Test",
                category_relevance=["invalid_category"],
            )

        errors = exc_info.value.errors()
        assert len(errors) >= 1


class TestMissionModel:
    """Tests for Mission model validation."""

    def test_valid_construction(self):
        """Mission with all required fields constructs correctly."""
        mission = Mission(
            campaign_id=1,
            title="Test Mission",
            title_plain="Test Mission",
            description="A test mission",
            category=InvestigationCategory.ARCHITECTURE,
            difficulty=MissionDifficulty.QUICK,
            time_estimate_minutes=15,
            completion_condition="Complete the test",
        )

        assert mission.campaign_id == 1
        assert mission.title == "Test Mission"
        assert mission.category == InvestigationCategory.ARCHITECTURE
        assert mission.difficulty == MissionDifficulty.QUICK
        assert mission.time_estimate_minutes == 15
        assert mission.status == MissionStatus.LOCKED
        assert mission.completion_method is None

    def test_all_fields_asserted(self):
        """Mission with all fields preserves all values."""
        now = datetime.now()
        mission = Mission(
            id=1,
            campaign_id=1,
            title="Test",
            title_plain="Test",
            description="Description",
            category=InvestigationCategory.DATA_FLOW,
            difficulty=MissionDifficulty.MEDIUM,
            time_estimate_minutes=30,
            completion_condition="Done",
            security_tag=True,
            standard_tag="PEP8",
            status=MissionStatus.COMPLETED,
            completion_method=CompletionMethod.SELF_REPORT,
            narrative_intro="Intro",
            narrative_outro="Outro",
            created_at=now,
            updated_at=now,
        )

        assert mission.id == 1
        assert mission.security_tag is True
        assert mission.standard_tag == "PEP8"
        assert mission.status == MissionStatus.COMPLETED
        assert mission.completion_method == CompletionMethod.SELF_REPORT
        assert mission.narrative_intro == "Intro"
        assert mission.narrative_outro == "Outro"

    def test_validation_error_on_invalid_category(self):
        """Invalid category enum raises ValidationError."""
        with pytest.raises(ValidationError):
            Mission(
                campaign_id=1,
                title="Test",
                title_plain="Test",
                description="Test",
                category="invalid",
                difficulty=MissionDifficulty.QUICK,
                time_estimate_minutes=15,
                completion_condition="Done",
            )


class TestMissionFileRef:
    """Tests for MissionFileRef model validation."""

    def test_whole_file_ref(self):
        """FileRef with both line fields None represents whole file."""
        ref = MissionFileRef(
            mission_id=1,
            file_path="src/main.py",
        )

        assert ref.mission_id == 1
        assert ref.file_path == "src/main.py"
        assert ref.line_start is None
        assert ref.line_end is None
        assert ref.description == ""
        assert ref.file_hash == ""

    def test_single_line_ref(self):
        """FileRef with line_start == line_end represents single line."""
        ref = MissionFileRef(
            mission_id=1,
            file_path="src/main.py",
            line_start=42,
            line_end=42,
            description="Important line",
            file_hash="abc123",
        )

        assert ref.line_start == 42
        assert ref.line_end == 42
        assert ref.description == "Important line"
        assert ref.file_hash == "abc123"

    def test_validation_error_on_missing_file_path(self):
        """Missing file_path raises ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            MissionFileRef(mission_id=1)

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert errors[0]["loc"] == ("file_path",)


class TestMissionChain:
    """Tests for MissionChain model validation."""

    def test_valid_construction(self):
        """MissionChain with required fields constructs correctly."""
        chain = MissionChain(
            campaign_id=1,
            name="Learning Path",
            name_plain="Learning Path",
            description="A learning path",
            category=InvestigationCategory.ARCHITECTURE,
        )

        assert chain.campaign_id == 1
        assert chain.name == "Learning Path"
        assert chain.category == InvestigationCategory.ARCHITECTURE
        assert chain.sort_order == 0

    def test_all_fields_asserted(self):
        """MissionChain with all fields preserves all values."""
        now = datetime.now()
        chain = MissionChain(
            id=1,
            campaign_id=1,
            name="Test",
            name_plain="Test",
            description="Description",
            category=InvestigationCategory.DOMAIN_BUSINESS_LOGIC,
            sort_order=5,
            created_at=now,
            updated_at=now,
        )

        assert chain.id == 1
        assert chain.sort_order == 5
        assert chain.created_at == now


class TestFileSnapshot:
    """Tests for FileSnapshot model validation."""

    def test_valid_construction(self):
        """FileSnapshot with required fields constructs correctly."""
        now = datetime.now()
        snapshot = FileSnapshot(
            campaign_id=1,
            file_path="src/main.py",
            file_hash="abc123",
            last_analyzed=now,
        )

        assert snapshot.campaign_id == 1
        assert snapshot.file_path == "src/main.py"
        assert snapshot.file_hash == "abc123"
        assert snapshot.last_analyzed == now


class TestJunctionModels:
    """Tests for junction/link models."""

    def test_module_mission_link(self):
        """ModuleMissionLink constructs correctly."""
        link = ModuleMissionLink(module_id=1, mission_id=2, is_primary=True)
        assert link.module_id == 1
        assert link.mission_id == 2
        assert link.is_primary is True

    def test_module_mission_link_default_is_primary(self):
        """ModuleMissionLink defaults is_primary to False."""
        link = ModuleMissionLink(module_id=1, mission_id=2)
        assert link.is_primary is False

    def test_chain_mission_link(self):
        """ChainMissionLink constructs correctly."""
        link = ChainMissionLink(chain_id=1, mission_id=2, position=3)
        assert link.chain_id == 1
        assert link.mission_id == 2
        assert link.position == 3


# --- Property-Based Tests ---


@settings(max_examples=100)
@given(categories=st.lists(st.sampled_from(list(InvestigationCategory)), unique=True))
def test_category_relevance_json_roundtrip(categories):
    """Category list survives JSON serialize/deserialize roundtrip."""
    # Convert enum values to strings for JSON serialization
    serialized = json.dumps([str(c) for c in categories])
    # Deserialize back to enum values
    deserialized = [InvestigationCategory(v) for v in json.loads(serialized)]
    assert deserialized == categories


@settings(max_examples=50)
@given(
    status=st.sampled_from(list(CampaignStatus)),
    mode=st.sampled_from(list(CampaignMode)),
    theme=st.sampled_from(list(Theme)),
)
def test_campaign_enum_fields_accept_all_valid_values(status, mode, theme):
    """Campaign accepts all valid enum values for status, mode, theme."""
    campaign = Campaign(
        repo_path="/test",
        status=status,
        mode=mode,
        theme=theme,
    )
    assert campaign.status == status
    assert campaign.mode == mode
    assert campaign.theme == theme


@settings(max_examples=50)
@given(
    category=st.sampled_from(list(InvestigationCategory)),
    difficulty=st.sampled_from(list(MissionDifficulty)),
    status=st.sampled_from(list(MissionStatus)),
)
def test_mission_enum_fields_accept_all_valid_values(category, difficulty, status):
    """Mission accepts all valid enum values for category, difficulty, status."""
    mission = Mission(
        campaign_id=1,
        title="Test",
        title_plain="Test",
        description="Test",
        category=category,
        difficulty=difficulty,
        time_estimate_minutes=difficulty.time_minutes,
        completion_condition="Done",
        status=status,
    )
    assert mission.category == category
    assert mission.difficulty == difficulty
    assert mission.status == status
