"""Tests for noise_generation.py: generators and verification."""

import numpy as np
import pytest
from noise_generation import (
    box_muller, ziggurat,
    colored_noise_ar, colored_noise_fd, voss_mccartney,
    perlin_1d, perlin_2d, simplex_2d,
    verify_noise,
)


class TestBoxMuller:
    def test_shape(self):
        x = box_muller(1000)
        assert x.shape == (1000,)

    def test_approx_standard_normal(self):
        x = box_muller(100000, seed=42)
        assert abs(np.mean(x)) < 0.02
        assert 0.98 < np.std(x) < 1.02

    def test_reproducible(self):
        a = box_muller(100, seed=42)
        b = box_muller(100, seed=42)
        assert np.array_equal(a, b)

    def test_odd_n(self):
        x = box_muller(101, seed=1)
        assert x.shape == (101,)


class TestZiggurat:
    def test_shape(self):
        x = ziggurat(500)
        assert x.shape == (500,)

    def test_approx_standard_normal(self):
        x = ziggurat(100000, seed=42)
        assert abs(np.mean(x)) < 0.05
        assert 0.95 < np.std(x) < 1.05

    def test_reproducible(self):
        a = ziggurat(100, seed=42)
        b = ziggurat(100, seed=42)
        assert np.array_equal(a, b)

    def test_no_extreme_values(self):
        """Gaussian samples should not routinely exceed ~6 sigma."""
        x = ziggurat(10000, seed=1)
        assert np.max(np.abs(x)) < 8.0


class TestColoredNoiseAR:
    def test_shape(self):
        x = colored_noise_ar(1000, alpha=1.0)
        assert x.shape == (1000,)

    def test_white_noise_flat(self):
        x = colored_noise_ar(20000, alpha=0.0, p=10, seed=42)
        from noise_generation import verify_noise
        info = verify_noise(x)
        assert abs(info['psd_slope']) < 0.3  # alpha=0 → flat

    def test_brown_noise_steep(self):
        x = colored_noise_ar(20000, alpha=2.0, p=20, seed=42)
        from noise_generation import verify_noise
        info = verify_noise(x)
        assert info['psd_slope'] < -1.0  # alpha=2 → steep negative slope

    def test_pink_noise_intermediate(self):
        x = colored_noise_ar(20000, alpha=1.0, p=20, seed=42)
        from noise_generation import verify_noise
        info = verify_noise(x)
        # alpha=1 should give roughly -1 slope
        assert -2.0 < info['psd_slope'] < 0.0


class TestColoredNoiseFD:
    def test_shape(self):
        x = colored_noise_fd(1024, alpha=1.0)
        assert x.shape == (1024,)

    def test_white_noise_flat(self):
        x = colored_noise_fd(8192, alpha=0.0, seed=42)
        from noise_generation import verify_noise
        info = verify_noise(x)
        assert abs(info['psd_slope']) < 0.5


class TestVossMcCartney:
    def test_shape(self):
        x = voss_mccartney(5000, octaves=8)
        assert x.shape == (5000,)

    def test_approx_pink(self):
        x = voss_mccartney(50000, octaves=10, seed=42)
        from noise_generation import verify_noise
        info = verify_noise(x)
        # Should be roughly 1/f (slope ~ -1)
        assert -2.0 < info['psd_slope'] < 0.0

    def test_reproducible(self):
        a = voss_mccartney(1000, octaves=6, seed=7)
        b = voss_mccartney(1000, octaves=6, seed=7)
        assert np.array_equal(a, b)


class TestPerlin1D:
    def test_shape(self):
        x = np.linspace(0, 10, 100)
        y = perlin_1d(x, seed=0)
        assert y.shape == x.shape

    def test_range(self):
        x = np.linspace(0, 100, 5000)
        y = perlin_1d(x, seed=1, octaves=1)
        # Single-octave Perlin should be roughly bounded.
        assert np.max(np.abs(y)) < 2.0

    def test_period_zero_at_integers_single_octave(self):
        """Single-octave Perlin is exactly zero at integer lattice points."""
        x = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
        y = perlin_1d(x, seed=0, octaves=1)
        assert np.allclose(y, 0.0, atol=1e-10)

    def test_octaves_increase_roughness(self):
        x = np.linspace(0, 10, 1000)
        y1 = perlin_1d(x, seed=0, octaves=1)
        y4 = perlin_1d(x, seed=0, octaves=4, persistence=0.5)
        # More octaves → more zero-crossings (higher frequency content).
        zc1 = np.sum(np.diff(np.sign(y1)) != 0)
        zc4 = np.sum(np.diff(np.sign(y4)) != 0)
        assert zc4 > zc1

    def test_reproducible(self):
        x = np.linspace(0, 5, 50)
        a = perlin_1d(x, seed=42)
        b = perlin_1d(x, seed=42)
        assert np.array_equal(a, b)

    def test_different_seeds_different(self):
        x = np.linspace(0, 5, 50)
        a = perlin_1d(x, seed=0)
        b = perlin_1d(x, seed=1)
        assert not np.array_equal(a, b)


class TestPerlin2D:
    def test_shape(self):
        x = np.linspace(0, 5, 50)
        y = np.linspace(0, 5, 60)
        X, Y = np.meshgrid(x, y, indexing='ij')
        Z = perlin_2d(X, Y, seed=0)
        assert Z.shape == (50, 60)

    def test_range(self):
        X, Y = np.meshgrid(np.linspace(0, 20, 100), np.linspace(0, 20, 100), indexing='ij')
        Z = perlin_2d(X, Y, seed=1, octaves=1)
        assert np.max(np.abs(Z)) < 2.0

    def test_zero_at_integer_lattice_points(self):
        """Single-octave 2D Perlin is zero at integer lattice intersections."""
        X, Y = np.meshgrid(np.array([0, 1, 2]), np.array([0, 1, 2]), indexing='ij')
        Z = perlin_2d(X, Y, seed=0, octaves=1)
        assert np.allclose(Z, 0.0, atol=1e-10)


class TestSimplex2D:
    def test_shape(self):
        X, Y = np.meshgrid(np.linspace(0, 5, 30), np.linspace(0, 5, 40), indexing='ij')
        Z = simplex_2d(X, Y, seed=0)
        assert Z.shape == (30, 40)

    def test_reproducible(self):
        X, Y = np.meshgrid(np.linspace(0, 3, 10), np.linspace(0, 3, 10), indexing='ij')
        a = simplex_2d(X, Y, seed=42)
        b = simplex_2d(X, Y, seed=42)
        assert np.array_equal(a, b)


class TestVerifyNoise:
    def test_returns_dict(self):
        x = box_muller(2000, seed=1)
        info = verify_noise(x, label='test')
        assert isinstance(info, dict)
        assert 'mean' in info
