"""Tests for RegulatorReportExporter — CECL/CCAR/DFAST regulatory HTML/MD reports."""

from __future__ import annotations

import ast
import pathlib
import tempfile

import pytest

from orxaq.credit.cecl import ECLResult
from orxaq.credit.portfolio import LoanExposure
from orxaq.credit.regulatory_report import RegulatorReportConfig, RegulatorReportExporter
from orxaq.credit.reporting import ReportSection, RiskReport
from orxaq.credit.stress_report import StressTestReport

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


def _make_ecl_result(
    total_ecl: float = 12_000.0,
    ead_total: float = 1_000_000.0,
    pd_mean: float = 0.05,
) -> ECLResult:
    return ECLResult(
        total_ecl=total_ecl,
        ecl_by_segment={"retail": total_ecl * 0.6, "commercial": total_ecl * 0.4},
        horizon_quarters=1,
        horizon_method="soliton_dynamic",
        confidence_band=(total_ecl * 0.8, total_ecl * 1.3),
        pd_mean=pd_mean,
        lgd=0.45,
        ead_total=ead_total,
    )


def _make_risk_report() -> RiskReport:
    return RiskReport(
        report_id="RPT-2026-001",
        generated_at="2026-02-18T09:00:00+00:00",
        sections=[
            ReportSection(
                title="Credit Risk Overview",
                metrics={"expected_loss": 12_000.0, "hhi": 0.12},
                narrative="Portfolio exhibits moderate concentration.",
            )
        ],
        summary="Portfolio risk is moderate. ECL within acceptable bounds.",
        risk_rating="moderate",
    )


def _make_stress_report(scenario: str = "adverse") -> StressTestReport:
    q_ecl = [
        11_000.0,
        10_800.0,
        10_500.0,
        10_200.0,
        9_800.0,
        9_400.0,
        9_000.0,
        8_600.0,
        8_200.0,
    ]
    return StressTestReport(
        scenario_name=scenario,
        quarterly_ecl=q_ecl,
        portfolio_loss_rate=0.0875,
        capital_impact=0.007,
        migration_matrices=[
            {
                "AAA": {"AAA": 0.9, "AA": 0.08, "A": 0.02},
                "AA": {"AAA": 0.01, "AA": 0.88, "A": 0.11},
                "A": {"AA": 0.02, "A": 0.85, "BBB": 0.13},
            },
            {
                "AAA": {"AAA": 0.85, "AA": 0.12, "A": 0.03},
                "AA": {"AAA": 0.005, "AA": 0.84, "A": 0.155},
                "A": {"AA": 0.01, "A": 0.80, "BBB": 0.19},
            },
        ],
        confidence_intervals={
            "total_ecl": (80_000.0, 130_000.0),
            "portfolio_loss_rate": (0.07, 0.11),
            "capital_impact": (0.006, 0.009),
        },
        generated_at="2026-02-18T09:00:00+00:00",
    )


def _make_portfolio() -> list[LoanExposure]:
    return [
        LoanExposure(
            loan_id=f"LN-{i:04d}",
            pd=0.04 + i * 0.005,
            lgd=0.45,
            ead=100_000.0 + i * 10_000,
            sector="retail",
            vintage="2024Q1",
        )
        for i in range(8)
    ] + [
        LoanExposure(
            loan_id=f"COM-{i:04d}",
            pd=0.03 + i * 0.004,
            lgd=0.40,
            ead=200_000.0 + i * 20_000,
            sector="commercial",
            vintage="2024Q2",
        )
        for i in range(4)
    ]


@pytest.fixture()
def exporter() -> RegulatorReportExporter:
    return RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=_make_ecl_result(),
        portfolio=_make_portfolio(),
    )


# ---------------------------------------------------------------------------
# 1. Instantiation
# ---------------------------------------------------------------------------


def test_instantiation_with_valid_inputs() -> None:
    exp = RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=_make_ecl_result(),
    )
    assert exp is not None


# ---------------------------------------------------------------------------
# 2. to_html() starts with DOCTYPE
# ---------------------------------------------------------------------------


def test_to_html_returns_string_starting_with_doctype(
    exporter: RegulatorReportExporter,
) -> None:
    out = exporter.to_html()
    assert isinstance(out, str)
    assert out.strip().startswith("<!DOCTYPE html>")


# ---------------------------------------------------------------------------
# 3. HTML contains all 7 required section headings
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "heading",
    [
        "Executive Summary",
        "Portfolio Composition",
        "ECL Estimates",
        "Stress Test Results",
        "Migration Matrix",
        "Capital Adequacy",
        "Fair Lending",
    ],
)
def test_html_contains_required_section_headings(
    exporter: RegulatorReportExporter,
    heading: str,
) -> None:
    out = exporter.to_html()
    assert heading in out, f"Section '{heading}' not found in HTML output"


# ---------------------------------------------------------------------------
# 4. ECL total in HTML matches ecl_result.total_ecl
# ---------------------------------------------------------------------------


def test_ecl_total_in_html_matches_ecl_result(exporter: RegulatorReportExporter) -> None:
    out = exporter.to_html()
    assert "$12,000.00" in out or "12,000" in out


# ---------------------------------------------------------------------------
# 5. Confidence interval lower <= total <= upper
# ---------------------------------------------------------------------------


def test_confidence_interval_brackets_total_ecl() -> None:
    ecl = _make_ecl_result(total_ecl=10_000.0)
    RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=ecl,
    )
    ci_low, ci_high = ecl.confidence_band
    assert ci_low <= ecl.total_ecl <= ci_high


# ---------------------------------------------------------------------------
# 6. Each Fed scenario appears in stress results section
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("scenario", ["adverse", "baseline", "severely_adverse"])
def test_scenario_name_appears_in_stress_results(scenario: str) -> None:
    exp = RegulatorReportExporter(
        stress_report=_make_stress_report(scenario=scenario),
        risk_report=_make_risk_report(),
        ecl_result=_make_ecl_result(),
    )
    out = exp.to_html()
    assert scenario.replace("_", " ").title() in out


# ---------------------------------------------------------------------------
# 7. Migration matrix table has correct row/column structure
# ---------------------------------------------------------------------------


def test_migration_matrix_table_rows_and_columns(exporter: RegulatorReportExporter) -> None:
    out = exporter.to_html()
    assert "Migration Matrix" in out
    assert "<table>" in out
    assert "<th>" in out
    assert "AAA" in out


# ---------------------------------------------------------------------------
# 8. to_markdown() returns non-empty string
# ---------------------------------------------------------------------------


def test_to_markdown_returns_non_empty_string(exporter: RegulatorReportExporter) -> None:
    md = exporter.to_markdown()
    assert isinstance(md, str)
    assert len(md) > 100


# ---------------------------------------------------------------------------
# 9. Markdown contains all section headings
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "heading",
    [
        "Executive Summary",
        "Portfolio Composition",
        "ECL Estimates",
        "Stress Test Results",
        "Migration Matrix",
        "Capital Adequacy",
        "Fair Lending",
    ],
)
def test_markdown_contains_all_section_headings(
    exporter: RegulatorReportExporter,
    heading: str,
) -> None:
    md = exporter.to_markdown()
    assert heading in md, f"Section '{heading}' not found in Markdown output"


# ---------------------------------------------------------------------------
# 10. save() writes HTML file to disk
# ---------------------------------------------------------------------------


def test_save_writes_html_file_to_disk(exporter: RegulatorReportExporter) -> None:
    with tempfile.TemporaryDirectory() as tmpdir:
        out = pathlib.Path(tmpdir) / "report.html"
        exporter.save(out, format="html")
        assert out.exists()
        content = out.read_text(encoding="utf-8")
        assert content.startswith("<!DOCTYPE html>")


# ---------------------------------------------------------------------------
# 11. save() with format=markdown writes .md file
# ---------------------------------------------------------------------------


def test_save_markdown_writes_md_file(exporter: RegulatorReportExporter) -> None:
    with tempfile.TemporaryDirectory() as tmpdir:
        out = pathlib.Path(tmpdir) / "report.md"
        exporter.save(out, format="markdown")
        assert out.exists()
        content = out.read_text(encoding="utf-8")
        assert "# CECL" in content


# ---------------------------------------------------------------------------
# 12. Zero external imports (no requests/pandas/numpy)
# ---------------------------------------------------------------------------


def test_zero_external_imports_in_module() -> None:
    src = pathlib.Path("src/orxaq/credit/regulatory_report.py").read_text()
    tree = ast.parse(src)
    forbidden = {"requests", "pandas", "numpy", "scipy", "httpx", "aiohttp"}
    imports_found: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                top = alias.name.split(".")[0]
                if top in forbidden:
                    imports_found.add(top)
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                top = node.module.split(".")[0]
                if top in forbidden:
                    imports_found.add(top)
    assert not imports_found, f"Forbidden imports found: {imports_found}"


# ---------------------------------------------------------------------------
# 13. HTML is valid enough (has html/head/body tags)
# ---------------------------------------------------------------------------


def test_html_has_required_structural_tags(exporter: RegulatorReportExporter) -> None:
    out = exporter.to_html()
    for tag in ["<html", "<head>", "<body>", "</html>"]:
        assert tag in out, f"Missing tag: {tag}"


# ---------------------------------------------------------------------------
# 14. Numeric values in ECL section are consistent across sections
# ---------------------------------------------------------------------------


def test_numeric_consistency_ecl_across_sections() -> None:
    ecl = _make_ecl_result(total_ecl=25_000.0)
    exp = RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=ecl,
    )
    out = exp.to_html()
    assert out.count("25,000") >= 2


# ---------------------------------------------------------------------------
# 15. Fair lending section contains attestation language
# ---------------------------------------------------------------------------


def test_fair_lending_section_contains_attestation_language(
    exporter: RegulatorReportExporter,
) -> None:
    out = exporter.to_html()
    assert "Fair Lending" in out
    assert "Equal Credit" in out
    assert "attests" in out.lower() or "attestation" in out.lower()


# ---------------------------------------------------------------------------
# 16. Capital adequacy section contains tier-1 ratio reference
# ---------------------------------------------------------------------------


def test_capital_adequacy_section_contains_tier1_ratio(
    exporter: RegulatorReportExporter,
) -> None:
    out = exporter.to_html()
    assert "Capital Adequacy" in out
    assert "Tier-1" in out
    assert "8.0%" in out or "Basel III" in out


# ---------------------------------------------------------------------------
# 17. Config — disable fair lending suppresses section
# ---------------------------------------------------------------------------


def test_config_can_disable_fair_lending() -> None:
    cfg = RegulatorReportConfig(include_fair_lending=False)
    exp = RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=_make_ecl_result(),
        config=cfg,
    )
    out = exp.to_html()
    assert "Equal Credit" not in out


# ---------------------------------------------------------------------------
# 18. Portfolio with no loans gracefully handled
# ---------------------------------------------------------------------------


def test_empty_portfolio_handled_gracefully() -> None:
    exp = RegulatorReportExporter(
        stress_report=_make_stress_report(),
        risk_report=_make_risk_report(),
        ecl_result=_make_ecl_result(),
        portfolio=[],
    )
    out = exp.to_html()
    assert "<!DOCTYPE html>" in out
    assert "Executive Summary" in out
