"""Tests for Theorem V: The Kaleidoscope Hyperboloid."""
import math
import pytest

from watkins_nn.coupling import (
    hyperboloid_point,
    hyperboloid_all,
    hyperboloid_pythagorean,
    lattice_density_check,
    HYPERBOLOID_POINTS,
)


PHI = (1 + math.sqrt(5)) / 2


class TestHyperboloidPoints:
    """Test the five canonical points on cosh^2 - sinh^2 = 1."""

    def test_all_points_on_hyperboloid(self):
        """Every canonical point satisfies cosh^2(t) - sinh^2(t) = 1."""
        for name in HYPERBOLOID_POINTS:
            pt = hyperboloid_point(name)
            assert pt["verification"] == pytest.approx(1.0, abs=1e-12), (
                f"{name}: cosh^2 - sinh^2 = {pt['verification']}"
            )

    def test_fibonacci_point(self):
        """t = ln(phi): cosh = sqrt(5)/2, sinh = 1/2."""
        pt = hyperboloid_point("fibonacci")
        assert pt["t"] == pytest.approx(math.log(PHI), abs=1e-14)
        assert pt["cosh_t"] == pytest.approx(math.sqrt(5) / 2, abs=1e-12)
        assert pt["sinh_t"] == pytest.approx(0.5, abs=1e-12)

    def test_lucas_point(self):
        """t = 2*ln(phi): cosh = 3/2, sinh = sqrt(5)/2."""
        pt = hyperboloid_point("lucas")
        assert pt["t"] == pytest.approx(2 * math.log(PHI), abs=1e-14)
        assert pt["cosh_t"] == pytest.approx(1.5, abs=1e-12)
        assert pt["sinh_t"] == pytest.approx(math.sqrt(5) / 2, abs=1e-12)

    def test_mersenne_point(self):
        """t = ln(2): cosh = 5/4, sinh = 3/4."""
        pt = hyperboloid_point("mersenne")
        assert pt["t"] == pytest.approx(math.log(2), abs=1e-14)
        assert pt["cosh_t"] == pytest.approx(1.25, abs=1e-12)
        assert pt["sinh_t"] == pytest.approx(0.75, abs=1e-12)

    def test_coupling_is_sum(self):
        """Coupling point t = ln(2*phi) = ln(phi) + ln(2)."""
        pt = hyperboloid_point("coupling")
        assert pt["a"] == 1 and pt["b"] == 1
        assert pt["t"] == pytest.approx(math.log(2 * PHI), abs=1e-14)

    def test_spectral_is_lucas_plus_mersenne(self):
        """Spectral point t = ln(2*phi^2) = 2*ln(phi) + ln(2)."""
        pt = hyperboloid_point("spectral")
        assert pt["a"] == 2 and pt["b"] == 1
        assert pt["t"] == pytest.approx(math.log(2 * PHI**2), abs=1e-14)

    def test_arbitrary_lattice_point(self):
        """Arbitrary (a, b) tuple works and lies on hyperboloid."""
        pt = hyperboloid_point((3, 2))
        assert pt["verification"] == pytest.approx(1.0, abs=1e-12)

    def test_hyperboloid_all_returns_five(self):
        """hyperboloid_all returns exactly 5 points."""
        pts = hyperboloid_all()
        assert len(pts) == 5
        assert set(pts.keys()) == set(HYPERBOLOID_POINTS.keys())


class TestPythagoreanIdentity:
    """Test (5*sqrt(5)+3)^2 - (3*sqrt(5)+5)^2 = 64."""

    def test_difference_is_64(self):
        result = hyperboloid_pythagorean()
        assert result["difference"] == pytest.approx(64.0, abs=1e-10)

    def test_is_64_flag(self):
        result = hyperboloid_pythagorean()
        assert result["is_64"] is True

    def test_lhs_value(self):
        """(5*sqrt(5) + 3) = 8*cosh(ln(2*phi))."""
        result = hyperboloid_pythagorean()
        expected = 8 * math.cosh(math.log(2 * PHI))
        assert result["lhs"] == pytest.approx(expected, abs=1e-10)


class TestLatticeDensity:
    """Test Kronecker density of the lattice."""

    def test_returns_correct_count(self):
        result = lattice_density_check(20)
        assert len(result) == 20

    def test_all_fractional_parts_in_unit_interval(self):
        for x in lattice_density_check(50):
            assert 0 <= x < 1

    def test_equidistribution(self):
        """Fractional parts should be spread across [0,1), not clustered."""
        parts = sorted(lattice_density_check(100))
        # Check max gap is < 0.1 (random would give ~0.01 mean gap)
        gaps = [parts[i+1] - parts[i] for i in range(len(parts)-1)]
        assert max(gaps) < 0.1, f"Max gap {max(gaps):.4f} too large"
