"""
Tests for Monte Carlo envelope functions.
"""
import numpy as np
import pytest

from pyspat.core import RectangleWindow, PPP
from pyspat.simulate import rpoispp, rThomas, rSSI
from pyspat.envelopes import (
    envelope, Envelope, envelope_K, envelope_L, envelope_pcf,
    envelope_G, envelope_F, envelope_J, dclf_test
)

HAS_SHAPELY = True
try:
    import shapely.geometry as sgeom
    from pyspat.core import PolyWindow
except ImportError:
    HAS_SHAPELY = False
    sgeom = None
    PolyWindow = None


@pytest.fixture
def rect_window():
    return RectangleWindow(0.0, 0.0, 100.0, 100.0)


@pytest.fixture
def csr_ppp(rect_window):
    """CSR pattern for envelope tests."""
    rng = np.random.default_rng(42)
    return rpoispp(0.02, rect_window, rng)


@pytest.fixture
def clustered_ppp(rect_window):
    """Clustered pattern for detecting departures from CSR."""
    rng = np.random.default_rng(42)
    return rThomas(0.001, 3.0, 20.0, rect_window, rng)


@pytest.fixture
def regular_ppp(rect_window):
    """Regular (inhibited) pattern for detecting departures from CSR."""
    rng = np.random.default_rng(42)
    return rSSI(200, 5.0, rect_window, rng)


@pytest.fixture
def r_grid():
    return np.linspace(2.0, 25.0, 20)


@pytest.fixture
def r_edges():
    return np.linspace(2.0, 25.0, 25)


class TestEnvelopeBasic:

    def test_returns_envelope_object(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=19, rng=np.random.default_rng(0))
        assert isinstance(env, Envelope)

    def test_envelope_attributes(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=19, stat_name="K",
                       rng=np.random.default_rng(0))

        assert env.r.shape == r_grid.shape
        assert env.observed.shape == r_grid.shape
        assert env.mean.shape == r_grid.shape
        assert env.lo.shape == r_grid.shape
        assert env.hi.shape == r_grid.shape
        assert env.nsim == 19
        assert env.rank == 1
        assert env.stat_name == "K"

    def test_lo_le_mean_le_hi(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=39, rng=np.random.default_rng(0))

        assert np.all(env.lo <= env.mean + 1e-10)
        assert np.all(env.mean <= env.hi + 1e-10)

    def test_higher_rank_gives_narrower_envelope(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        rng = np.random.default_rng(0)
        env1 = envelope(csr_ppp, stat_func, r_grid, nsim=39, rank=1, rng=rng)
        rng = np.random.default_rng(0)
        env5 = envelope(csr_ppp, stat_func, r_grid, nsim=39, rank=5, rng=rng)

        # Rank 5 envelope should be narrower (or equal)
        width1 = env1.hi - env1.lo
        width5 = env5.hi - env5.lo
        assert np.all(width5 <= width1 + 1e-10)

    def test_store_simulations(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=19,
                       store_simulations=True, rng=np.random.default_rng(0))

        assert env.simulations is not None
        assert env.simulations.shape == (19, len(r_grid))

    def test_invalid_nsim_raises(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, showplot=False)

        with pytest.raises(ValueError):
            envelope(csr_ppp, stat_func, r_grid, nsim=0)

    def test_invalid_rank_raises(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, showplot=False)

        with pytest.raises(ValueError):
            envelope(csr_ppp, stat_func, r_grid, nsim=19, rank=0)
        with pytest.raises(ValueError):
            envelope(csr_ppp, stat_func, r_grid, nsim=19, rank=15)  # rank > nsim/2

    def test_reproducible_with_seed(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env1 = envelope(csr_ppp, stat_func, r_grid, nsim=19, rng=np.random.default_rng(99))
        env2 = envelope(csr_ppp, stat_func, r_grid, nsim=19, rng=np.random.default_rng(99))

        assert np.array_equal(env1.lo, env2.lo)
        assert np.array_equal(env1.hi, env2.hi)


class TestGlobalEnvelopeTest:

    def test_global_pvalue_computed(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=39, global_test=True,
                       rng=np.random.default_rng(0))

        assert env.global_pvalue is not None
        assert 0.0 <= env.global_pvalue <= 1.0
        assert env.global_deviation is not None

    def test_csr_pattern_nonsignificant(self, csr_ppp, r_grid):
        """CSR pattern should usually not reject CSR null."""
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(csr_ppp, stat_func, r_grid, nsim=99, global_test=True,
                       rng=np.random.default_rng(123))

        # Should usually not reject at 5% level (may fail occasionally)
        assert env.global_pvalue > 0.01

    def test_clustered_pattern_significant(self, clustered_ppp, r_grid):
        """Clustered pattern should often reject CSR null."""
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(clustered_ppp, stat_func, r_grid, nsim=99, global_test=True,
                       alternative="greater", rng=np.random.default_rng(0))

        # Clustered => K > CSR K, should detect
        assert env.global_pvalue < 0.1

    def test_regular_pattern_significant(self, regular_ppp, r_grid):
        """Regular pattern should often reject CSR null."""
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        env = envelope(regular_ppp, stat_func, r_grid, nsim=99, global_test=True,
                       alternative="less", rng=np.random.default_rng(0))

        # Regular => K < CSR K at small r, should detect
        assert env.global_pvalue < 0.1


class TestEnvelopeK:

    def test_basic_operation(self, csr_ppp, r_grid):
        env = envelope_K(csr_ppp, r_grid, nsim=19, rng=np.random.default_rng(0))
        assert env.stat_name == "K"
        assert env.r.shape == r_grid.shape

    def test_csr_observed_within_envelope(self, csr_ppp, r_grid):
        """CSR pattern's K should usually stay within envelope."""
        env = envelope_K(csr_ppp, r_grid, nsim=39, rank=2, rng=np.random.default_rng(42))

        within = (env.observed >= env.lo) & (env.observed <= env.hi)
        # Most points should be within envelope (rank=2 with nsim=39 gives ~90% coverage,
        # but observed pattern is random so allow some margin)
        assert within.mean() > 0.5

    def test_translation_vs_border_correction(self, csr_ppp, r_grid):
        env_trans = envelope_K(csr_ppp, r_grid, nsim=19, correction="translation",
                               rng=np.random.default_rng(0))
        env_border = envelope_K(csr_ppp, r_grid, nsim=19, correction="border",
                                rng=np.random.default_rng(0))

        # Both should produce valid results
        assert np.all(np.isfinite(env_trans.observed) | np.isnan(env_trans.observed))
        assert np.all(np.isfinite(env_border.observed) | np.isnan(env_border.observed))


class TestEnvelopeL:

    def test_basic_operation(self, csr_ppp, r_grid):
        env = envelope_L(csr_ppp, r_grid, nsim=19, rng=np.random.default_rng(0))
        assert env.stat_name == "L"

    def test_csr_L_near_r(self, csr_ppp, r_grid):
        """For CSR, L(r) ≈ r."""
        env = envelope_L(csr_ppp, r_grid, nsim=39, rng=np.random.default_rng(0))

        # Mean should be close to r (within some tolerance)
        rel_diff = np.abs(env.mean - r_grid) / r_grid
        assert np.median(rel_diff) < 0.15


class TestEnvelopePcf:

    def test_basic_operation(self, csr_ppp, r_edges):
        env = envelope_pcf(csr_ppp, r_edges, nsim=19, rng=np.random.default_rng(0))
        assert env.stat_name == "g"
        # r should be midpoints
        expected_len = len(r_edges) - 1
        assert len(env.r) == expected_len

    def test_csr_pcf_near_one(self, csr_ppp, r_edges):
        """For CSR, g(r) ≈ 1."""
        env = envelope_pcf(csr_ppp, r_edges, nsim=39, rng=np.random.default_rng(0))

        # Mean should be close to 1
        assert np.abs(np.nanmean(env.mean) - 1.0) < 0.2


class TestEnvelopeG:

    def test_basic_operation(self, csr_ppp, r_grid):
        env = envelope_G(csr_ppp, r_grid, nsim=19, rng=np.random.default_rng(0))
        assert env.stat_name == "G"

    def test_G_bounded_0_1(self, csr_ppp, r_grid):
        env = envelope_G(csr_ppp, r_grid, nsim=19, rng=np.random.default_rng(0))

        finite = np.isfinite(env.observed)
        assert np.all((env.observed[finite] >= 0) & (env.observed[finite] <= 1))


class TestEnvelopeF:

    def test_basic_operation(self, csr_ppp, r_grid):
        env = envelope_F(csr_ppp, r_grid, nsim=19, nsamp=500,
                         rng=np.random.default_rng(0))
        assert env.stat_name == "F"

    def test_F_bounded_0_1(self, csr_ppp, r_grid):
        env = envelope_F(csr_ppp, r_grid, nsim=19, nsamp=500,
                         rng=np.random.default_rng(0))

        finite = np.isfinite(env.observed)
        assert np.all((env.observed[finite] >= 0) & (env.observed[finite] <= 1))


class TestEnvelopeJ:

    def test_basic_operation(self, csr_ppp, r_grid):
        env = envelope_J(csr_ppp, r_grid, nsim=19, nsamp_F=500,
                         rng=np.random.default_rng(0))
        assert env.stat_name == "J"

    def test_csr_J_near_one(self, csr_ppp):
        """For CSR, J(r) ≈ 1."""
        r = np.linspace(1.0, 8.0, 15)
        env = envelope_J(csr_ppp, r, nsim=39, nsamp_F=800,
                         rng=np.random.default_rng(0))

        finite = np.isfinite(env.mean)
        if finite.any():
            assert np.abs(np.nanmean(env.mean[finite]) - 1.0) < 0.3

class TestDCLF:

    def test_returns_pvalue_and_statistic(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        pval, obs_u, sim_u = dclf_test(csr_ppp, stat_func, r_grid, nsim=39,
                                        rng=np.random.default_rng(0))

        assert 0.0 <= pval <= 1.0
        assert obs_u >= 0
        assert len(sim_u) == 39

    def test_csr_nonsignificant(self, csr_ppp, r_grid):
        from pyspat.stats import Kest

        def stat_func(p, r):
            return Kest(p, r, correction="translation", showplot=False)

        pval, _, _ = dclf_test(csr_ppp, stat_func, r_grid, nsim=99,
                               rng=np.random.default_rng(123))

        # CSR pattern should usually not reject
        assert pval > 0.01



class TestEnvelopePlot:

    def test_plot_method_exists(self, csr_ppp, r_grid):
        env = envelope_K(csr_ppp, r_grid, nsim=9, rng=np.random.default_rng(0))
        assert hasattr(env, 'plot')

    def test_plot_smoke(self, csr_ppp, r_grid):
        """Just check plot doesn't crash."""
        pytest.importorskip("matplotlib")

        env = envelope_K(csr_ppp, r_grid, nsim=9, global_test=True,
                         rng=np.random.default_rng(0))
        # show=False to not block
        env.plot(show=False)


class TestEnvelopeEdgeCases:

    def test_small_pattern(self, rect_window):
        """Handle patterns with few points."""
        xy = np.array([[10.0, 10.0], [20.0, 20.0], [30.0, 30.0]])
        p = PPP(xy, rect_window)
        r = np.linspace(5.0, 20.0, 10)

        env = envelope_K(p, r, nsim=19, rng=np.random.default_rng(0))
        assert env.observed.shape == r.shape

    def test_empty_pattern(self, rect_window):
        """Handle empty pattern (intensity estimation edge case)."""
        p = PPP(np.empty((0, 2)), rect_window)
        r = np.linspace(5.0, 20.0, 10)

        # Should not crash
        env = envelope_K(p, r, nsim=9, rng=np.random.default_rng(0))
        assert env.observed.shape == r.shape

    def test_single_point_pattern(self, rect_window):
        """Handle single point pattern."""
        xy = np.array([[50.0, 50.0]])
        p = PPP(xy, rect_window)
        r = np.linspace(5.0, 20.0, 10)

        env = envelope_K(p, r, nsim=9, rng=np.random.default_rng(0))
        # K with 1 point should be 0
        assert np.allclose(env.observed, 0.0)


class TestEnvelopeIntegration:

    def test_full_workflow_clustered(self, rect_window):
        """Full workflow: simulate clustered, test with envelope."""
        rng = np.random.default_rng(12345)
        p = rThomas(0.001, 4.0, 15.0, rect_window, rng)
        r = np.linspace(2.0, 20.0, 15)

        env = envelope_K(p, r, nsim=99, global_test=True,
                         alternative="greater", rng=rng)

        # Should detect clustering (K > CSR K)
        # Observed should be above mean at small r
        small_r = r < 10
        assert np.mean(env.observed[small_r] - env.mean[small_r]) > 0

    def test_full_workflow_regular(self, rect_window):
        """Full workflow: simulate regular, test with envelope."""
        rng = np.random.default_rng(54321)
        p = rSSI(150, 5.0, rect_window, rng)
        r = np.linspace(2.0, 20.0, 15)

        env = envelope_K(p, r, nsim=99, global_test=True,
                         alternative="less", rng=rng)

        # Should detect regularity at small r (K < CSR K)
        very_small_r = r < 5
        assert np.mean(env.observed[very_small_r]) < np.mean(env.mean[very_small_r])

    @pytest.mark.skipif(not HAS_SHAPELY, reason="shapely not available")
    def test_with_poly_window(self):
        """Envelope works with polygon windows."""
        poly = sgeom.Polygon([(0, 0), (100, 0), (100, 100), (0, 100)])
        w = PolyWindow(poly)
        rng = np.random.default_rng(0)
        p = rpoispp(0.02, w, rng)
        r = np.linspace(2.0, 20.0, 15)

        env = envelope_K(p, r, nsim=19, rng=rng)
        assert env.observed.shape == r.shape