"""Tests for AggregateResult and PerilVineDiagnostics dataclasses."""

from __future__ import annotations

import numpy as np
import pandas as pd
import pytest

from insurance_copula import AggregateResult, PerilVineDiagnostics


# ------------------------------------------------------------------
# AggregateResult
# ------------------------------------------------------------------

class TestAggregateResult:
    def make_result(self, n: int = 1000, seed: int = 42) -> AggregateResult:
        rng = np.random.default_rng(seed)
        sims = rng.gamma(shape=2.0, scale=100_000.0, size=n)
        return AggregateResult(
            simulations=sims,
            pml_1_in_100=float(np.percentile(sims, 99)),
            pml_1_in_200=float(np.percentile(sims, 99.5)),
            expected_loss=float(sims.mean()),
            coefficient_of_variation=float(sims.std(ddof=1) / sims.mean()),
        )

    def test_construction(self):
        result = self.make_result()
        assert isinstance(result.simulations, np.ndarray)
        assert result.n_sim == 1000

    def test_fields_are_floats(self):
        result = self.make_result()
        assert isinstance(result.pml_1_in_100, float)
        assert isinstance(result.pml_1_in_200, float)
        assert isinstance(result.expected_loss, float)
        assert isinstance(result.coefficient_of_variation, float)

    def test_simulations_coerced_to_array(self):
        result = AggregateResult(
            simulations=[100.0, 200.0, 300.0],  # type: ignore[arg-type]
            pml_1_in_100=300.0,
            pml_1_in_200=300.0,
            expected_loss=200.0,
            coefficient_of_variation=0.5,
        )
        assert isinstance(result.simulations, np.ndarray)

    def test_pml_ordering(self):
        result = self.make_result()
        # PML 1-in-200 should be >= PML 1-in-100
        assert result.pml_1_in_200 >= result.pml_1_in_100

    def test_pml_above_expected(self):
        result = self.make_result()
        assert result.pml_1_in_100 > result.expected_loss

    def test_cv_positive(self):
        result = self.make_result()
        assert result.coefficient_of_variation > 0

    def test_percentile_method(self):
        result = self.make_result(n=10000)
        p99 = result.percentile(99)
        assert abs(p99 - result.pml_1_in_100) < 1e-6

    def test_percentile_bounds(self):
        result = self.make_result()
        p0 = result.percentile(0)
        p100 = result.percentile(100)
        assert p0 <= result.expected_loss
        assert p100 >= result.pml_1_in_200

    def test_percentile_invalid_raises(self):
        result = self.make_result()
        with pytest.raises(ValueError, match="p must be in"):
            result.percentile(101)
        with pytest.raises(ValueError, match="p must be in"):
            result.percentile(-1)

    def test_n_sim_property(self):
        result = self.make_result(n=500)
        assert result.n_sim == 500

    def test_repr_contains_key_info(self):
        result = self.make_result()
        r = repr(result)
        assert "AggregateResult" in r
        assert "1,000" in r  # n_sim with comma formatting

    def test_empty_simulations_raises(self):
        with pytest.raises(ValueError, match="non-empty"):
            AggregateResult(
                simulations=np.array([]),
                pml_1_in_100=0.0,
                pml_1_in_200=0.0,
                expected_loss=0.0,
                coefficient_of_variation=0.0,
            )

    def test_2d_simulations_raises(self):
        with pytest.raises(ValueError, match="1-D"):
            AggregateResult(
                simulations=np.ones((10, 2)),
                pml_1_in_100=1.0,
                pml_1_in_200=1.0,
                expected_loss=1.0,
                coefficient_of_variation=0.1,
            )

    def test_small_simulation(self):
        result = AggregateResult(
            simulations=np.array([100.0, 200.0, 300.0]),
            pml_1_in_100=280.0,
            pml_1_in_200=295.0,
            expected_loss=200.0,
            coefficient_of_variation=0.5,
        )
        assert result.n_sim == 3

    def test_scalar_coercion(self):
        result = AggregateResult(
            simulations=np.array([1.0, 2.0, 3.0]),
            pml_1_in_100=3,  # int
            pml_1_in_200=3,
            expected_loss=2,
            coefficient_of_variation=0,
        )
        assert isinstance(result.pml_1_in_100, float)


# ------------------------------------------------------------------
# PerilVineDiagnostics
# ------------------------------------------------------------------

PERILS = ["flood", "subsidence", "storm", "eow"]


def make_tau_matrix(perils: list[str], fill: float = 0.0) -> pd.DataFrame:
    d = len(perils)
    m = np.eye(d) + fill * (1 - np.eye(d))
    return pd.DataFrame(m, index=perils, columns=perils)


def make_diag(perils: list[str] = PERILS) -> PerilVineDiagnostics:
    tau = make_tau_matrix(perils, fill=0.2)
    upper = make_tau_matrix(perils, fill=0.1)
    lower = make_tau_matrix(perils, fill=0.05)
    pair_copulas = [
        {"tree": 1, "edge": 1, "family": "Gaussian", "params": [0.3], "tau": 0.2},
        {"tree": 1, "edge": 2, "family": "Clayton",  "params": [0.5], "tau": 0.3},
        {"tree": 1, "edge": 3, "family": "Gumbel",   "params": [1.5], "tau": 0.25},
    ]
    return PerilVineDiagnostics(
        kendall_tau_matrix=tau,
        tail_dependence_upper=upper,
        tail_dependence_lower=lower,
        aic=-234.5,
        bic=-220.1,
        n_parameters=6,
        pair_copulas=pair_copulas,
    )


class TestPerilVineDiagnostics:
    def test_construction(self):
        diag = make_diag()
        assert isinstance(diag.kendall_tau_matrix, pd.DataFrame)
        assert isinstance(diag.tail_dependence_upper, pd.DataFrame)
        assert isinstance(diag.tail_dependence_lower, pd.DataFrame)

    def test_aic_bic_are_floats(self):
        diag = make_diag()
        assert isinstance(diag.aic, float)
        assert isinstance(diag.bic, float)

    def test_n_parameters_is_int(self):
        diag = make_diag()
        assert isinstance(diag.n_parameters, int)
        assert diag.n_parameters == 6

    def test_pair_copulas_list(self):
        diag = make_diag()
        assert isinstance(diag.pair_copulas, list)
        assert len(diag.pair_copulas) == 3

    def test_pair_copulas_not_list_raises(self):
        tau = make_tau_matrix(PERILS)
        with pytest.raises(TypeError, match="list"):
            PerilVineDiagnostics(
                kendall_tau_matrix=tau,
                tail_dependence_upper=tau,
                tail_dependence_lower=tau,
                aic=-100.0,
                bic=-90.0,
                n_parameters=3,
                pair_copulas="not a list",  # type: ignore[arg-type]
            )

    def test_summary_table_returns_dataframe(self):
        diag = make_diag()
        table = diag.summary_table()
        assert isinstance(table, pd.DataFrame)
        assert "family" in table.columns
        assert "tau" in table.columns
        assert len(table) == 3

    def test_summary_table_empty(self):
        tau = make_tau_matrix(PERILS)
        diag = PerilVineDiagnostics(
            kendall_tau_matrix=tau,
            tail_dependence_upper=tau,
            tail_dependence_lower=tau,
            aic=-50.0,
            bic=-40.0,
            n_parameters=0,
            pair_copulas=[],
        )
        table = diag.summary_table()
        assert isinstance(table, pd.DataFrame)
        assert len(table) == 0

    def test_repr_contains_info(self):
        diag = make_diag()
        r = repr(diag)
        assert "PerilVineDiagnostics" in r
        assert "BIC" in r
        assert "n_parameters" in r

    def test_matrix_dimensions(self):
        diag = make_diag()
        d = len(PERILS)
        assert diag.kendall_tau_matrix.shape == (d, d)
        assert diag.tail_dependence_upper.shape == (d, d)
        assert diag.tail_dependence_lower.shape == (d, d)

    def test_matrix_index_columns(self):
        diag = make_diag()
        assert list(diag.kendall_tau_matrix.index) == PERILS
        assert list(diag.kendall_tau_matrix.columns) == PERILS

    def test_tau_diagonal_values(self):
        diag = make_diag()
        diag_vals = np.diag(diag.kendall_tau_matrix.values)
        np.testing.assert_allclose(diag_vals, 1.0)

    def test_negative_aic_bic(self):
        # AIC/BIC are often negative — ensure no validation rejects them
        diag = make_diag()
        assert diag.aic < 0
        assert diag.bic < 0

    def test_int_coercion(self):
        tau = make_tau_matrix(PERILS)
        diag = PerilVineDiagnostics(
            kendall_tau_matrix=tau,
            tail_dependence_upper=tau,
            tail_dependence_lower=tau,
            aic=-100,  # int
            bic=-90,
            n_parameters=3.0,  # float -> int  # type: ignore[arg-type]
            pair_copulas=[],
        )
        assert isinstance(diag.aic, float)
        assert isinstance(diag.n_parameters, int)
