#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
"""Run beside bzip2-salvage.py: python3 test_bzip2_salvage.py."""
import hashlib
import importlib.util
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

SCRIPT = Path(__file__).with_name("bzip2-salvage.py")
sys.dont_write_bytecode = True
spec = importlib.util.spec_from_file_location("salvage", SCRIPT)
salvage = importlib.util.module_from_spec(spec)
spec.loader.exec_module(salvage)


class SalvageControls(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.receipt = salvage.run()
        cls.rows = {row["id"]: row for row in cls.receipt["observations"]}

    def test_fixed_input(self):
        self.assertEqual(len(salvage.payload()), 320000)
        self.assertEqual(hashlib.sha256(salvage.payload()).hexdigest(),
                         "cd8731bd94345ed1a83e701a313402c21ca0f90817a434e0e7046ff4fdb00a1f")

    def test_bit_mutation_is_exact(self):
        for bit in range(24):
            before = bytes.fromhex("123456")
            after = salvage.flip_bit(before, bit)
            self.assertEqual(sum((a ^ b).bit_count() for a, b in zip(before, after)), 1)
            self.assertEqual(salvage.flip_bit(after, bit), before)
        for bit in (-1, 24):
            with self.assertRaises(ValueError):
                salvage.flip_bit(before, bit)

    def test_pristine_oracle_is_contiguous(self):
        blocks = self.receipt["referenceBlocks"]
        self.assertEqual(blocks[0]["startByte"], 0)
        self.assertEqual(blocks[-1]["endByteExclusive"], 320000)
        self.assertTrue(all(a["endByteExclusive"] == b["startByte"] for a, b in zip(blocks, blocks[1:])))

    def test_marker_damage_loses_two_candidates_not_two_known_payloads(self):
        self.assertEqual(self.rows["block-marker-bit"]["validatedReferenceBlocks"], [3, 4])
        self.assertEqual(self.rows["block-marker-bit"]["candidateCount"], 3)
        self.assertEqual(self.rows["block-marker-bit"]["validatedBytes"], 120039)

    def test_crc_only_has_complete_salvage_but_failed_original(self):
        row = self.rows["stream-crc-bit"]
        self.assertEqual(row["wholeIntegrityExit"], 2)
        self.assertFalse(row["wholeAccepted"])
        self.assertTrue(row["concatenationMatchesOriginal"])

    def test_validation_exit_is_not_recovery_exit(self):
        for row in self.rows.values():
            self.assertEqual(row["recoveryExit"], 0)
        self.assertTrue(any(not c["validationPassed"] for c in self.rows["block-data-bit"]["candidates"]))

    def test_every_byte_is_accounted_for(self):
        for row in self.rows.values():
            self.assertEqual(row["validatedBytes"] + sum(b - a for a, b in row["missingByteRanges"]), 320000)

    def test_deterministic_rerun(self):
        repeated = salvage.run()
        repeated.pop("generatedAt")
        first = self.receipt.copy()
        first.pop("generatedAt")
        self.assertEqual(first, repeated)

    def test_refuses_existing_result(self):
        with tempfile.TemporaryDirectory() as temp:
            target = Path(temp) / "keep.json"
            target.write_bytes(b"do not replace")
            result = subprocess.run([sys.executable, str(SCRIPT), "--output", str(target)], capture_output=True, timeout=5)
            self.assertNotEqual(result.returncode, 0)
            self.assertEqual(target.read_bytes(), b"do not replace")

    def test_refuses_existing_symlink(self):
        with tempfile.TemporaryDirectory() as temp:
            target = Path(temp) / "keep"
            target.write_bytes(b"keep")
            link = Path(temp) / "link.json"
            link.symlink_to(target)
            result = subprocess.run([sys.executable, str(SCRIPT), "--output", str(link)], capture_output=True, timeout=5)
            self.assertNotEqual(result.returncode, 0)
            self.assertEqual(target.read_bytes(), b"keep")


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