"""Tests for orxaq.credit.scenario_comparison."""

from __future__ import annotations

import json

import pytest

from orxaq.credit.scenario_comparison import ScenarioComparator
from orxaq.credit.stress_report import FED_SCENARIOS, StressTestReport, run_stress_test
from orxaq.credit.synthetic import CreditDataGenerator


def _make_report(scenario_name: str, n: int = 50) -> StressTestReport:
    from orxaq.credit.causal_pd import CausalPDModel
    from orxaq.credit.cecl import CECLEngine
    from orxaq.credit.ontology import build_credit_ontology
    from orxaq.credit.portfolio import LoanExposure

    dag = build_credit_ontology()
    data = CreditDataGenerator(dag, seed=42).generate(n)
    pd_model = CausalPDModel(dag)
    pd_model.fit(data)
    engine = CECLEngine(pd_model, dag)
    exposures = [
        LoanExposure(
            loan_id=str(i),
            pd=max(0.001, min(0.999, float(rec.get("default_flag", 0.05)))),
            lgd=0.45,
            ead=float(rec.get("loan_amount", 10_000.0)),
            sector=str(rec.get("sector", "retail")),
            vintage="2024",
        )
        for i, rec in enumerate(data[:30])
    ]
    return run_stress_test(
        exposures, FED_SCENARIOS, pd_model, engine, horizon_quarters=9, scenario_name=scenario_name
    )


@pytest.fixture(scope="module")
def three_reports():
    return {
        "baseline": _make_report("baseline"),
        "adverse": _make_report("adverse"),
        "severely_adverse": _make_report("severely_adverse"),
    }


@pytest.fixture(scope="module")
def two_reports():
    return {
        "baseline": _make_report("baseline"),
        "adverse": _make_report("adverse"),
    }


@pytest.fixture(scope="module")
def result_3(three_reports):
    return ScenarioComparator(three_reports).compare()


@pytest.fixture(scope="module")
def result_2(two_reports):
    return ScenarioComparator(two_reports).compare()


class TestScenarioComparatorInit:
    def test_3_scenarios(self, three_reports):
        assert ScenarioComparator(three_reports) is not None

    def test_2_scenarios(self, two_reports):
        assert ScenarioComparator(two_reports) is not None

    def test_single_raises(self):
        with pytest.raises(ValueError, match="at least 2"):
            ScenarioComparator({"baseline": _make_report("baseline", n=20)})

    def test_empty_raises(self):
        with pytest.raises(ValueError, match="at least 2"):
            ScenarioComparator({})

    def test_custom_baseline_zero_deltas(self, two_reports):
        result = ScenarioComparator(two_reports, baseline="adverse").compare()
        assert all(abs(d) < 1e-12 for d in result.ecl_deltas["adverse"])


class TestECLDeltas:
    def test_keys_match_scenarios(self, result_3):
        assert set(result_3.ecl_deltas.keys()) == {"baseline", "adverse", "severely_adverse"}

    def test_lists_of_floats(self, result_3):
        for deltas in result_3.ecl_deltas.values():
            assert isinstance(deltas, list) and all(isinstance(d, float) for d in deltas)

    def test_length_equals_horizon(self, result_3):
        for deltas in result_3.ecl_deltas.values():
            assert len(deltas) == len(result_3.quarter_labels)

    def test_baseline_zero(self, result_3):
        assert all(abs(d) < 1e-12 for d in result_3.ecl_deltas["baseline"])

    def test_sa_gte_adverse(self, result_3):
        sa = sum(abs(d) for d in result_3.ecl_deltas["severely_adverse"])
        adv = sum(abs(d) for d in result_3.ecl_deltas["adverse"])
        assert sa >= adv

    def test_two_keys(self, result_2):
        assert set(result_2.ecl_deltas.keys()) == {"baseline", "adverse"}

    def test_two_horizon(self, result_2):
        for d in result_2.ecl_deltas.values():
            assert len(d) == len(result_2.quarter_labels)


class TestPeakLossQuarter:
    def test_keys(self, result_3):
        assert set(result_3.peak_loss_quarter.keys()) == {"baseline", "adverse", "severely_adverse"}

    def test_valid_labels(self, result_3):
        valid = set(result_3.quarter_labels)
        for q in result_3.peak_loss_quarter.values():
            assert q in valid

    def test_two_keys(self, result_2):
        assert set(result_2.peak_loss_quarter.keys()) == {"baseline", "adverse"}


class TestCapitalWaterfall:
    _KEYS = {"starting_cet1", "credit_losses", "ppnr_offset", "ending_cet1"}

    def test_keys(self, result_3):
        assert set(result_3.capital_waterfall.keys()) == {"baseline", "adverse", "severely_adverse"}

    def test_all_four_keys(self, result_3):
        for wf in result_3.capital_waterfall.values():
            assert self._KEYS == set(wf.keys())

    def test_custom_cet1(self, three_reports):
        r = ScenarioComparator(three_reports).compare(starting_cet1=0.15)
        for wf in r.capital_waterfall.values():
            assert abs(wf["starting_cet1"] - 0.15) < 1e-9

    def test_ending_lte_starting(self, result_3):
        wf = result_3.capital_waterfall["severely_adverse"]
        assert wf["ending_cet1"] <= wf["starting_cet1"] + 1e-9

    def test_ending_non_negative(self, result_3):
        for wf in result_3.capital_waterfall.values():
            assert wf["ending_cet1"] >= 0.0

    def test_credit_losses_non_negative(self, result_3):
        for wf in result_3.capital_waterfall.values():
            assert wf["credit_losses"] >= 0.0

    def test_ppnr_non_negative(self, result_3):
        for wf in result_3.capital_waterfall.values():
            assert wf["ppnr_offset"] >= 0.0

    def test_formula(self, result_3):
        for wf in result_3.capital_waterfall.values():
            exp = wf["starting_cet1"] - wf["credit_losses"] + wf["ppnr_offset"]
            assert abs(wf["ending_cet1"] - max(0.0, exp)) < 1e-6


class TestSensitivity:
    def test_dict_of_floats(self, result_3):
        assert isinstance(result_3.sensitivity_attribution, dict)
        assert all(isinstance(v, float) for v in result_3.sensitivity_attribution.values())

    def test_non_empty(self, result_3):
        assert len(result_3.sensitivity_attribution) > 0

    def test_sums_to_one(self, result_3):
        assert abs(sum(result_3.sensitivity_attribution.values()) - 1.0) < 1e-4

    def test_macro_vars(self, result_3):
        assert set(result_3.sensitivity_attribution.keys()) == {
            "unemployment_rate",
            "fed_funds_rate",
            "gdp_growth",
            "hpi",
            "cpi",
        }

    def test_unit_range(self, result_3):
        for v in result_3.sensitivity_attribution.values():
            assert 0.0 <= v <= 1.0


class TestSerialization:
    def test_json_serializable(self, result_3):
        assert isinstance(json.dumps(result_3.to_dict()), str)

    def test_all_keys(self, result_3):
        exp = {
            "scenarios",
            "quarter_labels",
            "ecl_deltas",
            "peak_loss_quarter",
            "capital_waterfall",
            "sensitivity_attribution",
            "generated_at",
        }
        assert exp == set(result_3.to_dict().keys())

    def test_scenarios_len_3(self, result_3):
        assert len(result_3.to_dict()["scenarios"]) == 3

    def test_iso_timestamp(self, result_3):
        ts = result_3.to_dict()["generated_at"]
        assert isinstance(ts, str) and "T" in ts

    def test_frozen(self, result_3):
        with pytest.raises(AttributeError):
            result_3.scenarios = ["x"]  # type: ignore[misc]

    def test_two_scenario_serializable(self, result_2):
        d = result_2.to_dict()
        assert len(d["scenarios"]) == 2 and json.dumps(d)


class TestQuarterLabels:
    def test_format(self, result_3):
        for label in result_3.quarter_labels:
            assert label.startswith("Q") and label[1:].isdigit()

    def test_count_9(self, result_3):
        assert len(result_3.quarter_labels) == 9


class TestEdgeCases:
    def test_mismatched_horizon(self):
        from orxaq.credit.causal_pd import CausalPDModel
        from orxaq.credit.cecl import CECLEngine
        from orxaq.credit.ontology import build_credit_ontology
        from orxaq.credit.portfolio import LoanExposure

        dag = build_credit_ontology()
        data = CreditDataGenerator(dag, seed=99).generate(30)
        pm = CausalPDModel(dag)
        pm.fit(data)
        eng = CECLEngine(pm, dag)
        exp = [
            LoanExposure(
                loan_id=str(i), pd=0.05, lgd=0.45, ead=10_000.0, sector="retail", vintage="2024"
            )
            for i in range(10)
        ]
        r1 = run_stress_test(
            exp, FED_SCENARIOS, pm, eng, horizon_quarters=4, scenario_name="baseline"
        )
        r2 = run_stress_test(
            exp, FED_SCENARIOS, pm, eng, horizon_quarters=9, scenario_name="adverse"
        )
        result = ScenarioComparator({"baseline": r1, "adverse": r2}).compare()
        assert result is not None and len(result.quarter_labels) >= 4


# ── Targeted coverage (lines 109-110, 147-154) ──────────────


class _FakeReport:
    """Lightweight mock report for testing without heavy stress test setup."""

    def __init__(self, quarterly_ecl=None, portfolio_loss_rate=0.02):
        self.quarterly_ecl = quarterly_ecl
        self.portfolio_loss_rate = portfolio_loss_rate


class TestEmptyECLBranch:
    """Cover lines 108-109 (empty ecl in identify_peak_loss_quarter)."""

    def test_peak_loss_defaults_to_q1(self):
        """Report with no quarterly_ecl should default to Q1."""
        reports = {
            "baseline": _FakeReport(quarterly_ecl=[0.01] * 4),
            "empty": _FakeReport(quarterly_ecl=None),
        }
        comp = ScenarioComparator(reports, baseline="baseline")
        result = comp.compare()
        assert result.peak_loss_quarter["empty"] == "Q1"

    def test_peak_loss_with_data(self):
        """Report with quarterly_ecl should find actual peak."""
        reports = {
            "baseline": _FakeReport(quarterly_ecl=[0.01, 0.02, 0.05, 0.03]),
            "adverse": _FakeReport(quarterly_ecl=[0.02, 0.04, 0.10, 0.06]),
        }
        comp = ScenarioComparator(reports, baseline="baseline")
        result = comp.compare()
        assert result.peak_loss_quarter["adverse"] == "Q3"


class TestSensitivityEqualDivergence:
    """Cover lines 142-144 (equal ECL divergence equal-split)."""

    def test_zero_divergence_equal_split(self):
        """When ECL divergence is 0, sensitivity is uniformly distributed."""
        reports = {
            "baseline": _FakeReport(quarterly_ecl=[0.01, 0.01]),
            "severely_adverse": _FakeReport(quarterly_ecl=[0.01, 0.01]),
        }
        comp = ScenarioComparator(reports, baseline="baseline")
        result = comp.compare()
        # All 5 macro vars should get 1/5 = 0.2
        for v in result.sensitivity_attribution.values():
            assert abs(v - 0.2) < 1e-4
