"""Tests for PerilVine JSON serialisation round-trip."""

from __future__ import annotations

import json

import numpy as np
import pytest

from insurance_copula import PerilVine, HAS_PYVINECOPULIB
from insurance_copula._serialisation import (
    serialise_vine,
    deserialise_vine,
)

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


class TestSerialisationRoundTrip:
    @REQUIRES_PV
    def test_to_json_returns_string(self, fitted_vine):
        s = fitted_vine.to_json()
        assert isinstance(s, str)
        assert len(s) > 0

    @REQUIRES_PV
    def test_json_is_valid(self, fitted_vine):
        s = fitted_vine.to_json()
        envelope = json.loads(s)
        assert isinstance(envelope, dict)

    @REQUIRES_PV
    def test_envelope_contains_perils(self, fitted_vine):
        s = fitted_vine.to_json()
        envelope = json.loads(s)
        assert "perils" in envelope
        assert envelope["perils"] == PERILS

    @REQUIRES_PV
    def test_envelope_contains_hyperparams(self, fitted_vine):
        s = fitted_vine.to_json()
        envelope = json.loads(s)
        assert "family_set" in envelope
        assert "selection_criterion" in envelope
        assert "tree_criterion" in envelope
        assert "trunc_lvl" in envelope
        assert "vine_type" in envelope

    @REQUIRES_PV
    def test_envelope_contains_version(self, fitted_vine):
        s = fitted_vine.to_json()
        envelope = json.loads(s)
        assert "version" in envelope

    @REQUIRES_PV
    def test_envelope_contains_vine_json(self, fitted_vine):
        s = fitted_vine.to_json()
        envelope = json.loads(s)
        assert "vine_json" in envelope
        # vine_json should itself be a JSON string
        inner = json.loads(envelope["vine_json"])
        assert isinstance(inner, dict)

    @REQUIRES_PV
    def test_round_trip_perils(self, fitted_vine):
        s = fitted_vine.to_json()
        restored = PerilVine.from_json(s)
        assert restored.perils == fitted_vine.perils

    @REQUIRES_PV
    def test_round_trip_fitted(self, fitted_vine):
        s = fitted_vine.to_json()
        restored = PerilVine.from_json(s)
        assert restored.is_fitted

    @REQUIRES_PV
    def test_round_trip_hyperparams(self, fitted_vine):
        s = fitted_vine.to_json()
        restored = PerilVine.from_json(s)
        assert restored.family_set == fitted_vine.family_set
        assert restored.selection_criterion == fitted_vine.selection_criterion
        assert restored.tree_criterion == fitted_vine.tree_criterion
        assert restored.vine_type == fitted_vine.vine_type

    @REQUIRES_PV
    def test_round_trip_simulate(self, fitted_vine):
        """Restored vine produces same samples with same seed."""
        s = fitted_vine.to_json()
        restored = PerilVine.from_json(s)

        samples_orig = fitted_vine.simulate(100, seed=99)
        samples_restored = restored.simulate(100, seed=99)

        np.testing.assert_allclose(
            samples_orig.values, samples_restored.values, rtol=1e-6
        )

    @REQUIRES_PV
    def test_round_trip_column_names(self, fitted_vine):
        s = fitted_vine.to_json()
        restored = PerilVine.from_json(s)
        samples = restored.simulate(50, seed=0)
        assert list(samples.columns) == PERILS

    @REQUIRES_PV
    def test_unfitted_raises(self):
        vine = PerilVine(perils=PERILS)
        with pytest.raises(RuntimeError, match="not been fitted"):
            vine.to_json()

    @REQUIRES_PV
    def test_from_json_invalid_raises(self):
        with pytest.raises(ValueError, match="Invalid JSON"):
            PerilVine.from_json("not json at all")

    @REQUIRES_PV
    def test_from_json_missing_keys_raises(self):
        # Missing 'vine_json' key
        incomplete = json.dumps({
            "perils": PERILS,
            "family_set": "parametric",
        })
        with pytest.raises(ValueError, match="missing keys"):
            PerilVine.from_json(incomplete)

    def test_from_json_without_pyvinecopulib_raises(self):
        if HAS_PYVINECOPULIB:
            pytest.skip("pyvinecopulib is installed")
        with pytest.raises(ImportError, match="pyvinecopulib"):
            PerilVine.from_json("{}")


class TestDeserialiseVineFunction:
    @REQUIRES_PV
    def test_deserialise_returns_tuple(self, fitted_vine):
        s = fitted_vine.to_json()
        result = deserialise_vine(s)
        assert isinstance(result, tuple)
        assert len(result) == 2

    @REQUIRES_PV
    def test_metadata_dict_structure(self, fitted_vine):
        s = fitted_vine.to_json()
        _, metadata = deserialise_vine(s)
        assert "perils" in metadata
        assert "family_set" in metadata
        assert "selection_criterion" in metadata
        assert "tree_criterion" in metadata
        assert "vine_type" in metadata

    @REQUIRES_PV
    def test_trunc_lvl_preserved(self, fitted_vine):
        s = fitted_vine.to_json()
        _, metadata = deserialise_vine(s)
        assert metadata["trunc_lvl"] == fitted_vine.trunc_lvl

    @REQUIRES_PV
    def test_trunc_lvl_none_preserved(self, synthetic_losses):
        vine = PerilVine(perils=PERILS, trunc_lvl=None)
        vine.fit(synthetic_losses)
        s = vine.to_json()
        _, metadata = deserialise_vine(s)
        assert metadata["trunc_lvl"] is None
