"""Tests for aggregate portfolio loss simulation."""

from __future__ import annotations

import numpy as np
import pytest
from scipy.stats import gamma

from insurance_copula import PerilVine, AggregateResult, HAS_PYVINECOPULIB
from insurance_copula._aggregate import simulate_aggregate_loss

PERILS = ["flood", "subsidence", "storm", "eow"]
REQUIRES_PV = pytest.mark.skipif(
    not HAS_PYVINECOPULIB,
    reason="pyvinecopulib not available on this platform",
)


class TestAggregateResult:
    """Tests that don't require a fitted vine — direct dataclass tests."""

    def make_sims(self, n: int = 1000) -> np.ndarray:
        rng = np.random.default_rng(7)
        return rng.gamma(shape=2.0, scale=1_000_000.0, size=n)

    def test_from_manual_array(self):
        sims = self.make_sims()
        result = 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()),
        )
        assert result.n_sim == 1000
        assert result.expected_loss > 0
        assert result.pml_1_in_200 >= result.pml_1_in_100

    def test_percentile_50_near_median(self):
        sims = self.make_sims(n=10000)
        result = 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=0.5,
        )
        p50 = result.percentile(50)
        median = float(np.median(sims))
        assert abs(p50 - median) < 1e-6

    def test_cv_less_than_one_for_gamma(self):
        # Gamma(shape=2) has CV=1/sqrt(2) ~ 0.71
        sims = self.make_sims(n=50000)
        result = 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()),
        )
        assert 0.5 < result.coefficient_of_variation < 1.0


class TestSimulateAggregateLoss:
    @REQUIRES_PV
    def test_returns_aggregate_result(self, fitted_vine, marginals_4perils):
        result = fitted_vine.aggregate_loss(
            n_sim=100,
            marginals=marginals_4perils,
            n_policies=100,
            seed=42,
        )
        assert isinstance(result, AggregateResult)

    @REQUIRES_PV
    def test_n_sim_correct(self, fitted_vine, marginals_4perils):
        result = fitted_vine.aggregate_loss(
            n_sim=200,
            marginals=marginals_4perils,
            n_policies=50,
            seed=0,
        )
        assert result.n_sim == 200

    @REQUIRES_PV
    def test_all_simulations_positive(self, fitted_vine, marginals_4perils):
        result = fitted_vine.aggregate_loss(
            n_sim=100,
            marginals=marginals_4perils,
            n_policies=100,
            seed=1,
        )
        assert (result.simulations >= 0).all()

    @REQUIRES_PV
    def test_pml_ordering(self, fitted_vine, marginals_4perils):
        result = fitted_vine.aggregate_loss(
            n_sim=500,
            marginals=marginals_4perils,
            n_policies=100,
            seed=2,
        )
        assert result.pml_1_in_200 >= result.pml_1_in_100
        assert result.pml_1_in_100 >= result.expected_loss

    @REQUIRES_PV
    def test_cv_positive(self, fitted_vine, marginals_4perils):
        result = fitted_vine.aggregate_loss(
            n_sim=200,
            marginals=marginals_4perils,
            n_policies=100,
            seed=3,
        )
        assert result.coefficient_of_variation > 0

    @REQUIRES_PV
    def test_reproducible_with_seed(self, fitted_vine, marginals_4perils):
        r1 = fitted_vine.aggregate_loss(
            n_sim=50, marginals=marginals_4perils, n_policies=50, seed=42
        )
        r2 = fitted_vine.aggregate_loss(
            n_sim=50, marginals=marginals_4perils, n_policies=50, seed=42
        )
        np.testing.assert_allclose(r1.simulations, r2.simulations)

    @REQUIRES_PV
    def test_more_policies_higher_expected_loss(
        self, fitted_vine, marginals_4perils
    ):
        r_small = fitted_vine.aggregate_loss(
            n_sim=200, marginals=marginals_4perils, n_policies=100, seed=10
        )
        r_large = fitted_vine.aggregate_loss(
            n_sim=200, marginals=marginals_4perils, n_policies=1000, seed=10
        )
        assert r_large.expected_loss > r_small.expected_loss

    def test_unfitted_raises(self):
        vine = PerilVine(perils=PERILS)
        with pytest.raises(RuntimeError, match="not been fitted"):
            vine.aggregate_loss(
                n_sim=10,
                marginals={},
                n_policies=100,
            )

    @REQUIRES_PV
    def test_n_sim_zero_raises(self, fitted_vine, marginals_4perils):
        with pytest.raises(ValueError, match="n_sim"):
            fitted_vine.aggregate_loss(
                n_sim=0,
                marginals=marginals_4perils,
                n_policies=100,
            )

    @REQUIRES_PV
    def test_n_policies_zero_raises(self, fitted_vine, marginals_4perils):
        with pytest.raises(ValueError, match="n_policies"):
            fitted_vine.aggregate_loss(
                n_sim=10,
                marginals=marginals_4perils,
                n_policies=0,
            )

    @REQUIRES_PV
    def test_missing_marginal_raises(self, fitted_vine):
        partial = {"flood": gamma(a=0.8, scale=5000)}
        with pytest.raises(ValueError, match="subsidence"):
            fitted_vine.aggregate_loss(
                n_sim=10,
                marginals=partial,
                n_policies=100,
            )

    @REQUIRES_PV
    def test_solvency_ii_scr_level(self, fitted_vine, marginals_4perils):
        """PML at 99.5% is the Solvency II SCR metric."""
        result = fitted_vine.aggregate_loss(
            n_sim=1000,
            marginals=marginals_4perils,
            n_policies=1000,
            seed=42,
        )
        scr = result.pml_1_in_200
        assert scr > result.expected_loss
        assert scr > result.pml_1_in_100 * 0.9  # Within 10% is reasonable
