"""Tests for EligibilityInterpreterAI and pipeline AI phase integration."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, Mock, patch

import pytest

from claim_validator.conf import ClaimValidatorSettings
from claim_validator.constants import Severity
from claim_validator.eligibility.constants import CoverageStatus
from claim_validator.eligibility.deidentifier import EligibilityDeidentifier
from claim_validator.eligibility.models.deidentified import (
    DeidentifiedAAAError,
    DeidentifiedCoverageInfo,
    DeidentifiedEligibilityResponse,
)
from claim_validator.eligibility.models.request import EligibilityRequest
from claim_validator.eligibility.models.response import (
    AAAError,
    BenefitInfo,
    CoverageInfo,
    EligibilityResponse,
)
from claim_validator.eligibility.pipeline import EligibilityPipeline
from claim_validator.eligibility.validators.ai.interpreter import (
    EligibilityInterpreterAI,
)
from claim_validator.exceptions import LLMError
from claim_validator.models.results import Finding

# ---------------------------------------------------------------------------
# Helpers — mock chain results
# ---------------------------------------------------------------------------


def _make_chain_result(summary: str, findings: list[Finding] | None = None):
    """Build a mock chain result mimicking EligibilityInterpreterOutput."""
    result = MagicMock()
    result.reasoning = summary
    from claim_validator.models.results import ValidatorOutput
    result.to_validator_output.return_value = ValidatorOutput(
        validator_name="EligibilityInterpreterAI",
        findings=findings or [],
    )
    return result


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def chain_result_active():
    """Chain result for active coverage."""
    return _make_chain_result(
        summary="Patient has active coverage with $40 copay for office visits.",
        findings=[
            Finding(
                code="AI_ELIG_BENEFIT_NOTE",
                message="Annual deductible is $2,500",
                severity=Severity.WARNING,
                field_name="",
                suggestion="Verify remaining deductible before procedure",
            ),
        ],
    )


@pytest.fixture()
def chain_result_error():
    """Chain result for AAA error interpretation."""
    return _make_chain_result(
        summary="Patient not found in payer system. Coverage cannot be verified.",
        findings=[
            Finding(
                code="AI_ELIG_REJECTION_EXPLAINED",
                message="Rejection code 72 indicates subscriber not found",
                severity=Severity.WARNING,
                field_name="",
                suggestion="Verify member ID and payer ID with the patient",
            ),
        ],
    )


@pytest.fixture()
def chain_result_empty():
    """Chain result with no findings."""
    return _make_chain_result(
        summary="Patient has standard active coverage.",
        findings=[],
    )


@pytest.fixture()
def deidentified_active_response():
    """De-identified response with active coverage and benefits."""
    return DeidentifiedEligibilityResponse(
        eligible=True,
        coverage=DeidentifiedCoverageInfo(
            status=CoverageStatus.ACTIVE,
            effective_year=2023,
            termination_year=None,
        ),
        benefits=[
            BenefitInfo(
                service_type_code="30",
                service_type_name="Health Benefit Plan Coverage",
                copay=40.0,
                coinsurance=0.20,
                deductible=2500.0,
                in_network=True,
                prior_auth_required=False,
            ),
        ],
        errors=[],
    )


@pytest.fixture()
def deidentified_error_response():
    """De-identified response with AAA rejection errors."""
    return DeidentifiedEligibilityResponse(
        eligible=None,
        coverage=None,
        benefits=[],
        errors=[
            DeidentifiedAAAError(
                rejection_code="72",
                follow_up_code="C",
            ),
        ],
    )


@pytest.fixture()
def full_phi_response():
    """Full PHI eligibility response for de-identification testing."""
    return EligibilityResponse(
        eligible=True,
        coverage=CoverageInfo(
            status=CoverageStatus.ACTIVE,
            effective_date="2023-01-15",
            termination_date="2025-12-31",
            plan_name="Acme Corp Gold PPO",
            group_number="GRP-AC-2023",
        ),
        benefits=[
            BenefitInfo(
                service_type_code="30",
                service_type_name="Health Benefit Plan Coverage",
                copay=40.0,
                coinsurance=0.20,
                deductible=2500.0,
                in_network=True,
                prior_auth_required=False,
            ),
        ],
        errors=[
            AAAError(
                rejection_code="72",
                follow_up_code="C",
                message="Subscriber John Smith (ID: XYZ123) not found",
            ),
        ],
        raw_response={"subscriber": {"firstName": "John", "memberId": "XYZ123"}},
    )


@pytest.fixture()
def valid_request():
    """Valid eligibility request for pipeline tests."""
    return EligibilityRequest(
        provider_npi="1234567893",
        payer_id="60054",
        subscriber_id="XYZ123456",
        subscriber_first_name="Jane",
        subscriber_last_name="Doe",
        subscriber_dob="1985-03-15",
        date_of_service="2026-03-01",
        service_type_code="30",
    )


# ---------------------------------------------------------------------------
# TestInterpreterDirect — AC #1: Active coverage interpretation (chain-based)
# ---------------------------------------------------------------------------


class TestInterpreterDirect:
    """Test EligibilityInterpreterAI.interpret() directly via chain."""

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_active_coverage_summary(
        self, mock_invoke, chain_result_active, deidentified_active_response,
    ):
        """AC #1: AI generates human-readable coverage summary."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        summary, findings = interpreter.interpret(deidentified_active_response)

        assert summary
        assert isinstance(summary, str)
        assert len(summary) > 10

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_active_coverage_findings(
        self, mock_invoke, chain_result_active, deidentified_active_response,
    ):
        """AC #1: AI generates AI_ELIG_ prefixed findings."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        _, findings = interpreter.interpret(deidentified_active_response)

        assert len(findings) >= 1
        for f in findings:
            assert f.code.startswith("AI_ELIG_")

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_empty_response_interpretation(self, mock_invoke, chain_result_empty):
        """Minimal response with no findings."""
        mock_invoke.return_value = chain_result_empty
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        empty_response = DeidentifiedEligibilityResponse()
        summary, findings = interpreter.interpret(empty_response)

        assert summary
        assert findings == []


# ---------------------------------------------------------------------------
# TestInterpreterErrors — AC #2: AAA rejection interpretation
# ---------------------------------------------------------------------------


class TestInterpreterErrors:
    """Test interpretation of AAA rejection errors."""

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_rejection_explained(
        self, mock_invoke, chain_result_error, deidentified_error_response,
    ):
        """AC #2: AI interprets rejection codes into plain English."""
        mock_invoke.return_value = chain_result_error
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        summary, findings = interpreter.interpret(deidentified_error_response)

        assert "not found" in summary.lower() or "cannot" in summary.lower()
        assert any(
            f.code == "AI_ELIG_REJECTION_EXPLAINED" for f in findings
        )


# ---------------------------------------------------------------------------
# TestPipelineAIPhase — AC #3: Full pipeline with AI
# ---------------------------------------------------------------------------


class TestPipelineAIPhase:
    """Test EligibilityPipeline with AI phase enabled."""

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_full_pipeline_with_ai(
        self, mock_invoke, chain_result_active, valid_request, full_phi_response,
    ):
        """AC #3: Full pipeline populates ai_summary."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        pipeline = EligibilityPipeline(
            rule_validators=[],
            ai_interpreter=interpreter,
        )
        result = pipeline.run(valid_request, response=full_phi_response)

        assert result.ai_summary is not None
        assert len(result.ai_summary) > 0

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_full_pipeline_eligible_from_response(
        self, mock_invoke, chain_result_active, valid_request, full_phi_response,
    ):
        """AC #3: Result eligible comes from provided response."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        pipeline = EligibilityPipeline(
            rule_validators=[],
            ai_interpreter=interpreter,
        )
        result = pipeline.run(valid_request, response=full_phi_response)

        assert result.eligible is True
        assert result.response is full_phi_response

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_full_pipeline_ai_findings_merged(
        self, mock_invoke, chain_result_active, valid_request, full_phi_response,
    ):
        """AC #3: AI findings are merged into result."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        pipeline = EligibilityPipeline(
            rule_validators=[],
            ai_interpreter=interpreter,
        )
        result = pipeline.run(valid_request, response=full_phi_response)

        ai_findings = [f for f in result.findings if f.code.startswith("AI_ELIG_")]
        assert len(ai_findings) >= 1


# ---------------------------------------------------------------------------
# TestSkipAI — AC #4: AI phase skipped when not configured
# ---------------------------------------------------------------------------


class TestSkipAI:
    """Test AI phase skipping behavior."""

    def test_no_ai_config_no_summary(self, valid_request):
        """AC #4: No ai_config -> ai_summary=None."""
        pipeline = EligibilityPipeline(rule_validators=[])
        result = pipeline.run(valid_request)

        assert result.ai_summary is None

    def test_no_response_no_ai(self, valid_request):
        """AC #4: AI configured but no response -> AI skipped."""
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        pipeline = EligibilityPipeline(
            rule_validators=[],
            ai_interpreter=interpreter,
        )
        result = pipeline.run(valid_request)

        assert result.ai_summary is None

    def test_skip_ai_true_no_ai(self, valid_request, full_phi_response):
        """AC #4: eligibility_skip_ai=True -> AI skipped."""
        settings = ClaimValidatorSettings(
            eligibility_rule_validators=[],
            eligibility_skip_ai=True,
        )
        pipeline = EligibilityPipeline.from_settings(settings)
        result = pipeline.run(valid_request, response=full_phi_response)

        assert result.ai_summary is None


# ---------------------------------------------------------------------------
# TestGracefulDegradation — AC #5: Chain failures
# ---------------------------------------------------------------------------


class TestGracefulDegradation:
    """Test graceful degradation on chain failures."""

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_chain_failure_produces_fallback(
        self, mock_invoke, valid_request, full_phi_response,
    ):
        """AC #5: Chain failure -> fallback summary."""
        mock_invoke.return_value = None
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        pipeline = EligibilityPipeline(
            rule_validators=[],
            ai_interpreter=interpreter,
        )
        result = pipeline.run(valid_request, response=full_phi_response)

        # When chain returns None, interpreter returns fallback
        # The pipeline should handle this gracefully
        assert isinstance(result.findings, list)


# ---------------------------------------------------------------------------
# TestFindingConventions — AC #8: Code prefixes and severity
# ---------------------------------------------------------------------------


class TestFindingConventions:
    """Test finding code and severity conventions."""

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_all_findings_ai_elig_prefix(
        self, mock_invoke, chain_result_active, deidentified_active_response,
    ):
        """AC #8: All findings use AI_ELIG_ code prefix."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        _, findings = interpreter.interpret(deidentified_active_response)

        for f in findings:
            assert f.code.startswith("AI_ELIG_"), f"Finding {f.code} missing prefix"

    @patch("claim_validator.chains.base.invoke_chain_safe")
    def test_all_findings_warning_severity(
        self, mock_invoke, chain_result_active, deidentified_active_response,
    ):
        """AC #8: All AI findings have WARNING severity."""
        mock_invoke.return_value = chain_result_active
        mock_chain = MagicMock()
        interpreter = EligibilityInterpreterAI(chain=mock_chain)
        _, findings = interpreter.interpret(deidentified_active_response)

        for f in findings:
            assert f.severity == Severity.WARNING


# ---------------------------------------------------------------------------
# TestImports — importability
# ---------------------------------------------------------------------------


class TestImports:
    """Test that AI interpreter is importable from expected locations."""

    def test_import_from_eligibility_module(self):
        """Importable from claim_validator.eligibility."""
        from claim_validator.eligibility import EligibilityInterpreterAI

        assert EligibilityInterpreterAI is not None

    def test_import_from_validators_ai(self):
        """Importable from claim_validator.eligibility.validators.ai."""
        from claim_validator.eligibility.validators.ai import (
            EligibilityInterpreterAI,
        )

        assert EligibilityInterpreterAI is not None

    def test_import_from_interpreter_module(self):
        """Importable from full module path."""
        from claim_validator.eligibility.validators.ai.interpreter import (
            EligibilityInterpreterAI,
        )

        assert EligibilityInterpreterAI is not None
