"""Determinism tests — prove that identical input produces identical output."""
import json
from pathlib import Path

from abi_spec.canonical import canonical_json
from abi_spec.compiler import compile_prd_json_to_specpack

FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"


class TestCompileDeterminism:
    def test_same_input_same_hash_100_times(self, tmp_path):
        """Compiling the same PRD 100 times must produce the same hashes."""
        hashes = set()
        for i in range(100):
            out = tmp_path / f"out_{i}"
            result = compile_prd_json_to_specpack(FIXTURES / "prd.json", out)
            hashes.add(result["manifest_hash"])
        assert len(hashes) == 1

    def test_same_input_same_prd_hash(self, tmp_path):
        """PRD hash must be stable across compilations."""
        hashes = set()
        for i in range(10):
            out = tmp_path / f"out_{i}"
            result = compile_prd_json_to_specpack(FIXTURES / "prd.json", out)
            hashes.add(result["prd_hash"])
        assert len(hashes) == 1

    def test_output_files_byte_identical(self, tmp_path):
        """Two compilations must produce byte-identical output files."""
        out1 = tmp_path / "out1"
        out2 = tmp_path / "out2"
        compile_prd_json_to_specpack(FIXTURES / "prd.json", out1)
        compile_prd_json_to_specpack(FIXTURES / "prd.json", out2)
        for filename in ("spec_manifest.json", "hashes.json"):
            b1 = (out1 / filename).read_bytes()
            b2 = (out2 / filename).read_bytes()
            assert b1 == b2, f"{filename} not byte-identical"


class TestKeyOrderInsensitive:
    def test_prd_key_order_does_not_affect_hash(self, tmp_path):
        """PRD with reordered keys must produce the same hash."""
        prd_a = {
            "requirements": [
                {"id": "REQ-001", "text": "Test", "acceptance": ["OK"]}
            ]
        }
        prd_b = {
            "requirements": [
                {"acceptance": ["OK"], "text": "Test", "id": "REQ-001"}
            ]
        }
        prd_a_path = tmp_path / "a.json"
        prd_b_path = tmp_path / "b.json"
        prd_a_path.write_text(json.dumps(prd_a), encoding="utf-8")
        prd_b_path.write_text(json.dumps(prd_b), encoding="utf-8")

        out_a = tmp_path / "out_a"
        out_b = tmp_path / "out_b"
        r_a = compile_prd_json_to_specpack(prd_a_path, out_a)
        r_b = compile_prd_json_to_specpack(prd_b_path, out_b)
        assert r_a["prd_hash"] == r_b["prd_hash"]
        assert r_a["manifest_hash"] == r_b["manifest_hash"]


class TestTimestampExclusion:
    """Prove that the SpecPack output contains no timestamp-like keys.

    Backs the guarantee in docs/DETERMINISM_MODEL.md:
    'No timestamps in hashed payload.'
    """

    TIMESTAMP_KEYS = {"timestamp", "timestamps", "created_at", "time", "ts"}

    def _collect_keys_recursive(self, obj, prefix=""):
        """Yield every key path in a nested dict."""
        if isinstance(obj, dict):
            for k, v in obj.items():
                full = f"{prefix}.{k}" if prefix else k
                yield full
                yield from self._collect_keys_recursive(v, full)
        elif isinstance(obj, list):
            for i, item in enumerate(obj):
                yield from self._collect_keys_recursive(item, f"{prefix}[{i}]")

    def test_manifest_has_no_timestamp_keys(self, tmp_path):
        """The spec_manifest.json must not contain timestamp keys."""
        out = tmp_path / "out"
        compile_prd_json_to_specpack(FIXTURES / "prd.json", out)
        manifest = json.loads((out / "spec_manifest.json").read_text(encoding="utf-8"))
        all_keys = {
            k.split(".")[-1]
            for k in self._collect_keys_recursive(manifest)
        }
        found = all_keys & self.TIMESTAMP_KEYS
        assert not found, f"Timestamp-like keys found in manifest: {found}"

    def test_hashes_json_has_no_timestamp_keys(self, tmp_path):
        """hashes.json must not contain timestamp keys."""
        out = tmp_path / "out"
        compile_prd_json_to_specpack(FIXTURES / "prd.json", out)
        hashes = json.loads((out / "hashes.json").read_text(encoding="utf-8"))
        all_keys = {
            k.split(".")[-1]
            for k in self._collect_keys_recursive(hashes)
        }
        found = all_keys & self.TIMESTAMP_KEYS
        assert not found, f"Timestamp-like keys found in hashes.json: {found}"

    def test_hash_identical_when_prd_has_extra_timestamps(self, tmp_path):
        """Out-of-band timestamp metadata in PRD must not leak into manifest keys."""
        prd_base = {
            "requirements": [
                {"id": "REQ-001", "text": "Test requirement", "acceptance": ["Passes"]}
            ]
        }
        # PRD with timestamp metadata (gets hashed into prd_hash but should not
        # leak into manifest structure)
        prd_with_ts = {
            "requirements": [
                {"id": "REQ-001", "text": "Test requirement", "acceptance": ["Passes"]}
            ],
            "timestamp": "2026-01-01T00:00:00Z",
        }

        prd_a_path = tmp_path / "a.json"
        prd_b_path = tmp_path / "b.json"
        prd_a_path.write_text(json.dumps(prd_base), encoding="utf-8")
        prd_b_path.write_text(json.dumps(prd_with_ts), encoding="utf-8")

        out_a = tmp_path / "out_a"
        out_b = tmp_path / "out_b"
        compile_prd_json_to_specpack(prd_a_path, out_a)
        compile_prd_json_to_specpack(prd_b_path, out_b)

        # Manifest structure must remain free of timestamp keys in both
        for out in (out_a, out_b):
            manifest = json.loads(
                (out / "spec_manifest.json").read_text(encoding="utf-8")
            )
            all_keys = {
                k.split(".")[-1]
                for k in self._collect_keys_recursive(manifest)
            }
            found = all_keys & self.TIMESTAMP_KEYS
            assert not found, f"Timestamp keys leaked into manifest: {found}"

    def test_canonical_json_excludes_timestamps(self):
        """Canonical JSON of the manifest-like dict has no timestamp fields."""
        manifest_like = {
            "spec_version": "1.0",
            "spec_id": "spec-test",
            "prd_hash": "a" * 64,
            "requirements": [{"id": "REQ-001", "text": "t", "acceptance": ["ok"]}],
        }
        serialized = canonical_json(manifest_like)
        payload = json.loads(serialized)
        all_keys = {
            k.split(".")[-1]
            for k in self._collect_keys_recursive(payload)
        }
        found = all_keys & self.TIMESTAMP_KEYS
        assert not found, f"Timestamp keys in canonical payload: {found}"

    def test_stable_hash_without_timestamps(self, tmp_path):
        """Same PRD without timestamps produces identical hashes across runs."""
        results = []
        for i in range(10):
            out = tmp_path / f"out_{i}"
            r = compile_prd_json_to_specpack(FIXTURES / "prd.json", out)
            results.append(r["manifest_hash"])
        assert len(set(results)) == 1
