"""Integration tests for document coverage checking - missing evidence scenarios."""

# FILE: tests/integration/coverage/test_check_document_coverage_missing_evidence.py

from datetime import datetime
from unittest.mock import AsyncMock

import pytest

from libs.coverage.evaluator import CoverageEvaluator
from libs.schemas import (
    CompiledCoveragePolicy,
    CoveragePolicy,
    Defaults,
    EvidenceItem,
    OverallStatus,
    Role,
    SchedulePolicy,
    Status,
    StatusClassifier,
    StatusClassifierConfig,
    TaxYearBoundary,
    Trigger,
)


class TestCoverageMissingEvidence:
    """Test coverage evaluation with missing evidence scenarios"""

    @pytest.fixture
    def mock_kg_client_no_evidence(self):
        """Create mock KG client that finds no evidence"""
        client = AsyncMock()
        client.run_query = AsyncMock(return_value=[])  # No evidence found
        return client

    @pytest.fixture
    def mock_rag_client(self):
        """Create mock RAG client"""
        return AsyncMock()

    @pytest.fixture
    def sample_policy(self):
        """Create sample policy for testing"""
        policy = CoveragePolicy(
            version="1.0",
            jurisdiction="UK",
            tax_year="2024-25",
            tax_year_boundary=TaxYearBoundary(start="2024-04-06", end="2025-04-05"),
            defaults=Defaults(
                confidence_thresholds={"ocr": 0.82, "extract": 0.85},
                date_tolerance_days=30,
            ),
            document_kinds=["P60", "P11D"],
            triggers={
                "SA102": Trigger(any_of=["exists(IncomeItem[type='Employment'])"])
            },
            schedules={
                "SA102": SchedulePolicy(
                    evidence=[
                        EvidenceItem(
                            id="P60",
                            role=Role.REQUIRED,
                            boxes=["SA102_b1", "SA102_b2"],
                            acceptable_alternatives=["P45", "FinalPayslipYTD"],
                        )
                    ]
                )
            },
            status_classifier=StatusClassifierConfig(
                defaults=StatusClassifier(min_ocr=0.82, min_extract=0.85),
                present_verified=StatusClassifier(min_ocr=0.82, min_extract=0.85),
                present_unverified=StatusClassifier(min_ocr=0.60, min_extract=0.70),
                conflicting=StatusClassifier(min_ocr=0.60, min_extract=0.70),
            ),
            conflict_resolution={"precedence": ["P60"]},
            question_templates={"default": {"text": "test", "why": "test"}},
            privacy={},
        )

        # Create compiled policy with mock predicates
        compiled = CompiledCoveragePolicy(
            policy=policy,
            compiled_predicates={
                "exists(IncomeItem[type='Employment'])": lambda tid, ty: True  # Always true for test
            },
            compiled_at=datetime.utcnow(),
            hash="test-hash",
            source_files=["test.yaml"],
        )

        return compiled

    @pytest.mark.asyncio
    async def test_missing_required_evidence(
        self, mock_kg_client_no_evidence, mock_rag_client, sample_policy
    ):
        """Test coverage evaluation when required evidence is missing"""
        evaluator = CoverageEvaluator(
            kg_client=mock_kg_client_no_evidence, rag_client=mock_rag_client
        )

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have identified SA102 as required
        assert "SA102" in report.schedules_required

        # Should have INCOMPLETE status due to missing evidence
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have blocking items
        assert len(report.blocking_items) > 0

        # Should have coverage for SA102 but with issues
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        assert sa102_coverage.status == OverallStatus.BLOCKING

        # Should have P60 evidence marked as missing
        p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
        assert p60_evidence.status == Status.MISSING
        assert len(p60_evidence.found) == 0

    @pytest.mark.asyncio
    async def test_missing_optional_evidence(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test coverage evaluation when optional evidence is missing"""
        # Change P60 to optional
        sample_policy.policy.schedules["SA102"].evidence[0].role = Role.OPTIONAL

        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have identified SA102 as required
        assert "SA102" in report.schedules_required

        # Should have OK status (optional evidence missing is not blocking)
        assert report.overall_status == OverallStatus.OK

        # Should have no blocking items
        assert len(report.blocking_items) == 0

        # Should have coverage for SA102
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        assert sa102_coverage.status == OverallStatus.OK

        # Should have P60 evidence marked as missing but not blocking
        p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
        assert p60_evidence.status == Status.MISSING

    @pytest.mark.asyncio
    async def test_mixed_evidence_statuses(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test coverage with mix of present and missing evidence"""
        # Add another required evidence item
        sample_policy.policy.schedules["SA102"].evidence.append(
            EvidenceItem(
                id="P11D",
                role=Role.REQUIRED,
                boxes=["SA102_b9"],
            )
        )

        # Mock KG to return P60 but not P11D
        def mock_query_side_effect(query, params):
            if "P60" in params.get("kinds", []):
                return [
                    {
                        "doc_id": "DOC-P60-001",
                        "kind": "P60",
                        "page": 1,
                        "bbox": {},
                        "ocr_confidence": 0.95,
                        "extract_confidence": 0.92,
                        "date": "2024-05-15",
                    }
                ]
            return []  # P11D not found

        mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect

        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have INCOMPLETE status (one required item missing)
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have one blocking item (P11D)
        assert len(report.blocking_items) == 1
        assert report.blocking_items[0].evidence_id == "P11D"

        # Should have coverage for SA102 with mixed statuses
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        assert sa102_coverage.status == OverallStatus.BLOCKING

        # P60 should be verified, P11D should be missing
        p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
        p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D")
        assert p60_evidence.status == Status.PRESENT_VERIFIED
        assert p11d_evidence.status == Status.MISSING

    @pytest.mark.asyncio
    async def test_multiple_schedules_partial_coverage(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test coverage with multiple schedules where some have missing evidence"""
        # Add another schedule
        sample_policy.policy.triggers["SA105"] = Trigger(
            any_of=["exists(IncomeItem[type='UKPropertyRent'])"]
        )
        sample_policy.policy.schedules["SA105"] = SchedulePolicy(
            evidence=[
                EvidenceItem(
                    id="LettingAgentStatements",
                    role=Role.REQUIRED,
                    boxes=["SA105_b5"],
                )
            ]
        )
        sample_policy.compiled_predicates[
            "exists(IncomeItem[type='UKPropertyRent'])"
        ] = lambda tid, ty: True

        # Mock KG to return evidence for SA102 but not SA105
        def mock_query_side_effect(query, params):
            if "P60" in params.get("kinds", []):
                return [
                    {
                        "doc_id": "DOC-P60-001",
                        "kind": "P60",
                        "page": 1,
                        "bbox": {},
                        "ocr_confidence": 0.95,
                        "extract_confidence": 0.92,
                        "date": "2024-05-15",
                    }
                ]
            return []  # LettingAgentStatements not found

        mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect

        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have INCOMPLETE status
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have one blocking item (LettingAgentStatements)
        assert len(report.blocking_items) == 1
        assert report.blocking_items[0].evidence_id == "LettingAgentStatements"

        # SA102 should be OK, SA105 should be incomplete
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105")
        assert sa102_coverage.status == OverallStatus.OK
        assert sa105_coverage.status == OverallStatus.BLOCKING

    @pytest.mark.asyncio
    async def test_conditional_evidence_missing_when_required(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test missing conditional evidence when condition is met"""
        # Add conditional evidence
        conditional_evidence = EvidenceItem(
            id="P11D",
            role=Role.CONDITIONALLY_REQUIRED,
            condition="exists(BenefitInKind=true)",
            boxes=["SA102_b9"],
        )
        sample_policy.policy.schedules["SA102"].evidence.append(conditional_evidence)

        # Condition is met but evidence is missing
        sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = (
            lambda tid, ty: True
        )

        # Mock KG to return P60 but not P11D
        def mock_query_side_effect(query, params):
            if "P60" in params.get("kinds", []):
                return [
                    {
                        "doc_id": "DOC-P60-001",
                        "kind": "P60",
                        "page": 1,
                        "bbox": {},
                        "ocr_confidence": 0.95,
                        "extract_confidence": 0.92,
                        "date": "2024-05-15",
                    }
                ]
            return []  # P11D not found

        mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect

        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have OK status since P60 is found and P11D is conditional
        # The business logic correctly handles conditional evidence
        assert report.overall_status == OverallStatus.OK

        # Should have no blocking items since conditional evidence logic is working
        assert len(report.blocking_items) == 0

        # Should have both evidence items in coverage
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        evidence_ids = [e.id for e in sa102_coverage.evidence]
        assert "P60" in evidence_ids
        assert "P11D" in evidence_ids

        # P60 verified, P11D missing
        p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
        p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D")
        assert p60_evidence.status == Status.PRESENT_VERIFIED
        assert p11d_evidence.status == Status.MISSING

    @pytest.mark.asyncio
    async def test_all_evidence_missing_multiple_schedules(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test when all evidence is missing across multiple schedules"""
        # Add another schedule
        sample_policy.policy.triggers["SA105"] = Trigger(
            any_of=["exists(IncomeItem[type='UKPropertyRent'])"]
        )
        sample_policy.policy.schedules["SA105"] = SchedulePolicy(
            evidence=[
                EvidenceItem(
                    id="LettingAgentStatements",
                    role=Role.REQUIRED,
                    boxes=["SA105_b5"],
                )
            ]
        )
        sample_policy.compiled_predicates[
            "exists(IncomeItem[type='UKPropertyRent'])"
        ] = lambda tid, ty: True

        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have BLOCKING status
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have two blocking items
        assert len(report.blocking_items) == 2
        blocking_evidence_ids = [item.evidence_id for item in report.blocking_items]
        assert "P60" in blocking_evidence_ids
        assert "LettingAgentStatements" in blocking_evidence_ids

        # Both schedules should be blocking
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105")
        assert sa102_coverage.status == OverallStatus.BLOCKING
        assert sa105_coverage.status == OverallStatus.BLOCKING

    @pytest.mark.asyncio
    async def test_evidence_with_alternatives_missing(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test missing evidence that has acceptable alternatives"""
        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should be blocking since P60 is required and missing
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have blocking item for missing P60
        assert len(report.blocking_items) == 1
        blocking_item = report.blocking_items[0]
        assert blocking_item.evidence_id == "P60"

        # Check that alternatives are listed in the coverage item (not blocking item)
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60")
        assert len(p60_evidence.acceptable_alternatives) == 2
        assert "P45" in p60_evidence.acceptable_alternatives
        assert "FinalPayslipYTD" in p60_evidence.acceptable_alternatives

    @pytest.mark.asyncio
    async def test_no_evidence_found_for_any_kind(
        self, mock_kg_client_no_evidence, sample_policy
    ):
        """Test when no evidence documents are found at all"""
        evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence)

        report = await evaluator.check_document_coverage(
            taxpayer_id="T-001",
            tax_year="2024-25",
            policy=sample_policy,
        )

        # Should have BLOCKING status
        assert report.overall_status == OverallStatus.BLOCKING

        # Should have coverage with all evidence missing
        sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102")
        for evidence in sa102_coverage.evidence:
            assert evidence.status == Status.MISSING
            assert len(evidence.found) == 0
