"""Regression and replay tests. Authored code: CC0-1.0."""
import copy
import hashlib
from collections import Counter
import os
from pathlib import Path
import tempfile
import unittest

import probe

ROOT = Path(__file__).resolve().parent


class EvidenceTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.retained = probe.strict_json(ROOT / "results-system.json")
        cls.fresh = probe.run(os.environ.get("HESELA_BZIP2", "bzip2"))
        cls.rows = {row["id"]: row for row in cls.fresh["observations"]}

    def test_fresh_native_replay(self):
        probe.compare(self.retained, self.fresh)

    def test_two_native_builds_agree(self):
        probe.compare(self.retained, probe.strict_json(ROOT / "results-upstream-ubsan.json"))

    def test_original_control(self):
        row = self.rows["original"]
        self.assertEqual(row["test_exit"], 0)
        self.assertTrue(row["accepted_exact_original"])
        self.assertTrue(row["python"]["matches_expected"])

    def test_periodic_swap_passes_but_changes_output(self):
        row = self.rows["swap-1-33"]
        self.assertEqual((row["test_exit"], row["decode_exit"], row["small_decode_exit"]),
                         (0, 0, 0))
        self.assertTrue(row["same_output_size"])
        self.assertTrue(row["same_compressed_size"])
        self.assertTrue(row["all_original_block_bits_retained"])
        self.assertTrue(row["same_stored_stream_crc"])
        self.assertTrue(row["python"]["eof"])
        self.assertEqual(row["python"]["unused_bytes"], 0)
        self.assertFalse(row["accepted_exact_original"])
        self.assertFalse(row["python"]["matches_expected"])
        self.assertNotEqual(row["decoded_sha256"], self.rows["original"]["decoded_sha256"])
        self.assertEqual(row["recomputed_combined_crc_hex"],
                         self.rows["original"]["recomputed_combined_crc_hex"])

    def test_four_negative_controls_are_rejected(self):
        for name in ("swap-1-2", "swap-1-32", "block-crc-bit", "stream-crc-bit"):
            with self.subTest(name=name):
                row = self.rows[name]
                self.assertEqual((row["test_exit"], row["decode_exit"],
                                  row["small_decode_exit"]), (2, 2, 2))
                self.assertFalse(row["python"]["accepted"])

    def test_fixture_oracle_and_lengths(self):
        fixture = self.fresh["fixture"]
        expected = hashlib.shake_256(probe.SEED).digest(probe.PAYLOAD_BYTES)
        self.assertEqual(fixture["payload_sha256"], probe.digest(expected))
        self.assertEqual(sum(fixture["block_decoded_bytes"]), len(expected))
        self.assertEqual(fixture["block_count"], 33)
        self.assertEqual(len(set(fixture["block_sha256"])), 33)
        self.assertEqual([r["id"] for r in self.fresh["observations"]], list(probe.EXPECTED_IDS))

    def test_evidence_mutations_are_not_accepted(self):
        mutations = []
        changed = copy.deepcopy(self.retained)
        changed["schema_version"] = True
        mutations.append(changed)
        changed = copy.deepcopy(self.retained)
        changed["observations"][3]["test_exit"] = False
        mutations.append(changed)
        changed = copy.deepcopy(self.retained)
        changed["observations"][3]["decoded_bytes"] = 3250000.0
        mutations.append(changed)
        changed = copy.deepcopy(self.retained)
        changed["observations"][3]["matches_original"] = True
        mutations.append(changed)
        changed = copy.deepcopy(self.retained)
        changed["observations"].pop()
        mutations.append(changed)
        changed = copy.deepcopy(self.retained)
        changed["fixture"]["payload_sha256"] = "0" * 64
        mutations.append(changed)
        for index, changed in enumerate(mutations):
            with self.subTest(index=index), self.assertRaises(ValueError):
                probe.compare(changed, self.fresh)

    def test_environment_exclusion_is_explicit(self):
        changed = copy.deepcopy(self.retained)
        changed["generated_at"] = "another execution"
        changed["environment"]["binary"] = "/another/build"
        probe.compare(changed, self.fresh)


class FormulaTests(unittest.TestCase):
    def test_rotation_against_bit_string_oracle(self):
        for value in (0, 1, 0x80000000, 0xFEDCBA98, 0xFFFFFFFF):
            representation = f"{value:032b}"
            for count in range(97):
                shift = count % 32
                reference = int(representation[shift:] + representation[:shift], 2)
                self.assertEqual(probe.rol(value, count), reference)

    def test_recurrence_and_closed_form(self):
        values = [int.from_bytes(hashlib.sha256(str(i).encode()).digest()[:4], "big")
                  for i in range(97)]
        for length in (0, 1, 2, 31, 32, 33, 64, 65, 97):
            self.assertEqual(probe.combined(values[:length]),
                             probe.closed_form(values[:length]))

    def test_exchange_period_32_and_64(self):
        values = [int.from_bytes(hashlib.sha256(str(i).encode()).digest()[:4], "big")
                  for i in range(97)]
        for a, b in ((0, 32), (1, 33), (31, 63), (0, 64), (32, 96)):
            changed = values.copy()
            changed[a], changed[b] = changed[b], changed[a]
            self.assertEqual(probe.combined(values), probe.combined(changed))

    def test_other_distances_are_not_guaranteed_detection(self):
        # Equal CRC values are a simple counterexample to an overbroad claim.
        values = [0x12345678, 0x12345678]
        self.assertEqual(probe.combined(values), probe.combined(list(reversed(values))))

    def test_parser_rejects_wrong_header_and_trailing_byte(self):
        expected = hashlib.shake_256(probe.SEED).digest(probe.PAYLOAD_BYTES)
        encoded = probe.command("bzip2", ["-1c"], expected).stdout
        with self.assertRaises(ValueError):
            probe.parse_fixture(b"BAD!" + encoded[4:])
        with self.assertRaises(ValueError):
            probe.parse_fixture(encoded + b"\0")

    def test_swap_preserves_exact_block_bit_inventory_and_trailer(self):
        expected = hashlib.shake_256(probe.SEED).digest(probe.PAYLOAD_BYTES)
        original = probe.command("bzip2", ["-1c"], expected).stdout
        header, blocks, trailer, _, _ = probe.parse_fixture(original)
        order = list(range(33))
        order[0], order[32] = order[32], order[0]
        changed = probe.mutate(header, blocks, trailer, order)
        h2, b2, t2, _, _ = probe.parse_fixture(changed)
        self.assertEqual(header, h2)
        self.assertEqual(trailer, t2)
        self.assertEqual(Counter(blocks), Counter(b2))
        self.assertEqual(b2[0], blocks[32])
        self.assertEqual(b2[32], blocks[0])
        self.assertNotEqual(changed, original)
        self.assertEqual(len(changed), len(original))

    def test_invalid_bit_input_and_permutation(self):
        with self.assertRaises(ValueError):
            probe.packed("012")
        with self.assertRaises(ValueError):
            probe.mutate("", ["0", "1"], "", [0, 0])

    def test_strict_json_rejects_duplicate_nonfinite(self):
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory) / "example.json"
            for text in ('{"a":1,"a":2}', '{"a":NaN}', '{"a":Infinity}'):
                path.write_text(text)
                with self.assertRaises(ValueError):
                    probe.strict_json(path)
            path.write_text('{"a": 1, "b": true}')
            self.assertEqual(probe.strict_json(path), {"a": 1, "b": True})


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