import copy
import json
from pathlib import Path
import unittest

from model import choose_form, encode, payload_bytes
from packed import certificate, history_certificate, pack, unpack


class PackedTests(unittest.TestCase):
    def test_frozen_witnesses_exact_bytes_and_reconstruction(self):
        stored = json.loads(Path(__file__).with_name("packed-results.json").read_text())
        self.assertEqual(stored, certificate())
        self.assertEqual(len(stored["cases"]), 8)

    def test_handwritten_bitmap_and_head_order(self):
        one = {"bits_per_entry": 1, "symbols": [1, 0, 0, 1, 0, 1, 0, 0],
               "heads": [[0, 1], [3, 32], [5, 6]]}
        two = {"bits_per_entry": 2, "symbols": [2, 0, 0, 3, 1, 0, 0, 0],
               "heads": [[0, 1], [3, 32]]}
        expected = [1, 2, 3, 32, 33, 6, 7, 8]
        self.assertEqual(pack(one).hex(), "29010000002000000006000000")
        self.assertEqual(pack(two).hex(), "c2010100000020000000")
        self.assertEqual(unpack(pack(one), 8, 1), expected)
        self.assertEqual(unpack(pack(two), 8, 2), expected)

    def test_partial_bytes_and_address_widths(self):
        for n in range(1, 20):
            for bits in (1, 2):
                obj = encode(range(n), [0] * n if bits == 2 else None)
                for width in (1, 4, 8):
                    data = pack(obj, width)
                    self.assertEqual(len(data), payload_bytes(obj, width))
                    self.assertEqual(unpack(data, n, bits, width), list(range(n)))

    def test_truncation_and_trailing_bytes_rejected(self):
        data = pack(encode([1, 2, 40]))
        for bad in (b"", data[:-1], data + b"\0"):
            with self.assertRaises(ValueError):
                unpack(bad, 3, 1)

    def test_padding_and_headless_continuation_rejected(self):
        data = pack(encode([1, 2]))
        for bad in (bytes([data[0] | 128]) + data[1:], b"\0"):
            with self.assertRaises(ValueError):
                unpack(bad, 2, 1)

    def test_local_bridge_rejected_without_base_bridge_rejection(self):
        obj = {"bits_per_entry": 2, "symbols": [3, 2, 1],
               "heads": [[0, 10], [1, 100]]}
        with self.assertRaises(ValueError):
            pack(obj)
        obj = {"bits_per_entry": 2, "symbols": [2, 3, 0],
               "heads": [[0, 10], [1, 100]]}
        self.assertEqual(unpack(pack(obj), 3, 2), [10, 100, 12])

    def test_duplicate_and_overflow_reconstruction_rejected(self):
        for data in (bytes([3, 7, 7]), bytes([1, 255])):
            with self.assertRaises(ValueError):
                unpack(data, 2, 1, 1)

    def test_invalid_dimensions_and_heads(self):
        for n, bits, width in [(True, 1, 4), (0, 1, 4), (1, True, 4),
                               (1, 3, 4), (1, 1, 0), (1, 1, True)]:
            with self.assertRaises(ValueError):
                unpack(b"", n, bits, width)
        good = encode([1, 2, 40])
        for heads in ([[[0, 1]]], [[0, True], [2, 40]],
                      [[2, 40], [0, 1]], [[0, 256], [2, 40]]):
            obj = copy.deepcopy(good)
            obj["heads"] = heads
            with self.assertRaises(ValueError):
                pack(obj, 1)

    def test_valid_changed_head_is_not_detected_as_corruption(self):
        data = bytearray(pack(encode([10, 11])))
        data[1] = 20
        self.assertEqual(unpack(bytes(data), 2, 1), [20, 21])

    def test_hysteresis_history_requires_no_new_map_population(self):
        # A conditional size-state witness, not a reconstructed device trace.
        cert = history_certificate()
        for history in cert["histories"]:
            state = history["initial_form"]
            for step in history["steps"]:
                state = choose_form(state, step["candidate_bitmap_bytes"], cert["flat_bytes"])
                self.assertEqual(state, step["form"])
        self.assertEqual([h["steps"][-1]["selected_payload_bytes"]
                          for h in cert["histories"]], [1728, 2048])
        flat, compressed = 2048, 1728
        self.assertEqual(choose_form("flat", 1636, flat), "bitmap")
        self.assertEqual(choose_form("bitmap", compressed, flat), "bitmap")
        self.assertEqual(choose_form("flat", 1844, flat), "flat")
        self.assertEqual(choose_form("flat", compressed, flat), "flat")
        self.assertEqual(flat - compressed, 320)
        self.assertEqual(compressed * 100 / flat, 84.375)


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