import copy
import json
from pathlib import Path
import unittest
from experiment import MARKER, PAYLOAD, digest, mutate, verify_case


class MutationTests(unittest.TestCase):
    def test_exactly_one_bit_and_same_length(self):
        changed, offset = mutate(PAYLOAD)
        self.assertEqual(offset, 24000)
        self.assertEqual(len(changed), len(PAYLOAD))
        self.assertEqual(sum((a ^ b).bit_count() for a, b in zip(PAYLOAD, changed)), 1)
        self.assertEqual(changed[:24000], PAYLOAD[:24000])
        self.assertNotEqual(digest(changed), digest(PAYLOAD))

    def test_absent_marker_rejected(self):
        with self.assertRaises(ValueError):
            mutate(b"not the fixture")

    def test_duplicate_marker_rejected(self):
        with self.assertRaises(ValueError):
            mutate(MARKER + MARKER)

    def test_payload_oracle(self):
        self.assertEqual(len(PAYLOAD), 24045)
        self.assertEqual(digest(PAYLOAD), "28bd9067b827f8989cbf6a302fc0f6d9e35af84f87a3d694a39bba28ad6de4a7")


class EvidenceTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        root = Path(__file__).parent
        cls.runs = [json.loads((root / f"results-{n}.json").read_text()) for n in (1, 2)]

    def test_all_recorded_cases(self):
        for result in self.runs:
            for case in result["cases"]:
                with self.subTest(checksums=case["checksums"], at=result["generated_at"]):
                    verify_case(case)

    def test_repeat_same_semantic_results(self):
        first, second = self.runs
        self.assertEqual(first["fixture"], second["fixture"])
        for a, b in zip(first["cases"], second["cases"]):
            self.assertEqual(a["checksums"], b["checksums"])
            self.assertEqual(a["toast_chunks"], b["toast_chunks"])
            for state in ("clean", "damaged"):
                for name in a[state]["probes"]:
                    pa, pb = a[state]["probes"][name], b[state]["probes"][name]
                    self.assertEqual(pa["returncode"], pb["returncode"], name)
                    if pa["returncode"] == 0:
                        self.assertEqual(pa["stdout"], pb["stdout"], name)

    def test_wrong_content_cannot_be_accepted_as_expected(self):
        case = copy.deepcopy(self.runs[0]["cases"][1])
        case["damaged"]["probes"]["whole_sha256"]["stdout"] = digest(PAYLOAD)
        with self.assertRaises(AssertionError):
            verify_case(case)

    def test_masked_read_error_rejected(self):
        case = copy.deepcopy(self.runs[0]["cases"][0])
        case["damaged"]["probes"]["whole_sha256"]["returncode"] = 0
        with self.assertRaises(AssertionError):
            verify_case(case)

    def test_missing_probe_rejected(self):
        case = copy.deepcopy(self.runs[0]["cases"][0])
        del case["damaged"]["probes"]["byte_length"]
        with self.assertRaises(KeyError):
            verify_case(case)

    def test_disabled_offline_checks_not_a_pass(self):
        for result in self.runs:
            case = result["cases"][1]
            for state in ("clean", "damaged"):
                self.assertNotEqual(case[state]["offline_checksums"]["returncode"], 0)
                self.assertIn("not enabled", case[state]["offline_checksums"]["stderr"])


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