"""test_spectral_codebook.py — Gap-aware spectral codebook round-trip tests.

Validates the spectral_codebook module against the 250-matrix PIST corpus:
exact char-poly fingerprints (Cayley–Hamilton in integer arithmetic),
pure-stdlib vs NumPy spectral-radius agreement, gap-aware quantization
invariants, and the encode/decode round trip over all 250 equations.
"""

from __future__ import annotations

import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))

import spectral_codebook as sc


def _mat_mul(a, b):
    n = len(a)
    return [
        [sum(a[i][t] * b[t][j] for t in range(n)) for j in range(n)]
        for i in range(n)
    ]


class TestSpectralCodebook(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.matrices = sc.parse_matrices_lean()
        cls.codebook = sc.Codebook(cls.matrices)

    # ── parsing ─────────────────────────────────────────────────────

    def test_parse_250_8x8(self):
        self.assertEqual(len(self.matrices), 250)
        for eid, mat in self.matrices.items():
            self.assertEqual(len(mat), 8, eid)
            self.assertTrue(all(len(r) == 8 for r in mat), eid)
            self.assertTrue(all(x >= 0 for r in mat for x in r), eid)

    # ── exact char poly ─────────────────────────────────────────────

    def test_charpoly_identity(self):
        # det(λI − I) = (λ − 1)^8 → coefficients are signed binomials
        ident = tuple(tuple(1 if i == j else 0 for j in range(8)) for i in range(8))
        expect = (-8, 28, -56, 70, -56, 28, -8, 1)
        self.assertEqual(sc.charpoly_coeffs(ident), expect)

    def test_cayley_hamilton_integer(self):
        """p(M) = 0 exactly in integer arithmetic (spot check)."""
        for eid in sorted(self.matrices)[::50]:  # 5 spread-out matrices
            m = [list(r) for r in self.matrices[eid]]
            coeffs = sc.charpoly_coeffs(m)
            n = len(m)
            acc = [[1 if i == j else 0 for j in range(n)] for i in range(n)]  # M^0
            total = [[coeffs[-1] if i == j else 0 for j in range(n)] for i in range(n)]
            for k in range(1, n + 1):
                acc = _mat_mul(acc, m)  # M^k
                c = 1 if k == n else coeffs[n - 1 - k]
                for i in range(n):
                    for j in range(n):
                        total[i][j] += c * acc[i][j]
            self.assertTrue(
                all(x == 0 for row in total for x in row),
                f"Cayley–Hamilton failed for {eid}",
            )

    def test_charpoly_trace_relation(self):
        for eid, mat in self.matrices.items():
            coeffs = self.codebook.profiles[eid]["charpoly"]
            self.assertEqual(coeffs[0], -sum(mat[i][i] for i in range(8)), eid)

    # ── spectral radius ─────────────────────────────────────────────

    def test_pure_root_finder_matches_numpy(self):
        """Durand–Kerner on the exact char poly agrees with the fast path."""
        for eid in sorted(self.matrices)[::10]:  # 25 matrices
            p = self.codebook.profiles[eid]
            pure = sc._durand_kerner_max_modulus(p["charpoly"])
            self.assertAlmostEqual(pure, p["spectral_radius"], places=4, msg=eid)

    def test_peripheral_spectra_are_unit(self):
        """The 9 'mid band' λ in the committed raw JSON (0.5 ≤ λ < 1) are
        power-iteration non-convergence artifacts: exact ρ = 1 for all 9."""
        import json

        raw_path = Path(__file__).resolve().parent.parent / "data" / "spectral_codebook_raw.json"
        raw = json.loads(raw_path.read_text())
        artifacts = [
            e["equation_id"] for e in raw["entries"]
            if 0.5 <= e["spectral_radius"] < 1.0
        ]
        self.assertEqual(len(artifacts), 9)
        for eid in artifacts:
            self.assertAlmostEqual(
                self.codebook.profiles[eid]["spectral_radius"], 1.0, places=6, msg=eid
            )

    def test_nilpotent_class(self):
        """Every ρ = 0 matrix has char poly exactly x^8 (nilpotent), and the
        pure path detects it with no iteration."""
        zeros = [
            eid for eid, p in self.codebook.profiles.items()
            if p["spectral_radius"] == 0.0
        ]
        self.assertEqual(len(zeros), 35)
        for eid in zeros:
            coeffs = self.codebook.profiles[eid]["charpoly"]
            self.assertEqual(list(coeffs), [0] * 8, eid)
            self.assertEqual(sc._durand_kerner_max_modulus(coeffs), 0.0)

    # ── fingerprints & collisions ───────────────────────────────────

    def test_charpoly_strictly_finer_than_lambda(self):
        col = self.codebook.collision_report()
        self.assertEqual(col["corpus_size"], 250)
        self.assertEqual(col["distinct_matrices"], 238)
        self.assertEqual(col["duplicate_matrix_groups"], 11)
        self.assertEqual(col["duplicate_matrix_members"], 23)
        self.assertGreater(col["unique_charpoly"], col["unique_lambda_4dp"])
        self.assertGreater(
            col["identified_by_charpoly"], col["identified_by_lambda_4dp"]
        )
        # char poly is NOT injective: collision classes must be explicit
        self.assertGreater(col["charpoly_collision_groups"], 0)
        self.assertEqual(
            sum(len(v) for v in col["charpoly_collision_classes"].values()),
            col["charpoly_collision_members"],
        )

    # ── quantization invariants ─────────────────────────────────────

    def test_boundaries_sorted_and_gapped(self):
        b = self.codebook.boundaries
        self.assertEqual(b, sorted(b))
        self.assertGreater(len(b), 0)
        # no λ value sits on a boundary
        for p in self.codebook.profiles.values():
            self.assertNotIn(p["spectral_radius"], b)

    def test_min_support_guard(self):
        """Every cluster holds ≥ MIN_SUPPORT distinct matrices."""
        for row in self.codebook.cluster_table():
            self.assertGreaterEqual(
                row["distinct_matrices"], sc.MIN_SUPPORT, row["codeword"]
            )

    def test_cluster_partition_covers_corpus(self):
        table = self.codebook.cluster_table()
        self.assertEqual(sum(r["count"] for r in table), 250)
        self.assertEqual(sum(r["distinct_matrices"] for r in table), 238)

    # ── round trip ──────────────────────────────────────────────────

    def test_decode_is_bijective_over_250(self):
        seen = set()
        for eid in self.matrices:
            cw, idx = self.codebook.codeword_of(eid)
            self.assertNotIn((cw, idx), seen)
            seen.add((cw, idx))
            self.assertEqual(self.codebook.decode(cw, idx), eid)
        self.assertEqual(len(seen), 250)

    def test_encode_decode_round_trip(self):
        """encode(matrix) → decode must return the same equation for every
        unique matrix, and a byte-identical matrix for exact duplicates
        (identical inputs are information-theoretically indistinguishable)."""
        dup_members = {
            eid
            for ids in self.codebook.matrix_groups.values()
            if len(ids) > 1
            for eid in ids
        }
        for eid, mat in self.matrices.items():
            cw, idx = self.codebook.encode([list(r) for r in mat])
            self.assertIsNotNone(idx, eid)
            decoded = self.codebook.decode(cw, idx)
            if eid in dup_members:
                self.assertEqual(self.matrices[decoded], mat, eid)
            else:
                self.assertEqual(decoded, eid)

    def test_encode_novel_matrix(self):
        novel = [[3 if i == j else 1 for j in range(8)] for i in range(8)]
        cw, idx = self.codebook.encode(novel)
        self.assertIsNone(idx)
        self.assertTrue(cw.startswith("C"))

    # ── 278-row manifold ────────────────────────────────────────────

    def test_manifold_278_no_new_gaps(self):
        mc = sc.manifold_check(self.codebook)
        self.assertEqual(mc["rows"], 278)
        self.assertEqual(mc["unique_ids"], 250)
        self.assertEqual(mc["extra_rows"], 28)
        self.assertEqual(mc["ids_without_matrix"], [])
        self.assertTrue(mc["boundaries_match_250"])


class TestSpectralCodebookDbSync(unittest.TestCase):
    """DB sync layer (spectral_codebook_db): dry-run only, no network."""

    @classmethod
    def setUpClass(cls):
        import spectral_codebook_db as db
        cls.db = db
        cls.codebook = sc.Codebook(sc.parse_matrices_lean())
        cls.rows = db.build_rows(cls.codebook)

    def test_rows_shape(self):
        self.assertEqual(len(self.rows), 250)
        cols = {"id", "equation_id", "proxy_pred", "exact_pred",
                "matrix_hash", "confidence"}
        for r in self.rows:
            self.assertEqual(set(r), cols)
            self.assertTrue(r["proxy_pred"].startswith("C"))
            self.assertIn(r["exact_pred"], {
                "LogogramProjection", "SignalShapedRouteCompiler",
                "CognitiveLoadField",
            })
            self.assertTrue(r["matrix_hash"].startswith("charpoly="))
            self.assertIn(";pos10=", r["matrix_hash"])
        # deterministic uuid5 ids, unique per equation
        self.assertEqual(len({r["id"] for r in self.rows}), 250)

    def test_confidence_matches_collision_classes(self):
        col = self.codebook.collision_report()
        unique = [r for r in self.rows if r["confidence"] == 1.0]
        self.assertEqual(len(unique), col["identified_by_charpoly"])
        by_eid = {r["equation_id"]: r for r in self.rows}
        for cls_ids in col["charpoly_collision_classes"].values():
            k = len(cls_ids)
            for eid in cls_ids:
                self.assertAlmostEqual(by_eid[eid]["confidence"], 1.0 / k, places=6)

    def test_exact_pred_mirrors_classifyn_thresholds(self):
        # ClassifyN.lean: 1.5/4.0 Q16.16 thresholds
        self.assertEqual(self.db.classify_exact_shape(0.0), "LogogramProjection")
        self.assertEqual(self.db.classify_exact_shape(1.0), "LogogramProjection")
        self.assertEqual(self.db.classify_exact_shape(1.5), "LogogramProjection")  # green==0 edge
        self.assertEqual(self.db.classify_exact_shape(2.0), "SignalShapedRouteCompiler")
        self.assertEqual(self.db.classify_exact_shape(4.0), "CognitiveLoadField")
        self.assertEqual(self.db.classify_exact_shape(16.99), "CognitiveLoadField")

    def test_positional_hash_injective_over_corpus(self):
        """Base-10 positional hash separates all 238 distinct matrices
        (ClassifyN's base-5 hash offers no such guarantee: entries reach 9)."""
        max_entry = max(x for m in self.codebook.matrices.values()
                        for row in m for x in row)
        self.assertLessEqual(max_entry, 9)
        hashes = {self.db.positional_hash(m) for m in self.codebook.matrix_groups}
        self.assertEqual(len(hashes), len(self.codebook.matrix_groups))

    def test_dry_run_writes_nothing(self):
        import contextlib
        import io

        out = io.StringIO()
        with contextlib.redirect_stdout(out):
            rows = self.db.sync_db(codebook=self.codebook)  # pure dry run
        text = out.getvalue()
        self.assertEqual(len(rows), 250)
        self.assertIn("250 rows", text)
        self.assertIn("DRY RUN", text)
        self.assertFalse(
            (sc.REPO_ROOT / "data" / "spectral_codebook_sync.sql").exists(),
            "dry run must not write the sync SQL file",
        )

    def test_emit_sql_renders_all_rows(self):
        import contextlib
        import io
        import tempfile

        with tempfile.TemporaryDirectory() as td:
            target = Path(td) / "sync.sql"
            with contextlib.redirect_stdout(io.StringIO()):
                self.db.sync_db(codebook=self.codebook, emit_sql=target)
            text = target.read_text()
            self.assertEqual(text.count("INSERT INTO ene.rrc_predictions"), 250)
            self.assertTrue(text.startswith("--"))
            self.assertIn("BEGIN;", text)
            self.assertIn("COMMIT;", text)
            self.assertIn("ON CONFLICT (id) DO UPDATE", text)

    def test_schema_matches_repo_ddl(self):
        """Offline check: emitted columns ⊆ ene.rrc_predictions DDL in
        scripts/auto/ene_schema.sql (live-DB check is --verify-schema)."""
        import re

        ddl = (sc.REPO_ROOT / "scripts" / "auto" / "ene_schema.sql").read_text()
        m = re.search(
            r"CREATE TABLE IF NOT EXISTS ene\.rrc_predictions \((.*?)\);",
            ddl, re.DOTALL,
        )
        self.assertIsNotNone(m)
        ddl_cols = set(re.findall(r"(\w+)\s+(?:UUID|TEXT|FLOAT|INT|TIMESTAMPTZ)", m.group(1)))
        emitted = {"id", "equation_id", "proxy_pred", "exact_pred",
                   "matrix_hash", "confidence"}
        self.assertTrue(emitted <= ddl_cols, ddl_cols)

    def test_neon_dsn_convention(self):
        import os

        old = os.environ.pop("NEON_PG", None)
        try:
            self.assertEqual(
                self.db.neon_dsn(),
                "postgres://postgres:postgres@100.92.88.64:5432/research_stack",
            )
            # auto_pipeline.py convention: host:port only → db appended
            os.environ["NEON_PG"] = "postgres://postgres:postgres@100.92.88.64:5432"
            self.assertTrue(self.db.neon_dsn().endswith("/research_stack"))
        finally:
            if old is None:
                os.environ.pop("NEON_PG", None)
            else:
                os.environ["NEON_PG"] = old


class TestPhiCorkscrewPacking(unittest.TestCase):
    """STEP 1: integer spiral-index packing — exact, injective, reversible."""

    @classmethod
    def setUpClass(cls):
        cls.matrices = sc.parse_matrices_lean()
        cls.codebook = sc.Codebook(cls.matrices)

    def test_base_is_odd_and_covers_corpus(self):
        base, offset = self.codebook.pack_base, self.codebook.pack_offset
        self.assertEqual(base, 2 * offset + 1)
        for p in self.codebook.profiles.values():
            for c in p["charpoly"]:
                self.assertLessEqual(abs(c), offset)

    def test_round_trip_all_250(self):
        base, offset = self.codebook.pack_base, self.codebook.pack_offset
        for eid, p in self.codebook.profiles.items():
            fp = tuple(p["charpoly"])
            n = self.codebook.spiral_indices[eid]
            self.assertEqual(sc.unpack_spiral_index(n, base, offset, 8), fp, eid)

    def test_injective_on_fingerprints(self):
        by_fp = {}
        for eid, p in self.codebook.profiles.items():
            by_fp.setdefault(tuple(p["charpoly"]), set()).add(
                self.codebook.spiral_indices[eid])
        # one index per fingerprint …
        for fp, idxs in by_fp.items():
            self.assertEqual(len(idxs), 1, fp)
        # … and distinct fingerprints get distinct indices
        all_idx = [next(iter(v)) for v in by_fp.values()]
        self.assertEqual(len(set(all_idx)), len(by_fp))

    def test_signed_collision_regression(self):
        # The unoffset packing collapsed (-1, 1) and (1, 0); ours must not.
        base, offset = sc.packing_params([(-1, 1), (1, 0)])
        a = sc.pack_spiral_index((-1, 1), base, offset)
        b = sc.pack_spiral_index((1, 0), base, offset)
        self.assertNotEqual(a, b)

    def test_out_of_range_raises(self):
        with self.assertRaises(ValueError):
            sc.pack_spiral_index((5,), base=3, offset=1)

    def test_integration_sprint_integer_path_fixed(self):
        import numpy as np
        import integration_sprint as isp
        # same shape, same abs_max (=1) → same per-call base/offset, so the
        # historical base-2 collision pair must now pack to distinct indices
        a = isp.phi_corkscrew_index(np.array([-1.0, 1.0, 0.0]))
        b = isp.phi_corkscrew_index(np.array([1.0, 0.0, -1.0]))
        self.assertNotEqual(a, b)


class TestCorkscrewLayout(unittest.TestCase):
    """STEP 2: f(n) layout — decorative placement, verified against V007."""

    @classmethod
    def setUpClass(cls):
        cls.matrices = sc.parse_matrices_lean()
        cls.codebook = sc.Codebook(cls.matrices)

    def test_v007_reference_point(self):
        # VERIFICATION_LOG.md V007: f(20121) = (-137.80079576, -33.64432624)
        x, y = sc.corkscrew_xy(20121)
        self.assertAlmostEqual(x, -137.80079576, places=5)
        self.assertAlmostEqual(y, -33.64432624, places=5)

    def test_angle_frac_in_unit_interval(self):
        for eid, n in self.codebook.spiral_indices.items():
            f = sc.angle_frac(n)
            self.assertGreaterEqual(f, 0.0, eid)
            self.assertLess(f, 1.0, eid)

    def test_angle_frac_exceeds_float64_naive(self):
        # for n >> 2^52 the float product n·α has no fractional bits left;
        # the decimal path must still give a nontrivial fraction
        n = 10 ** 40 + 7
        f = sc.angle_frac(n)
        self.assertGreaterEqual(f, 0.0)
        self.assertLess(f, 1.0)
        # consecutive indices step by frac(α): distinguishable at any scale
        g = sc.angle_frac(n + 1)
        alpha_frac = 0.3819660112501051  # frac((3−√5)/2)
        diff = (g - f) % 1.0
        self.assertAlmostEqual(diff, alpha_frac, places=9)

    def test_layout_in_json_and_information_neutral(self):
        doc = self.codebook.to_json()
        pc = doc["phi_corkscrew"]
        self.assertEqual(pc["packing"]["base"], self.codebook.pack_base)
        sep = pc["layout"]["min_angular_separation_frac"]
        self.assertIsNotNone(sep)
        self.assertGreater(sep, 0.0)
        for entry in doc["entries"]:
            # radius² IS the spiral index — layout adds no information
            self.assertEqual(entry["layout"]["radius_sq"], entry["spiral_index"])


class TestCartanFloor(unittest.TestCase):
    """STEP 3: Cartan ∆ = 17/1792 as Fisher-distance resolution floor."""

    @classmethod
    def setUpClass(cls):
        cls.matrices = sc.parse_matrices_lean()
        cls.codebook = sc.Codebook(cls.matrices)
        cls.report = sc.cartan_floor_report(cls.codebook.profiles)

    def test_delta_exact_value(self):
        from fractions import Fraction
        self.assertEqual(sc.CARTAN_DELTA, Fraction(17, 1792))

    def test_fisher_metric_axioms(self):
        p = sc.simplex_project((1, -2, 3, 0, 0, 0, 0, 4))
        q = sc.simplex_project((0, 5, 0, 1, 0, 0, 0, 0))
        self.assertAlmostEqual(sc.fisher_distance(p, p), 0.0, places=12)
        self.assertAlmostEqual(sc.fisher_distance(p, q),
                               sc.fisher_distance(q, p), places=12)
        # disjoint support → maximal distance π
        u = sc.simplex_project((1, 0, 0, 0, 0, 0, 0, 0))
        v = sc.simplex_project((0, 1, 0, 0, 0, 0, 0, 0))
        import math
        self.assertAlmostEqual(sc.fisher_distance(u, v), math.pi, places=12)

    def test_simplex_projection_sums_to_one(self):
        for p in self.codebook.profiles.values():
            pt = sc.simplex_project(p["charpoly"])
            self.assertAlmostEqual(sum(pt), 1.0, places=12)

    def test_zero_fingerprint_convention(self):
        pt = sc.simplex_project((0,) * 8)
        self.assertEqual(pt, tuple(1.0 / 8 for _ in range(8)))

    def test_report_consistency(self):
        r = self.report
        self.assertEqual(r["distinct_fingerprints"], 196)
        # merging k sub-∆ pairs can remove at most k components
        self.assertGreaterEqual(
            r["delta_resolution_codewords"],
            r["distinct_fingerprints"] - r["pairs_below_delta"])
        self.assertLessEqual(r["delta_resolution_codewords"],
                             r["distinct_fingerprints"])

    def test_simplex_projection_is_lossy(self):
        # |c|/Σ|c| destroys sign+scale: distinct fingerprints can collide on
        # Δ₇ (min Fisher distance 0.0 in this corpus) — similarity layer,
        # NOT an identity key. This is asserted so the doc claim stays true.
        self.assertEqual(self.report["min_fisher_distance"], 0.0)


class TestTorusWindingExact(unittest.TestCase):
    """STEP 4: torus-winding round trip must not saturate at n ≥ 65536."""

    @classmethod
    def setUpClass(cls):
        import pist_braid_bridge as pbb
        cls.pbb = pbb

    def test_round_trip_regression_1e9(self):
        pbb = self.pbb
        for n in (0, 1, 65535, 65536, 10 ** 9):
            w = pbb.spiral_to_torus_winding(n)
            self.assertEqual(pbb.torus_to_spiral_index(w), n, n)

    def test_exact_fields_populated(self):
        pbb = self.pbb
        w = pbb.spiral_to_torus_winding(10 ** 9)
        self.assertEqual(w.a_exact, 10 ** 9 % 2)
        self.assertEqual(w.b_exact, 10 ** 9 // 2)

    def test_q16_raws_still_saturate_and_are_flagged(self):
        pbb = self.pbb
        w, saturated = pbb.spiral_to_torus_winding_safe(10 ** 9)
        self.assertTrue(saturated)
        self.assertEqual(w.b, pbb.Q16_MAX_RAW)  # legacy raw clamps…
        self.assertEqual(pbb.torus_to_spiral_index(w), 10 ** 9)  # …exact wins

    def test_legacy_winding_without_exact_fields(self):
        pbb = self.pbb
        w = pbb.TorusWinding(a=pbb.float_to_q16(1.0), b=pbb.float_to_q16(21.0))
        self.assertEqual(pbb.torus_to_spiral_index(w), 43)


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