"""End-to-end integration tests for insurance-copula workflows.

These tests simulate real pricing team usage patterns:
- Full fit -> diagnose -> simulate -> aggregate workflow
- Exposure-weighted fitting
- Multi-peril conditional pricing
- Portfolio SCR calculation
"""

from __future__ import annotations

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

from insurance_copula import PerilVine, AggregateResult, PerilVineDiagnostics, HAS_PYVINECOPULIB

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


class TestFullWorkflow:
    @REQUIRES_PV
    def test_fit_diagnose_simulate(self, synthetic_losses, marginals_4perils):
        """Standard pricing workflow: fit -> diagnose -> simulate."""
        vine = PerilVine(perils=PERILS, trunc_lvl=2)
        vine.fit(synthetic_losses)

        # Diagnostics
        diag = vine.diagnostics()
        assert isinstance(diag, PerilVineDiagnostics)
        assert diag.aic < 0 or diag.aic > -1e8  # finite

        # Copula-space simulation
        u_samples = vine.simulate(1000, seed=42)
        assert u_samples.shape == (1000, 4)

        # Loss-space simulation
        loss_samples = vine.simulate(1000, marginals=marginals_4perils, seed=42)
        assert loss_samples.shape == (1000, 4)
        assert (loss_samples.values >= 0).all()

    @REQUIRES_PV
    def test_exposure_weighted_fit(self, synthetic_losses, exposure_series, marginals_4perils):
        """Exposure-weighted fitting should produce a valid model."""
        vine = PerilVine(perils=PERILS, trunc_lvl=2)
        vine.fit(synthetic_losses, exposure=exposure_series)

        samples = vine.simulate(500, marginals=marginals_4perils, seed=0)
        assert samples.shape == (500, 4)
        assert (samples.values >= 0).all()

    @REQUIRES_PV
    def test_portfolio_scr_workflow(self, synthetic_losses, marginals_4perils):
        """Compute Solvency II SCR-level PML for a portfolio."""
        vine = PerilVine(perils=PERILS, trunc_lvl=2)
        vine.fit(synthetic_losses)

        result = vine.aggregate_loss(
            n_sim=500,
            marginals=marginals_4perils,
            n_policies=500,
            seed=42,
        )
        assert isinstance(result, AggregateResult)
        assert result.pml_1_in_200 > 0
        assert result.pml_1_in_200 > result.expected_loss

    @REQUIRES_PV
    def test_conditional_pricing_workflow(self, fitted_vine, marginals_4perils):
        """Conditional expectation for flood given high storm loss."""
        storm_75th = gamma(a=1.2, scale=800).ppf(0.75)
        conditional_flood = fitted_vine.conditional_expectation(
            target_peril="flood",
            condition_peril="storm",
            threshold=storm_75th,
            n_samples=20_000,
            marginals=marginals_4perils,
        )
        unconditional_flood = gamma(a=0.8, scale=5000).mean()

        # Both should be positive
        assert conditional_flood > 0
        assert unconditional_flood > 0

    @REQUIRES_PV
    def test_serialise_and_use_in_production(self, fitted_vine, marginals_4perils):
        """Simulate saving a model and loading it in production."""
        json_str = fitted_vine.to_json()
        assert isinstance(json_str, str)

        restored = PerilVine.from_json(json_str)
        assert restored.is_fitted

        # Can simulate from restored model
        samples = restored.simulate(200, marginals=marginals_4perils, seed=7)
        assert samples.shape == (200, 4)

    @REQUIRES_PV
    def test_method_chaining(self, synthetic_losses):
        """fit() returns self — enabling method chaining."""
        vine = PerilVine(perils=PERILS, trunc_lvl=2)
        is_fitted = vine.fit(synthetic_losses).is_fitted
        assert is_fitted

    @REQUIRES_PV
    def test_six_peril_workflow(
        self, synthetic_losses_6perils, marginals_6perils
    ):
        """Full workflow with 6 perils."""
        vine = PerilVine(
            perils=PERILS_6,
            family_set="parametric",
            trunc_lvl=3,
            n_threads=1,
        )
        vine.fit(synthetic_losses_6perils)
        assert vine.is_fitted
        assert vine.n_obs == len(synthetic_losses_6perils)

        samples = vine.simulate(200, seed=0)
        assert samples.shape == (200, 6)

    @REQUIRES_PV
    def test_archimedean_family_set(self, synthetic_losses):
        """Archimedean family set should produce a valid fitted model."""
        vine = PerilVine(
            perils=PERILS,
            family_set="archimedean",
            trunc_lvl=2,
        )
        vine.fit(synthetic_losses)
        assert vine.is_fitted

    @REQUIRES_PV
    def test_aic_selection_criterion(self, synthetic_losses):
        """AIC criterion should produce a valid fitted model."""
        vine = PerilVine(
            perils=PERILS,
            selection_criterion="aic",
            trunc_lvl=2,
        )
        vine.fit(synthetic_losses)
        assert vine.is_fitted


class TestEdgeCases:
    def test_module_imports(self):
        """All public exports should be importable."""
        from insurance_copula import PerilVine, AggregateResult, PerilVineDiagnostics
        assert PerilVine is not None
        assert AggregateResult is not None
        assert PerilVineDiagnostics is not None

    def test_has_pyvinecopulib_is_bool(self):
        assert isinstance(HAS_PYVINECOPULIB, bool)

    def test_peril_vine_repr_is_string(self):
        vine = PerilVine(perils=PERILS)
        assert isinstance(repr(vine), str)

    @REQUIRES_PV
    def test_two_peril_full_vine(self, synthetic_losses_2perils):
        """2-peril vine with trunc_lvl=None should work."""
        vine = PerilVine(
            perils=["flood", "storm"],
            trunc_lvl=None,
        )
        vine.fit(synthetic_losses_2perils)
        assert vine.is_fitted

    @REQUIRES_PV
    def test_refit_same_vine(self, synthetic_losses):
        """Refitting a vine on new data should work."""
        vine = PerilVine(perils=PERILS, trunc_lvl=2)
        vine.fit(synthetic_losses)
        assert vine.n_obs == len(synthetic_losses)

        # Fit on a subset
        subset = synthetic_losses.head(200)
        vine.fit(subset)
        assert vine.n_obs == 200
        assert vine.is_fitted
