"""Tests for new FR Y-14A schedule builder API in orxaq.credit.regulatory_export.

Covers: CCARSchedule fields, 9-quarter CCAR horizon, segment breakdowns,
JSON schema compliance, CECLDisclosure fields, quantitative accuracy,
integration with real credit pipeline (30 tests).
"""

from __future__ import annotations

import json
import re

import pytest

from orxaq.credit.regulatory_export import (
    JSON_SCHEMA_CECL,
    JSON_SCHEMA_FR_Y14,
    CCARSchedule,
    CECLDisclosure,
    RegulatoryExporter,
    _generate_quarter_labels,
)

# ── Shared fixtures ───────────────────────────────────────────────────────────

_QUARTER_RE = re.compile(r"^Q[1-4]-\d{4}$")


@pytest.fixture(scope="module")
def exporter() -> RegulatoryExporter:
    return RegulatoryExporter(institution_id="TEST001", reporting_period="2024-Q4")


@pytest.fixture(scope="module")
def ecl_result(exporter: RegulatoryExporter):
    """Real ECLResult from the full credit pipeline."""
    return _run_ecl(exporter)


@pytest.fixture(scope="module")
def stress_report_adverse(exporter: RegulatoryExporter):
    """Real StressTestReport for the adverse scenario."""
    return _run_stress(exporter, "adverse")


@pytest.fixture(scope="module")
def stress_report_severe(exporter: RegulatoryExporter):
    """Real StressTestReport for the severely_adverse scenario."""
    return _run_stress(exporter, "severely_adverse")


@pytest.fixture(scope="module")
def stress_report_baseline(exporter: RegulatoryExporter):
    """Real StressTestReport for the baseline scenario."""
    return _run_stress(exporter, "baseline")


@pytest.fixture(scope="module")
def ccar_schedule(exporter: RegulatoryExporter, stress_report_adverse, ecl_result) -> CCARSchedule:
    return exporter.build_fr_y14_schedule(
        stress_report=stress_report_adverse,
        ecl_result=ecl_result,
        institution_name="Orxaq Bank",
    )


@pytest.fixture(scope="module")
def cecl_disclosure(exporter: RegulatoryExporter, ecl_result) -> CECLDisclosure:
    return exporter.build_cecl_disclosure(ecl_result=ecl_result)


def _run_ecl(exp: RegulatoryExporter):
    from orxaq.credit.causal_pd import CausalPDModel
    from orxaq.credit.cecl import CECLEngine
    from orxaq.kernel import WorldDAG

    dag = WorldDAG()
    pd_model = CausalPDModel(dag)
    portfolio = exp._get_portfolio()
    pd_model.fit(portfolio)  # type: ignore[arg-type]
    engine = CECLEngine(pd_model, dag)
    return engine.expected_credit_loss(portfolio)  # type: ignore[arg-type]


def _run_stress(exp: RegulatoryExporter, scenario_name: str):
    from orxaq.credit.causal_pd import CausalPDModel
    from orxaq.credit.cecl import CECLEngine
    from orxaq.credit.portfolio import LoanExposure
    from orxaq.credit.stress_report import FED_SCENARIOS, run_stress_test
    from orxaq.kernel import WorldDAG

    dag = WorldDAG()
    pd_model = CausalPDModel(dag)
    portfolio = exp._get_portfolio()
    pd_model.fit(portfolio)  # type: ignore[arg-type]
    cecl_engine = CECLEngine(pd_model, dag)
    exposures = [
        LoanExposure(
            loan_id=str(i),
            pd=pd_model.predict_pd(row),  # type: ignore[arg-type]
            lgd=float(row.get("lgd", 0.45)),  # type: ignore[arg-type]
            ead=float(row.get("loan_amount", 100_000.0)),  # type: ignore[arg-type]
            sector=str(row.get("sector", "corporate")),
            vintage="2024",
        )
        for i, row in enumerate(portfolio)
    ]
    return run_stress_test(
        portfolio=exposures,
        scenarios={scenario_name: FED_SCENARIOS[scenario_name]},
        pd_model=pd_model,
        cecl_engine=cecl_engine,
        scenario_name=scenario_name,
    )


# ── TestCCARScheduleFields ────────────────────────────────────────────────────


class TestCCARScheduleFields:
    def test_has_9_quarters(self, ccar_schedule: CCARSchedule) -> None:
        assert ccar_schedule.horizon_quarters == 9
        assert len(ccar_schedule.quarters) == 9

    def test_quarter_labels_format(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            assert _QUARTER_RE.match(q.quarter), f"Bad label: {q.quarter!r}"

    def test_projected_loss_positive(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            assert q.projected_loss >= 0.0

    def test_capital_ratio_impact_bounded(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            assert 0.0 <= q.capital_ratio_impact <= 0.5, q.capital_ratio_impact

    def test_total_loss_equals_sum_of_quarters(self, ccar_schedule: CCARSchedule) -> None:
        total = sum(q.projected_loss for q in ccar_schedule.quarters)
        assert abs(ccar_schedule.total_projected_loss - total) < 1e-6

    def test_institution_name_present(self, ccar_schedule: CCARSchedule) -> None:
        assert ccar_schedule.institution_name == "Orxaq Bank"

    def test_scenario_field_matches_input(self, ccar_schedule: CCARSchedule) -> None:
        assert ccar_schedule.scenario == "adverse"

    def test_generated_at_is_iso_string(self, ccar_schedule: CCARSchedule) -> None:
        assert "T" in ccar_schedule.generated_at  # ISO-8601 format


# ── TestSegmentBreakdown ──────────────────────────────────────────────────────


class TestSegmentBreakdown:
    def test_segment_breakdown_present(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            assert isinstance(q.segment_breakdown, dict)
            assert len(q.segment_breakdown) > 0

    def test_segment_breakdown_sums_to_quarter_loss(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            seg_sum = sum(q.segment_breakdown.values())
            assert abs(seg_sum - q.projected_loss) < 1e-9, (
                f"Q={q.quarter}: seg_sum={seg_sum} != loss={q.projected_loss}"
            )

    def test_segment_keys_nonempty(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            for key in q.segment_breakdown:
                assert len(key) > 0


# ── TestJSONSchemaCompliance ──────────────────────────────────────────────────


class TestJSONSchemaCompliance:
    def test_to_json_is_valid_json(
        self, exporter: RegulatoryExporter, ccar_schedule: CCARSchedule
    ) -> None:
        raw = exporter.to_json(ccar_schedule)
        parsed = json.loads(raw)
        assert isinstance(parsed, dict)

    def test_required_fields_present(
        self, exporter: RegulatoryExporter, ccar_schedule: CCARSchedule
    ) -> None:
        raw = json.loads(exporter.to_json(ccar_schedule))
        for req in JSON_SCHEMA_FR_Y14["required"]:
            assert req in raw, f"Missing required field: {req}"

    def test_validate_schema_passes_complete_dict(
        self, exporter: RegulatoryExporter, ccar_schedule: CCARSchedule
    ) -> None:
        import dataclasses

        d = dataclasses.asdict(ccar_schedule)
        missing = exporter.validate_schema(d)
        assert missing == [], f"Unexpected missing fields: {missing}"

    def test_validate_schema_catches_missing_field(self, exporter: RegulatoryExporter) -> None:
        partial = {"institution_name": "X", "scenario": "baseline"}
        missing = exporter.validate_schema(partial)
        assert len(missing) > 0
        assert "quarters" in missing

    def test_validate_schema_cecl_schema(
        self, exporter: RegulatoryExporter, cecl_disclosure: CECLDisclosure
    ) -> None:
        import dataclasses

        d = dataclasses.asdict(cecl_disclosure)
        missing = exporter.validate_schema(d, schema=JSON_SCHEMA_CECL)
        assert missing == [], f"Unexpected CECL missing fields: {missing}"


# ── TestCECLDisclosure ────────────────────────────────────────────────────────


class TestCECLDisclosure:
    def test_methodology_nonempty_string(self, cecl_disclosure: CECLDisclosure) -> None:
        assert isinstance(cecl_disclosure.methodology, str)
        assert len(cecl_disclosure.methodology) > 0

    def test_key_assumptions_is_list(self, cecl_disclosure: CECLDisclosure) -> None:
        assert isinstance(cecl_disclosure.key_assumptions, list)
        assert len(cecl_disclosure.key_assumptions) >= 3

    def test_macro_sensitivity_is_dict_of_floats(self, cecl_disclosure: CECLDisclosure) -> None:
        assert isinstance(cecl_disclosure.macro_sensitivity, dict)
        for k, v in cecl_disclosure.macro_sensitivity.items():
            assert isinstance(k, str)
            assert isinstance(v, float)

    def test_allowance_estimate_positive(self, cecl_disclosure: CECLDisclosure) -> None:
        assert cecl_disclosure.allowance_estimate >= 0.0

    def test_confidence_band_ordered(self, cecl_disclosure: CECLDisclosure) -> None:
        assert cecl_disclosure.confidence_lower <= cecl_disclosure.allowance_estimate
        assert cecl_disclosure.allowance_estimate <= cecl_disclosure.confidence_upper

    def test_reporting_date_matches_period(
        self, cecl_disclosure: CECLDisclosure, exporter: RegulatoryExporter
    ) -> None:
        assert cecl_disclosure.reporting_date == exporter.reporting_period

    def test_qualitative_adjustments_nonempty(self, cecl_disclosure: CECLDisclosure) -> None:
        assert len(cecl_disclosure.qualitative_adjustments) > 0

    def test_custom_macro_vars(self, exporter: RegulatoryExporter, ecl_result) -> None:
        custom = {"my_shock": 0.05, "gdp_shock": -0.10}
        d = exporter.build_cecl_disclosure(ecl_result, macro_vars=custom)
        assert d.macro_sensitivity == custom


# ── TestQuantitativeAccuracy ──────────────────────────────────────────────────


class TestQuantitativeAccuracy:
    def test_loss_arc_peaks_mid_horizon(self, ccar_schedule: CCARSchedule) -> None:
        losses = [q.projected_loss for q in ccar_schedule.quarters]
        peak_idx = losses.index(max(losses))
        # Peak should be in quarters 3-5 (0-indexed: 2-4)
        assert 2 <= peak_idx <= 4, f"Peak at {peak_idx}, expected 2-4"

    def test_severely_adverse_higher_than_baseline(
        self,
        exporter: RegulatoryExporter,
        stress_report_severe,
        stress_report_baseline,
        ecl_result,
    ) -> None:
        sched_severe = exporter.build_fr_y14_schedule(
            stress_report=stress_report_severe, ecl_result=ecl_result
        )
        sched_baseline = exporter.build_fr_y14_schedule(
            stress_report=stress_report_baseline, ecl_result=ecl_result
        )
        assert sched_severe.total_projected_loss > sched_baseline.total_projected_loss

    def test_ppnr_proxy_is_fraction_of_loss(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            if q.projected_loss > 0:
                assert q.ppnr_proxy < q.projected_loss

    def test_segment_weights_sum_to_quarter_loss(self, ccar_schedule: CCARSchedule) -> None:
        for q in ccar_schedule.quarters:
            seg_total = sum(q.segment_breakdown.values())
            assert abs(seg_total - q.projected_loss) < 1e-9

    def test_quarter_label_sequence(self, exporter: RegulatoryExporter, ecl_result) -> None:
        sched = exporter.build_fr_y14_schedule(
            stress_report=type("SR", (), {"scenario_name": "baseline"})(),
            ecl_result=ecl_result,
            base_quarter="Q1-2024",
        )
        labels = [q.quarter for q in sched.quarters]
        assert labels[0] == "Q1-2024"
        assert labels[3] == "Q4-2024"
        assert labels[4] == "Q1-2025"
        assert labels[8] == "Q1-2026"


# ── TestGenerateQuarterLabels ─────────────────────────────────────────────────


class TestGenerateQuarterLabels:
    def test_generates_correct_count(self) -> None:
        labels = _generate_quarter_labels("Q1-2024", 9)
        assert len(labels) == 9

    def test_wraps_at_q4(self) -> None:
        labels = _generate_quarter_labels("Q4-2024", 4)
        assert labels == ["Q4-2024", "Q1-2025", "Q2-2025", "Q3-2025"]

    def test_invalid_base_falls_back(self) -> None:
        labels = _generate_quarter_labels("invalid", 3)
        assert len(labels) == 3
        assert labels[0] == "Q1-2024"
