"""
Tests for Perfect Cuboid Search Module
=======================================

Framework coverage:
  - AX52: Multiplicative gate — all 4 equations checked
  - AX22/T6: Unreachable supremum — no perfect cuboid expected
  - AX5: Integration prerequisite — simultaneous satisfaction
  - AX34/M2: Constitutive wound measurement (near-miss gap)
  - KV₃: Observer non-interference — tests don't create order
  - KV₇: Independence — each test function independent
"""

import unittest
import math

from perfect_cuboid.search import (
    is_perfect_square,
    integer_sqrt,
    check_cuboid,
    CuboidResult,
    modular_sieve,
    pythagorean_triples,
    euler_brick_search,
    find_near_misses,
)


class TestHelpers(unittest.TestCase):
    """Test helper functions."""

    def test_is_perfect_square_true(self):
        for n in [0, 1, 4, 9, 16, 25, 144, 10000]:
            self.assertTrue(is_perfect_square(n), f"{n} should be perfect square")

    def test_is_perfect_square_false(self):
        for n in [2, 3, 5, 7, 8, 10, 15, 99, 101]:
            self.assertFalse(is_perfect_square(n), f"{n} should not be perfect square")

    def test_is_perfect_square_negative(self):
        self.assertFalse(is_perfect_square(-1))
        self.assertFalse(is_perfect_square(-100))

    def test_integer_sqrt(self):
        self.assertEqual(integer_sqrt(144), 12)
        self.assertEqual(integer_sqrt(0), 0)
        self.assertIsNone(integer_sqrt(2))
        self.assertIsNone(integer_sqrt(-1))

    def test_large_perfect_square(self):
        """Test with large numbers to ensure no float precision issues."""
        n = 123456789 ** 2
        self.assertTrue(is_perfect_square(n))
        self.assertEqual(integer_sqrt(n), 123456789)


class TestCheckCuboid(unittest.TestCase):
    """Test the core cuboid checker — AX52 multiplicative gate."""

    def test_pythagorean_triple_3_4_5(self):
        """Basic Pythagorean triple: 3² + 4² = 5²."""
        r = check_cuboid(3, 4, 5)
        self.assertEqual(r.d_ab, 5)  # 3² + 4² = 25 = 5²
        self.assertGreaterEqual(r.equations_satisfied, 1)

    def test_known_euler_brick_44_117_240(self):
        """The smallest known Euler brick: (44, 117, 240).

        Face diagonals: 125, 244, 267
        44² + 117² = 1936 + 13689 = 15625 = 125²  ✓
        44² + 240² = 1936 + 57600 = 59536 = 244²   ✓
        117² + 240² = 13689 + 57600 = 71289 = 267² ✓
        Space diagonal: 44² + 117² + 240² = 73225 = ?
        √73225 ≈ 270.6... NOT integer ✗
        """
        r = check_cuboid(44, 117, 240)
        self.assertTrue(r.is_euler_brick)
        self.assertFalse(r.is_perfect_cuboid)
        self.assertEqual(r.d_ab, 125)
        self.assertEqual(r.d_ac, 244)
        self.assertEqual(r.d_bc, 267)
        self.assertIsNone(r.d_space)
        self.assertEqual(r.equations_satisfied, 3)

    def test_not_euler_brick(self):
        """Random triple that is not an Euler brick."""
        r = check_cuboid(1, 2, 3)
        self.assertFalse(r.is_euler_brick)
        self.assertFalse(r.is_perfect_cuboid)

    def test_ax52_multiplicative_gate(self):
        """AX52: Perfect cuboid requires ALL 4 equations.

        Even if 3 pass (Euler brick), one failure means
        the multiplicative gate collapses.
        """
        # (44, 117, 240) passes 3/4 equations
        r = check_cuboid(44, 117, 240)
        self.assertEqual(r.equations_satisfied, 3)
        # Gate collapses: NOT a perfect cuboid
        self.assertFalse(r.is_perfect_cuboid)

    def test_another_euler_brick_85_132_720(self):
        """Second known Euler brick: (85, 132, 720).

        85² + 132² = 7225 + 17424 = 24649 = 157²  ✓
        85² + 720² = 7225 + 518400 = 525625 = 725²  ✓
        132² + 720² = 17424 + 518400 = 535824 = 732² ✓
        """
        r = check_cuboid(85, 132, 720)
        self.assertTrue(r.is_euler_brick)
        self.assertFalse(r.is_perfect_cuboid)
        self.assertEqual(r.d_ab, 157)
        self.assertEqual(r.d_ac, 725)
        self.assertEqual(r.d_bc, 732)


class TestModularSieve(unittest.TestCase):
    """Test modular sieve — AX3 selection mechanism."""

    def test_euler_brick_passes_sieve(self):
        """Known Euler brick (44, 117, 240) should pass the sieve."""
        self.assertTrue(modular_sieve(44, 117, 240))

    def test_all_odd_rejected(self):
        """Three odd edges must be rejected (parity constraint)."""
        self.assertFalse(modular_sieve(3, 5, 7))

    def test_all_even_rejected(self):
        """Three even edges must be rejected (need exactly 1 odd)."""
        self.assertFalse(modular_sieve(2, 4, 6))

    def test_no_div_by_9_rejected(self):
        """Triple without any edge divisible by 9."""
        # 16, 3, 4 -> odd_count=1, div4=yes, div16=yes, div9=no
        self.assertFalse(modular_sieve(16, 3, 4))

    def test_no_div_by_16_rejected(self):
        """Triple without any edge divisible by 16."""
        # 4, 9, 3 -> odd_count=1, div4=yes, div16=no
        self.assertFalse(modular_sieve(4, 9, 3))

    def test_valid_sieve_candidate(self):
        """Triple that passes all sieve conditions."""
        # 44, 117, 240 -> 1 odd (117), div4=44, div16=240(240/16=15), div9=117(117/9=13)
        self.assertTrue(modular_sieve(44, 117, 240))


class TestPythagoreanTriples(unittest.TestCase):
    """Test the Pythagorean triple generator."""

    def test_small_triples(self):
        """Check that well-known small triples are generated."""
        triples = list(pythagorean_triples(100))
        # (3, 4, 5) is the smallest primitive triple
        self.assertIn((3, 4, 5), triples)
        # (5, 12, 13)
        self.assertIn((5, 12, 13), triples)
        # (8, 15, 17)
        self.assertIn((8, 15, 17), triples)

    def test_all_valid(self):
        """Every generated triple must satisfy a² + b² = c²."""
        for a, b, c in pythagorean_triples(200):
            self.assertEqual(a * a + b * b, c * c,
                             f"({a}, {b}, {c}) is not Pythagorean")

    def test_primitive(self):
        """All generated triples should be primitive (gcd = 1)."""
        for a, b, c in pythagorean_triples(200):
            self.assertEqual(math.gcd(math.gcd(a, b), c), 1,
                             f"({a}, {b}, {c}) is not primitive")


class TestEulerBrickSearch(unittest.TestCase):
    """Test Euler brick search — AX22/T6 unreachable supremum demonstration."""

    def test_finds_smallest_euler_brick(self):
        """Must find (44, 117, 240) — the smallest Euler brick."""
        results = euler_brick_search(250)
        edges = [(r.a, r.b, r.c) for r in results]
        self.assertIn((44, 117, 240), edges,
                      "Smallest Euler brick (44, 117, 240) not found")

    def test_no_perfect_cuboid_in_range(self):
        """AX22: No perfect cuboid should be found in small range.

        This is the computational confirmation of the
        unreachable supremum hypothesis.
        """
        results = euler_brick_search(250)
        for r in results:
            self.assertFalse(r.is_perfect_cuboid,
                             f"Unexpected perfect cuboid: ({r.a}, {r.b}, {r.c})")


class TestNearMisses(unittest.TestCase):
    """Test near-miss analysis — AX34/M2 wound measurement."""

    def test_near_misses_found(self):
        """Should find Euler bricks as near-misses."""
        misses = find_near_misses(250, max_results=5)
        self.assertGreater(len(misses), 0, "Should find at least one near-miss")

    def test_gap_positive(self):
        """M2: The wound (gap) must be positive — it's real."""
        misses = find_near_misses(250, max_results=5)
        for m in misses:
            self.assertGreater(m["gap"], 0,
                               "Gap must be positive (wound is real)")

    def test_gap_structure(self):
        """Near-miss dict has expected keys."""
        misses = find_near_misses(250, max_results=1)
        if misses:
            m = misses[0]
            self.assertIn("edges", m)
            self.assertIn("gap", m)
            self.assertIn("gap_relative", m)
            self.assertIn("space_diagonal_approx", m)


class TestStructuralProperties(unittest.TestCase):
    """Test structural properties of the constraint system.

    Framework: These tests validate the STRUCTURAL claims (külliyat)
    as opposed to detail claims (tafsîlât). Per T15, these carry
    higher reliability.
    """

    def test_constraint_coupling(self):
        """AX5: The 4 equations are coupled through shared variables.

        If any single equation is removed, the problem becomes
        trivially solvable. The INTEGRATION is what creates the constraint.
        """
        # Any Pythagorean triple satisfies 1 equation trivially
        r1 = check_cuboid(3, 4, 12)
        # At least 1 equation should be satisfied for most triples
        # The point: individual equations are easy; integration is hard
        self.assertLessEqual(r1.equations_satisfied, 4)

    def test_euler_brick_is_ne_ayn_ne_gayr(self):
        """T14: Euler bricks participate in the truth of perfect cuboids
        without being identical to them. 0 < Fidelity < 1.

        An Euler brick satisfies 3/4 equations: fidelity = 0.75.
        Not zero (participates) and not one (not identical).
        """
        r = check_cuboid(44, 117, 240)
        fidelity = r.equations_satisfied / 4
        self.assertGreater(fidelity, 0, "Must participate (> 0)")
        self.assertLess(fidelity, 1, "Must not be identical (< 1)")
        self.assertAlmostEqual(fidelity, 0.75)

    def test_parity_structural_constraint(self):
        """AX11: The parity constraint is not random — it's structural.

        For a² + b² to be a perfect square, the parity structure
        of (a, b) is constrained. Three odd edges NEVER work.
        """
        # Check many all-odd triples: NONE should be Euler bricks
        for a in range(1, 50, 2):
            for b in range(a, 50, 2):
                for c in range(b, 50, 2):
                    r = check_cuboid(a, b, c)
                    self.assertFalse(r.is_euler_brick,
                                     f"All-odd ({a},{b},{c}) shouldn't be Euler brick")


if __name__ == "__main__":
    unittest.main()
