"""Tests for CCAR/DFAST stress testing report engine."""

from __future__ import annotations

import json

import pytest

from orxaq.credit.portfolio import LoanExposure
from orxaq.credit.stress_report import (
    CCAR_HORIZON_QUARTERS,
    FED_SCENARIO_NAMES,
    FED_SCENARIOS,
    StressTestReport,
    run_stress_test,
)
from orxaq.orchestration.integration import OrxaqEngine

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


@pytest.fixture()
def engine_with_data():
    """Create engine with loaded ontology, synthetic data, and fitted model."""
    engine = OrxaqEngine()
    engine.load_credit_ontology()
    data = engine.generate_synthetic(100, seed=42)
    model = engine.fit_pd_model(data)
    return engine, data, model


@pytest.fixture()
def small_portfolio() -> list[LoanExposure]:
    """Small portfolio for fast tests."""
    mk = LoanExposure
    return [
        mk(loan_id="L1", pd=0.05, lgd=0.4, ead=100_000, sector="Retail", vintage="2024Q1"),
        mk(loan_id="L2", pd=0.10, lgd=0.5, ead=150_000, sector="Corp", vintage="2024Q1"),
        mk(loan_id="L3", pd=0.20, lgd=0.6, ead=200_000, sector="Retail", vintage="2024Q2"),
    ]


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


class TestConstants:
    def test_fed_scenario_names(self) -> None:
        assert FED_SCENARIO_NAMES == ("baseline", "adverse", "severely_adverse")

    def test_fed_scenarios_has_all_names(self) -> None:
        for name in FED_SCENARIO_NAMES:
            assert name in FED_SCENARIOS

    def test_ccar_horizon(self) -> None:
        assert CCAR_HORIZON_QUARTERS == 9


# ---------------------------------------------------------------------------
# StressTestReport dataclass
# ---------------------------------------------------------------------------


class TestStressTestReport:
    def test_frozen(self) -> None:
        report = StressTestReport(
            scenario_name="adverse",
            quarterly_ecl=[1.0] * 9,
            portfolio_loss_rate=0.05,
            capital_impact=0.004,
            migration_matrices=[{}] * 9,
            confidence_intervals={"total_ecl": (7.0, 11.0)},
        )
        with pytest.raises(AttributeError):
            report.scenario_name = "x"  # type: ignore[misc]

    def test_to_dict(self) -> None:
        report = StressTestReport(
            scenario_name="baseline",
            quarterly_ecl=[0.5, 0.4],
            portfolio_loss_rate=0.02,
            capital_impact=0.0016,
            migration_matrices=[{"A": {"A": 0.9, "B": 0.1}}],
            confidence_intervals={"total_ecl": (0.7, 1.1)},
        )
        d = report.to_dict()
        assert d["scenario_name"] == "baseline"
        assert d["quarterly_ecl"] == [0.5, 0.4]
        assert d["confidence_intervals"]["total_ecl"] == [0.7, 1.1]

    def test_to_json(self) -> None:
        report = StressTestReport(
            scenario_name="adverse",
            quarterly_ecl=[1.0],
            portfolio_loss_rate=0.05,
            capital_impact=0.004,
            migration_matrices=[{}],
            confidence_intervals={},
        )
        j = report.to_json()
        parsed = json.loads(j)
        assert parsed["scenario_name"] == "adverse"

    def test_generated_at_default(self) -> None:
        report = StressTestReport(
            scenario_name="baseline",
            quarterly_ecl=[],
            portfolio_loss_rate=0.0,
            capital_impact=0.0,
            migration_matrices=[],
            confidence_intervals={},
        )
        assert len(report.generated_at) > 10  # ISO timestamp


# ---------------------------------------------------------------------------
# run_stress_test
# ---------------------------------------------------------------------------


class TestRunStressTest:
    def test_adverse_scenario(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        assert report.scenario_name == "adverse"
        assert len(report.quarterly_ecl) == 9
        assert report.portfolio_loss_rate >= 0.0
        assert report.capital_impact >= 0.0

    def test_baseline_scenario(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="baseline",
        )
        assert report.scenario_name == "baseline"
        assert len(report.quarterly_ecl) == 9

    def test_severely_adverse_scenario(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="severely_adverse",
        )
        assert report.scenario_name == "severely_adverse"

    def test_default_scenario_name(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        scenarios = {"adverse": FED_SCENARIOS["adverse"]}
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=scenarios,
            pd_model=model,
            cecl_engine=cecl,
        )
        assert report.scenario_name == "adverse"

    def test_empty_scenarios_raises(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        with pytest.raises(ValueError, match="empty"):
            run_stress_test(
                portfolio=small_portfolio,
                scenarios={},
                pd_model=model,
                cecl_engine=cecl,
            )

    def test_unknown_scenario_raises(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        with pytest.raises(KeyError, match="nonexistent"):
            run_stress_test(
                portfolio=small_portfolio,
                scenarios=FED_SCENARIOS,
                pd_model=model,
                cecl_engine=cecl,
                scenario_name="nonexistent",
            )


class TestReportMetrics:
    def test_quarterly_ecl_positive(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        for ecl_val in report.quarterly_ecl:
            assert ecl_val >= 0.0

    def test_quarterly_ecl_decreases(self, engine_with_data, small_portfolio) -> None:
        """Later quarters should have lower ECL due to survival factor."""
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        # First quarter should be >= last quarter (survival decay)
        assert report.quarterly_ecl[0] >= report.quarterly_ecl[-1]

    def test_migration_matrices_count(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        assert len(report.migration_matrices) == 9

    def test_confidence_intervals_present(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        assert "total_ecl" in report.confidence_intervals
        assert "portfolio_loss_rate" in report.confidence_intervals
        assert "capital_impact" in report.confidence_intervals

    def test_confidence_intervals_ordered(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
        )
        for key, (low, high) in report.confidence_intervals.items():
            assert low <= high, f"{key}: {low} > {high}"

    def test_custom_horizon(self, engine_with_data, small_portfolio) -> None:
        engine, data, model = engine_with_data
        from orxaq.credit.cecl import CECLEngine

        cecl = CECLEngine(model, engine.world_dag)
        report = run_stress_test(
            portfolio=small_portfolio,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl,
            scenario_name="adverse",
            horizon_quarters=4,
        )
        assert len(report.quarterly_ecl) == 4
        assert len(report.migration_matrices) == 4


# ── Targeted coverage (lines 210, 230) ───────────────────


class TestStressReportHelpers:
    """Cover lines 210 and 230: _portfolio_mean_pd and _pd_to_rating (AA branch)."""

    def test_portfolio_mean_pd_empty(self):
        from orxaq.credit.stress_report import _portfolio_mean_pd

        assert _portfolio_mean_pd([]) == 0.0

    def test_migration_matrix_aa_rating(self):
        from orxaq.credit.stress_report import _build_migration_matrices

        # PD = 0.02 falls in AA range (0.01 <= pd < 0.03)
        loans = [
            LoanExposure(loan_id="L1", pd=0.02, lgd=0.4, ead=100.0, sector="A", vintage="2024Q1")
        ]
        stressed = [{"pd": 0.02, "lgd": 0.4, "ead": 100.0}]
        matrices = _build_migration_matrices(loans, stressed, horizon_quarters=1)
        assert len(matrices) == 1
        assert "AA" in matrices[0]
