import copy
from itertools import permutations, product
import json
from pathlib import Path
import unittest

from model import choose_form, decode, encode, experiment, payload_bytes


class RepresentationTests(unittest.TestCase):
    def test_expected_patch_bitmaps_and_distances(self):
        values = [100, 101, 102, 900, 901, 105, 106, 107]
        labels = [0, 0, 0, 1, 1, 0, 0, 0]
        one, two = encode(values), encode(values, labels)
        self.assertEqual(one["symbols"], [1, 0, 0, 1, 0, 1, 0, 0])
        self.assertEqual(two["symbols"], [2, 0, 0, 3, 1, 0, 0, 0])
        self.assertEqual(one["heads"], [[0, 100], [3, 900], [5, 105]])
        self.assertEqual(two["heads"], [[0, 100], [3, 900]])
        self.assertEqual(decode(two), values)
        self.assertEqual((payload_bytes(one), payload_bytes(two)), (13, 10))

    def test_exhaustive_injective_mappings_and_all_labels(self):
        cases = 0
        for n in range(1, 7):
            for values in permutations(range(6), n):
                self.assertEqual(decode(encode(values)), list(values))
                offsets = [value - i for i, value in enumerate(values)]
                heads = 1 + sum(a != b for a, b in zip(offsets, offsets[1:]))
                self.assertEqual(len(encode(values)["heads"]), heads)
                for labels in product((0, 1), repeat=n):
                    original = (values, labels)
                    result = encode(values, labels)
                    self.assertEqual(decode(result), list(values))
                    base = [offsets[i] for i in range(n) if labels[i] == 0]
                    base_heads = int(bool(base)) + sum(a != b for a, b in zip(base, base[1:]))
                    local_heads = sum(labels[i] == 1 and
                                      (i == 0 or labels[i-1] != 1 or offsets[i] != offsets[i-1])
                                      for i in range(n))
                    self.assertEqual(len(result["heads"]), base_heads + local_heads)
                    self.assertEqual((values, labels), original)
                    cases += 1
        self.assertEqual(cases, 75972)

    def test_local_track_does_not_bridge_a_base_gap(self):
        values, labels = [100, 900, 901, 103, 903, 904, 106], [0, 1, 1, 0, 1, 1, 0]
        result = encode(values, labels)
        self.assertEqual(result["heads"], [[0, 100], [1, 900], [4, 903]])
        self.assertEqual(decode(result), values)

    def test_512_entry_crossover(self):
        cases = {c["id"]: c for c in experiment()["cases"]}
        for patches, expected in [(0, (68, 132)), (1, (76, 136)),
                                   (16, (196, 196)), (17, (204, 200))]:
            c = cases["512_entries_" + str(patches) + "_patches"]
            self.assertEqual(len(c["one_bit"]["heads"]), 1 + 2 * patches)
            self.assertEqual(len(c["two_bit"]["heads"]), 1 + patches)
            self.assertEqual((c["one_bit_payload_bytes"], c["two_bit_payload_bytes"]), expected)

    def test_width_sensitivity_and_rounding(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):
                    self.assertEqual(payload_bytes(obj, width), (n * bits + 7) // 8 + width)
        with self.assertRaises(ValueError):
            payload_bytes(encode([255, 256]), 1)

    def test_hysteresis_boundaries(self):
        self.assertEqual([choose_form("flat", n, 100) for n in (79, 80, 85, 90, 91)],
                         ["bitmap", "flat", "flat", "flat", "flat"])
        self.assertEqual([choose_form("bitmap", n, 100) for n in (79, 80, 85, 90, 91)],
                         ["bitmap", "bitmap", "bitmap", "bitmap", "flat"])
        for scale in (1, 2 ** 60):
            self.assertEqual(choose_form("flat", 80 * scale, 100 * scale), "flat")
            self.assertEqual(choose_form("bitmap", 90 * scale, 100 * scale), "bitmap")

    def test_invalid_inputs(self):
        for bad in ([], [1, 1], [-1], [True], [1.0]):
            with self.assertRaises(ValueError):
                encode(bad)
        for labels in ([], [2], [True], [0.0], [0, 1]):
            with self.assertRaises(ValueError):
                encode([1], labels)
        for args in (("unknown", 1, 1), ("flat", -1, 1), ("flat", 0, 0),
                     ("flat", True, 1), ("bitmap", 0.0, 1)):
            with self.assertRaises(ValueError):
                choose_form(*args)

    def test_rejects_missing_and_misbound_heads(self):
        good = encode([100, 101, 900, 103], [0, 0, 1, 0])
        for change in ("missing", "position", "width", "symbol", "headless"):
            obj = copy.deepcopy(good)
            if change == "missing":
                obj["heads"].pop()
            elif change == "position":
                obj["heads"][1][0] = 3
            elif change == "width":
                obj["bits_per_entry"] = True
            elif change == "symbol":
                obj["symbols"][1] = 4
            else:
                obj["symbols"][0] = 0
                obj["heads"].pop(0)
            with self.assertRaises(ValueError):
                decode(obj)

    def test_roundtrip_is_not_integrity_protection(self):
        obj = encode([100, 101])
        obj["heads"][0][1] = 200
        self.assertEqual(decode(obj), [200, 201])

    def test_results_are_exact(self):
        stored = json.loads(Path(__file__).with_name("results.json").read_text())
        self.assertEqual(stored, experiment())
        self.assertEqual(stored["evidence_class"], "synthetic-representation-accounting")


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