"""Tests for orxaq.orchestration.run — RunConfig, RunResult, run_pipeline."""

from __future__ import annotations

import dataclasses
import json
from pathlib import Path

import pytest

from orxaq.orchestration.run import RunConfig, RunResult, _load_portfolio_file, run_pipeline

# ── RunConfig ───────────────────────────────────────────────────────────────


def test_run_config_institution_id_required() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.institution_id == "BANK001"


def test_run_config_period_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.period == "2026Q1"


def test_run_config_output_dir_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.output_dir == "./filing"


def test_run_config_n_loans_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.n_loans == 200


def test_run_config_seed_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.seed == 42


def test_run_config_horizon_quarters_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.horizon_quarters == 9


def test_run_config_include_fair_lending_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert cfg.include_fair_lending is True


def test_run_config_is_dataclass() -> None:
    assert dataclasses.is_dataclass(RunConfig)


def test_run_config_period_stored_correctly() -> None:
    cfg = RunConfig(institution_id="BANK001", period="2025Q4")
    assert cfg.period == "2025Q4"


def test_run_config_scenario_names_default() -> None:
    cfg = RunConfig(institution_id="BANK001")
    assert "baseline" in cfg.scenario_names
    assert "adverse" in cfg.scenario_names
    assert "severely_adverse" in cfg.scenario_names


# ── RunResult fields ────────────────────────────────────────────────────────


def test_run_result_is_dataclass() -> None:
    assert dataclasses.is_dataclass(RunResult)


def test_run_result_has_required_fields() -> None:
    fields = {f.name for f in dataclasses.fields(RunResult)}
    expected = {
        "institution_id",
        "period",
        "output_dir",
        "files",
        "duration_s",
        "audit_entries",
        "ecl_total",
        "stress_summaries",
        "fair_lending_flags",
    }
    assert expected <= fields


# ── run_pipeline ─────────────────────────────────────────────────────────────


@pytest.fixture()
def small_config(tmp_path: Path) -> RunConfig:
    """Fast pipeline config using n_loans=50."""
    return RunConfig(
        institution_id="TEST001",
        period="2026Q1",
        output_dir=str(tmp_path / "filing"),
        n_loans=50,
        seed=42,
    )


def test_run_pipeline_creates_output_directory(small_config: RunConfig, tmp_path: Path) -> None:
    result = run_pipeline(small_config)
    assert Path(result.output_dir).is_dir()


def test_run_pipeline_returns_positive_duration(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert result.duration_s > 0.0


def test_run_pipeline_duration_is_float(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert isinstance(result.duration_s, float)


def test_run_pipeline_returns_nonempty_files(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert len(result.files) > 0


def test_run_pipeline_ecl_total_positive(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert result.ecl_total > 0.0


def test_run_pipeline_audit_entries_nonnegative(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert isinstance(result.audit_entries, int)
    assert result.audit_entries >= 0


def test_run_pipeline_institution_id_matches(
    small_config: RunConfig,
) -> None:
    result = run_pipeline(small_config)
    assert result.institution_id == small_config.institution_id


def test_run_pipeline_period_matches(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert result.period == small_config.period


def test_run_pipeline_produces_cecl_disclosure(
    small_config: RunConfig,
) -> None:
    result = run_pipeline(small_config)
    assert (Path(result.output_dir) / "cecl_disclosure.json").exists()


def test_run_pipeline_produces_manifest(small_config: RunConfig) -> None:
    result = run_pipeline(small_config)
    assert (Path(result.output_dir) / "manifest.json").exists()


def test_run_pipeline_custom_n_loans(tmp_path: Path) -> None:
    cfg = RunConfig(
        institution_id="BANK002",
        output_dir=str(tmp_path / "out"),
        n_loans=30,
    )
    result = run_pipeline(cfg)
    assert result.duration_s > 0.0


def test_run_pipeline_custom_seed_reproducible(tmp_path: Path) -> None:
    cfg_a = RunConfig(
        institution_id="SEED_TEST",
        output_dir=str(tmp_path / "a"),
        n_loans=30,
        seed=99,
    )
    cfg_b = RunConfig(
        institution_id="SEED_TEST",
        output_dir=str(tmp_path / "b"),
        n_loans=30,
        seed=99,
    )
    r_a = run_pipeline(cfg_a)
    r_b = run_pipeline(cfg_b)
    assert round(r_a.ecl_total, 4) == round(r_b.ecl_total, 4)


def test_run_pipeline_without_fair_lending(tmp_path: Path) -> None:
    cfg = RunConfig(
        institution_id="BANK003",
        output_dir=str(tmp_path / "out"),
        n_loans=30,
        include_fair_lending=False,
    )
    result = run_pipeline(cfg)
    assert result.fair_lending_flags == []


def test_run_pipeline_with_fair_lending(tmp_path: Path) -> None:
    cfg = RunConfig(
        institution_id="BANK004",
        output_dir=str(tmp_path / "out"),
        n_loans=30,
        include_fair_lending=True,
    )
    result = run_pipeline(cfg)
    # flags is a list (may be empty if no direct effect found)
    assert isinstance(result.fair_lending_flags, list)


def test_run_pipeline_stress_summaries_has_scenarios(
    small_config: RunConfig,
) -> None:
    result = run_pipeline(small_config)
    # Scenario comparison CSV always has the three Fed scenarios
    if result.stress_summaries:
        fed_scenarios = ("baseline", "adverse", "severely_adverse")
        assert any(s in result.stress_summaries for s in fed_scenarios)


def test_run_pipeline_creates_directory_for_new_path(tmp_path: Path) -> None:
    nested = str(tmp_path / "deep" / "nested" / "out")
    cfg = RunConfig(institution_id="NEWDIR", output_dir=nested, n_loans=30)
    result = run_pipeline(cfg)
    assert Path(result.output_dir).is_dir()


# ── _load_portfolio_file JSON path (lines 168-170) ──────────────────────────


def test_load_portfolio_file_json_list(tmp_path: Path) -> None:
    """Lines 168-170: JSON file path (list of dicts)."""
    loans = [{"credit_score": "720", "dti": "0.35"}, {"credit_score": "650", "dti": "0.40"}]
    p = tmp_path / "loans.json"
    p.write_text(json.dumps(loans))
    result = _load_portfolio_file(str(p))
    assert isinstance(result, list)
    assert len(result) == 2
    assert result[0]["credit_score"] == 720.0


def test_load_portfolio_file_json_dict_with_loans_key(tmp_path: Path) -> None:
    """Lines 168-170: JSON dict with 'loans' key."""
    payload = {"loans": [{"credit_score": "700", "ltv": "0.8"}], "meta": "x"}
    p = tmp_path / "portfolio.json"
    p.write_text(json.dumps(payload))
    result = _load_portfolio_file(str(p))
    assert len(result) == 1
    assert result[0]["ltv"] == 0.8


def test_load_portfolio_file_not_found_raises(tmp_path: Path) -> None:
    """Line 159: FileNotFoundError when file missing."""
    with pytest.raises(FileNotFoundError):
        _load_portfolio_file(str(tmp_path / "missing.json"))
