"""Test fixtures for insurance-copula.

Provides synthetic peril loss data, mock marginals, and helper
functions used across the test suite.

On ARM64 (Raspberry Pi), pyvinecopulib has no wheels. Tests that
require actual vine fitting are marked with
``@pytest.mark.skipif(not HAS_PYVINECOPULIB, ...)``.
"""

from __future__ import annotations

import numpy as np
import pandas as pd
import pytest
from scipy.stats import gamma, lognorm, expon

from insurance_copula import HAS_PYVINECOPULIB

# ------------------------------------------------------------------
# Standard peril set
# ------------------------------------------------------------------

PERILS = ["flood", "subsidence", "storm", "eow"]
PERILS_2 = ["flood", "storm"]
PERILS_6 = ["flood", "subsidence", "storm", "eow", "fire", "theft"]


# ------------------------------------------------------------------
# Synthetic loss data
# ------------------------------------------------------------------

@pytest.fixture(scope="session")
def rng() -> np.random.Generator:
    """Seeded numpy RNG for reproducible test data."""
    return np.random.default_rng(42)


@pytest.fixture(scope="session")
def synthetic_losses(rng: np.random.Generator) -> pd.DataFrame:
    """500-observation synthetic loss DataFrame for 4 perils.

    Losses are gamma-distributed with mild positive correlation
    introduced by adding a common factor. This ensures the vine
    copula has something non-trivial to fit.
    """
    n = 500
    common = rng.gamma(shape=1.5, scale=1.0, size=n)

    data = {
        "flood":      rng.gamma(shape=0.8 + 0.3 * common, scale=5_000.0, size=n),
        "subsidence": rng.gamma(shape=0.6 + 0.1 * common, scale=3_000.0, size=n),
        "storm":      rng.gamma(shape=1.2 + 0.2 * common, scale=800.0,   size=n),
        "eow":        rng.gamma(shape=1.5 + 0.2 * common, scale=600.0,   size=n),
    }
    return pd.DataFrame(data)


@pytest.fixture(scope="session")
def synthetic_losses_2perils(rng: np.random.Generator) -> pd.DataFrame:
    """200-observation synthetic loss DataFrame for 2 perils (flood, storm)."""
    n = 200
    return pd.DataFrame({
        "flood": rng.gamma(shape=0.8, scale=5_000.0, size=n),
        "storm": rng.gamma(shape=1.2, scale=800.0,   size=n),
    })


@pytest.fixture(scope="session")
def synthetic_losses_6perils(rng: np.random.Generator) -> pd.DataFrame:
    """300-observation synthetic loss DataFrame for 6 perils."""
    n = 300
    common = rng.gamma(shape=1.0, scale=1.0, size=n)
    return pd.DataFrame({
        "flood":      rng.gamma(shape=0.8 + 0.3 * common, scale=5_000.0, size=n),
        "subsidence": rng.gamma(shape=0.6 + 0.1 * common, scale=3_000.0, size=n),
        "storm":      rng.gamma(shape=1.2 + 0.2 * common, scale=800.0,   size=n),
        "eow":        rng.gamma(shape=1.5 + 0.2 * common, scale=600.0,   size=n),
        "fire":       rng.gamma(shape=1.0 + 0.1 * common, scale=2_000.0, size=n),
        "theft":      rng.gamma(shape=2.0 + 0.1 * common, scale=400.0,   size=n),
    })


@pytest.fixture(scope="session")
def exposure_series(synthetic_losses: pd.DataFrame) -> pd.Series:
    """Simulated earned exposure weights (days at risk, normalised)."""
    rng_local = np.random.default_rng(99)
    n = len(synthetic_losses)
    weights = rng_local.uniform(0.1, 1.0, size=n)
    return pd.Series(weights, index=synthetic_losses.index, name="exposure")


# ------------------------------------------------------------------
# Mock marginals
# ------------------------------------------------------------------

@pytest.fixture(scope="session")
def marginals_4perils() -> dict:
    """Frozen scipy distributions for 4-peril test cases."""
    return {
        "flood":      gamma(a=0.8, scale=5_000),
        "subsidence": gamma(a=0.6, scale=3_000),
        "storm":      gamma(a=1.2, scale=800),
        "eow":        gamma(a=1.5, scale=600),
    }


@pytest.fixture(scope="session")
def marginals_2perils() -> dict:
    """Frozen scipy distributions for 2-peril test cases."""
    return {
        "flood": gamma(a=0.8, scale=5_000),
        "storm": gamma(a=1.2, scale=800),
    }


@pytest.fixture(scope="session")
def marginals_6perils() -> dict:
    """Frozen scipy distributions for 6-peril test cases."""
    return {
        "flood":      gamma(a=0.8, scale=5_000),
        "subsidence": gamma(a=0.6, scale=3_000),
        "storm":      gamma(a=1.2, scale=800),
        "eow":        gamma(a=1.5, scale=600),
        "fire":       gamma(a=1.0, scale=2_000),
        "theft":      gamma(a=2.0, scale=400),
    }


@pytest.fixture(scope="session")
def marginals_lognorm() -> dict:
    """Lognormal marginals for testing back-transform."""
    return {
        "flood": lognorm(s=1.5, scale=1_000),
        "storm": lognorm(s=1.2, scale=500),
    }


# ------------------------------------------------------------------
# Fitted vine fixture (only available if pyvinecopulib present)
# ------------------------------------------------------------------

@pytest.fixture(scope="session")
def fitted_vine(synthetic_losses):
    """Pre-fitted PerilVine for 4 perils. Skipped on ARM64."""
    if not HAS_PYVINECOPULIB:
        pytest.skip("pyvinecopulib not available on this platform")

    from insurance_copula import PerilVine
    vine = PerilVine(perils=PERILS, trunc_lvl=2, n_threads=1)
    vine.fit(synthetic_losses)
    return vine


@pytest.fixture(scope="session")
def fitted_vine_2perils(synthetic_losses_2perils):
    """Pre-fitted PerilVine for 2 perils. Skipped on ARM64."""
    if not HAS_PYVINECOPULIB:
        pytest.skip("pyvinecopulib not available on this platform")

    from insurance_copula import PerilVine
    vine = PerilVine(perils=PERILS_2, trunc_lvl=1, n_threads=1)
    vine.fit(synthetic_losses_2perils)
    return vine
