import copy
import json
from pathlib import Path
import subprocess
import sys
import unittest

import experiment as e
import routing

ROOT = Path(__file__).parent


class EvidenceTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.report = json.loads((ROOT / "observations.json").read_text())
        cls.routing = json.loads((ROOT / "routing.json").read_text())

    def test_recorded_evidence(self):
        e.validate(self.report)
        routing.validate(self.routing)

    def test_independent_crc_known_answer(self):
        self.assertEqual(e.crc32c(b"123456789"), 0xE3069283)
        self.assertEqual(e.crc32c(b""), 0)

    def test_missing_row_is_rejected(self):
        altered = copy.deepcopy(self.report)
        altered["observations"].pop()
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_duplicate_row_is_rejected(self):
        altered = copy.deepcopy(self.report)
        altered["observations"][-1] = altered["observations"][0]
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_changed_value_is_rejected(self):
        altered = copy.deepcopy(self.report)
        altered["observations"][0]["result"]["values"][0] = 99
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_changed_hash_is_rejected(self):
        altered = copy.deepcopy(self.report)
        altered["observations"][0]["result"]["sha256_int32_le"] = "0" * 64
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_wrong_exception_is_rejected(self):
        altered = copy.deepcopy(self.report)
        next(r for r in altered["observations"] if r["result"]["status"] == "error")["result"]["error_type"] = "OSError"
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_false_index_integrity_is_rejected(self):
        altered = copy.deepcopy(self.report)
        fixture = next(r for r in altered["fixtures"] if r["layout"] == "shard-end" and r["state"] == "corrupt-index")
        fixture["shard"]["index_crc_valid"] = True
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_false_history_equivalence_is_rejected(self):
        altered = copy.deepcopy(self.report)
        fixture = next(r for r in altered["fixtures"] if r["layout"] == "shard-start" and r["state"] == "stale-sparse-shard")
        fixture["after"]["c/0"] = "0" * 64
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_routing_effective_flag_is_checked(self):
        altered = copy.deepcopy(self.routing)
        altered["observations"][0]["result"]["effective_read_missing_chunks"] = False
        with self.assertRaises(ValueError):
            routing.validate(altered)

    def test_routing_missing_case_is_rejected(self):
        altered = copy.deepcopy(self.routing)
        altered["observations"].pop()
        with self.assertRaises(ValueError):
            routing.validate(altered)

    def test_version_scope_is_checked(self):
        altered = copy.deepcopy(self.report)
        altered["environment"]["packages"]["zarr"] = "unmeasured"
        with self.assertRaises(ValueError):
            e.validate(altered)

    def test_runtime_validation_survives_optimization(self):
        result = subprocess.run([sys.executable, "-O", "-c",
                                 "from experiment import require; require(False, 'control')"],
                                cwd=ROOT, capture_output=True, text=True)
        self.assertNotEqual(result.returncode, 0)
        self.assertIn("ValueError: control", result.stderr)

    def test_native_replay(self):
        replay = e.run()
        for key in ("fixtures", "observations", "reopen_controls"):
            self.assertEqual(replay[key], self.report[key])
        self.assertEqual(routing.run(), self.routing)


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