"""Synthetic record mutation tests only; numerical execution is in test_audit."""

import copy
import math
import unittest

import audit
import field
import radiation


class ValidationTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # These are deliberately idealized accounting records, not simulated
        # observations. They isolate validator behavior from a solver rerun.
        refs = {str(width): {label: {str(n): audit.reference(width, phase, n) for n in (33, 65)}
                             for label, phase in audit.PHASES.items()} for width in (0.25, 0.2)}
        rows = []
        for name, width, phase in audit.CASES:
            h, half, cells, steps, dt = field.CONFIGS[name]
            oracle = radiation.integrated(radiation.axes(h, width), phase)
            data = {"status": field.STATUS, "backend": "numpy-2.5.3", "configuration": name,
                    "width": width, "phase": phase, "source_rule": "clipped-cell-mass-at-node-v1",
                    "spacing": h, "time_step": dt, "steps": steps, "control_half_width": cells * h,
                    "outer_half_width": half * h,
                    "earliest_boundary_return": 2 * half * h - cells * h - math.pi / 4 - 3 * width - h / 2,
                    "source_nodes": len(field.sources(h, half, width)[1]),
                    "energy_change": 0.0, "momentum_change": 0.0,
                    "integrated_energy_flux": oracle["energy_flux"], "integrated_source_work": oracle["energy_flux"],
                    "integrated_momentum_flux": oracle["momentum_flux"], "integrated_source_force": oracle["source_reaction"],
                    "energy_residual": 0.0, "momentum_residual": 0.0,
                    "peak_abs_stored_momentum": abs(oracle["source_reaction"])}
            rows.append({"field": data, "radiation": oracle})
        cls.record = {"status": audit.STATUS, "references": refs, "rows": rows}
        cls.baseline = audit.assess(cls.record)

    def test_idealized_records_retain_eight_coarse_sampling_failures(self):
        self.assertEqual(len(self.baseline), 8)
        self.assertTrue(all("sampling exceeds allocation" in item for item in self.baseline))

    def test_impossible_storage_and_counter_types_refuse(self):
        for values in (
            {"energy_change": -1e-8, "energy_residual": -1e-8},
            {"peak_abs_stored_momentum": 0.0},
            {"source_nodes": float(self.record["rows"][0]["field"]["source_nodes"])},
            {"spacing": float("nan")},
            {"time_step": float("inf")},
        ):
            damaged = copy.deepcopy(self.record)
            damaged["rows"][0]["field"].update(values)
            self.assertTrue(set(audit.assess(damaged)) - set(self.baseline))

    def test_equivalent_json_number_spellings_preserve_geometry(self):
        equivalent = copy.deepcopy(self.record)
        for row in equivalent["rows"]:
            row["field"]["control_half_width"] = 4
            if row["field"]["phase"] == 0:
                row["field"]["phase"] = 0
        self.assertEqual(audit.assess(equivalent), self.baseline)

    def test_finest_controls_detect_balanced_mutations(self):
        for width in (0.25, 0.2):
            for case, values, failure in (
                (("cell_finest", width, math.pi / 2),
                 {"integrated_source_force": 1e-7, "momentum_residual": 1e-7}, "phase reversal failed"),
                (("cell_finest", width, 0.0),
                 {"integrated_momentum_flux": 2e-8, "momentum_residual": 2e-8}, "null failed"),
                (("cell_finest_outer_wide", width, -math.pi / 2),
                 {"integrated_source_work": 2e-7, "energy_residual": -2e-7}, "outer-domain sensitivity failed"),
            ):
                damaged = copy.deepcopy(self.record)
                data = damaged["rows"][audit.CASES.index(case)]["field"]
                for key, delta in values.items():
                    data[key] += delta
                self.assertIn(f"finest/{width}: {failure}", audit.assess(damaged))

    def test_time_pair_cannot_hide_between_individually_allowed_errors(self):
        for width in (0.25, 0.2):
            damaged = copy.deepcopy(self.record)
            for name, delta in (("cell_finest", -0.003), ("cell_finest_half_dt", 0.003)):
                row = damaged["rows"][audit.CASES.index((name, width, -math.pi / 2))]
                for key in ("integrated_energy_flux", "integrated_source_work"):
                    row["field"][key] += delta
            self.assertIn(f"finest/{width}: time-step sensitivity failed for integrated_energy_flux", audit.assess(damaged))

    def test_missing_reordered_or_relabelled_controls_refuse(self):
        for mutate in (
            lambda run: run["rows"].pop(),
            lambda run: run["rows"].reverse(),
            lambda run: run["rows"][-1]["field"].update(configuration="cell_finest"),
            lambda run: run["rows"][-2]["field"].update(time_step=0.02),
            lambda run: run["references"]["0.2"].pop("null"),
            lambda run: run["references"]["0.25"]["reverse"]["65"].update(energy_flux=0.0),
        ):
            damaged = copy.deepcopy(self.record)
            mutate(damaged)
            self.assertTrue(set(audit.assess(damaged)) - set(self.baseline))


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