# SPDX-License-Identifier: MIT
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

import observe


class MapTests(unittest.TestCase):
    def test_complete_map(self):
        blocks = observe.parse_map("# heading\n0 + 1\n0 010000 +\n")
        self.assertEqual(blocks, [{"offset": 0, "length": 4096, "status": "+"}])

    def test_gap_overlap_truncation_and_alignment(self):
        for body in ("0 512 +\n1024 3072 +", "0 1024 +\n512 3584 +", "0 512 +",
                     "0 0 +\n0 4096 +", "0 8192 +", "0 1 +\n1 4095 +"):
            with self.subTest(body=body), self.assertRaises(ValueError):
                observe.parse_map("0 ? 1\n" + body)

    def test_invalid_headers_and_numbers(self):
        for text in ("", "# only comments", "0 ++ 1\n0 4096 +", "0 + x\n0 4096 +",
                     "0 + 1\n0 4096 q", "0 + 1\n-1 4096 +", "0 + 1\n0 09 +"):
            with self.subTest(text=text), self.assertRaises(ValueError):
                observe.parse_map(text)

    def test_all_five_block_types_are_preserved(self):
        text = "0 ? 1\n0 512 ?\n512 512 *\n1024 512 /\n1536 512 -\n2048 2048 +\n"
        self.assertEqual(observe.map_sectors(observe.parse_map(text)), list("?*/-++++"))


class ReceiptTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.receipt = json.loads(Path(__file__).with_name("results.json").read_text())

    def test_recorded_receipt(self):
        self.assertTrue(observe.validate(self.receipt))

    def test_missing_duplicate_and_unexpected_cases(self):
        for kind in ("missing", "duplicate", "unexpected"):
            data = copy.deepcopy(self.receipt)
            if kind == "missing":
                data["cases"].pop()
            elif kind == "duplicate":
                data["cases"][-1] = data["cases"][0]
            else:
                data["cases"][0]["id"] = "not-a-case"
            with self.subTest(kind=kind), self.assertRaises(ValueError):
                observe.validate(data)

    def test_misleading_image_and_coverage_evidence(self):
        mutations = {
            "imageSha256": "0" * 64, "imageBytes": 4095, "sectorSha256": [],
            "matchesSourceBySector": [True] * 8, "matchingSourceBytes": 4096,
            "zeroSectors": [], "mapBlocks": [], "mapStatusBySector": ["+"] * 8,
            "finishedBytes": 4096, "mapText": "0 + 1\n0 4096 +\n",
        }
        for field, value in mutations.items():
            data = copy.deepcopy(self.receipt)
            data["cases"][0][field] = value
            with self.subTest(field=field), self.assertRaises(ValueError):
                observe.validate(data)

    def test_subprocess_failure_is_not_evidence(self):
        for operation, bad_status in (("operation", 1), ("doneStatus", 0), ("firstSectorDoneStatus", 1)):
            data = copy.deepcopy(self.receipt)
            data["cases"][0][operation]["returncode"] = bad_status
            with self.subTest(operation=operation), self.assertRaises(ValueError):
                observe.validate(data)

    def test_wrong_source_is_rejected(self):
        data = copy.deepcopy(self.receipt)
        data["source"]["sha256"] = "0" * 64
        with self.assertRaises(ValueError):
            observe.validate(data)

    def test_same_digest_different_acquisition_coverage(self):
        cases = {c["id"]: c for c in self.receipt["cases"]}
        self.assertEqual(cases["fresh-bad-zero"]["imageSha256"], cases["fresh-healthy"]["imageSha256"])
        self.assertNotEqual(cases["fresh-bad-zero"]["mapBlocks"], cases["fresh-healthy"]["mapBlocks"])

    def test_resume_repairs_only_unfinished_sector(self):
        cases = {c["id"]: c for c in self.receipt["cases"]}
        self.assertEqual(cases["sparse-reused-bad-data"]["matchesSourceBySector"], [True, False, False, True, True, True, True, True])
        self.assertEqual(cases["resume-sparse-reused"]["matchesSourceBySector"], [True, False, True, True, True, True, True, True])
        self.assertEqual(cases["resume-sparse-reused"]["doneStatus"]["returncode"], 0)

    def test_optimized_python_keeps_validation(self):
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "invalid.json"
            data = copy.deepcopy(self.receipt)
            data["cases"][0]["finishedBytes"] = 4096
            path.write_text(json.dumps(data))
            command = "import json,sys,observe; observe.validate(json.load(open(sys.argv[1])))"
            for flag in ("-O", "-OO"):
                result = subprocess.run([sys.executable, flag, "-c", command, str(path)],
                                        cwd=Path(__file__).parent, capture_output=True, text=True)
                self.assertNotEqual(result.returncode, 0)
                self.assertIn("finished count", result.stderr)


class RuntimeTests(unittest.TestCase):
    @unittest.skipUnless(os.environ.get("DDRESCUE_BIN") and os.environ.get("DDRESCUELOG_BIN"), "set executable paths for live reproduction")
    def test_live_reproduction(self):
        receipt = observe.observe(Path(os.environ["DDRESCUE_BIN"]), Path(os.environ["DDRESCUELOG_BIN"]))
        recorded = json.loads(Path(__file__).with_name("results.json").read_text())
        self.assertEqual(observe.stable_observations(receipt), observe.stable_observations(recorded))


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