"""Tests for contract lock generation."""

from __future__ import annotations

import subprocess
import sys
import zipfile
from pathlib import Path

from abi_control_core.sdk.lock import generate_lock, serialize_lock

REPO_ROOT = Path(__file__).resolve().parent.parent
LOCK_PATH = REPO_ROOT / "contracts.lock.json"


class TestContractLock:
    def test_lock_lists_all_schemas(self) -> None:
        lock = generate_lock()
        names = [c["name"] for c in lock["contracts"]]
        # Tier-1 schemas
        assert "run_manifest.schema.json" in names
        assert "plan_graph.schema.json" in names
        assert "evidence_pack_manifest.schema.json" in names
        assert "eval_suite.schema.json" in names
        # Tier-2 schemas
        assert "tier2_event_envelope.schema.json" in names
        assert "tier2_pack_manifest.schema.json" in names
        assert "tier2_policy_decision.schema.json" in names
        assert "tier2_runtime_context.schema.json" in names
        assert "tier2_span_envelope.schema.json" in names
        assert "tier2_summary.schema.json" in names

    def test_lock_has_sha256_digests(self) -> None:
        lock = generate_lock()
        for contract in lock["contracts"]:
            assert len(contract["sha256"]) == 64
            assert all(c in "0123456789abcdef" for c in contract["sha256"])

    def test_lock_is_sorted(self) -> None:
        lock = generate_lock()
        names = [c["name"] for c in lock["contracts"]]
        assert names == sorted(names)

    def test_lock_is_deterministic(self) -> None:
        lock1 = generate_lock()
        lock2 = generate_lock()
        assert lock1 == lock2

    def test_lock_no_timestamp(self) -> None:
        lock = generate_lock()
        assert "generated_at" not in lock

    def test_lock_count(self) -> None:
        from abi_control_core.contracts import CONTRACTS_DIR
        expected = len(list(CONTRACTS_DIR.glob("*.schema.json")))
        lock = generate_lock()
        assert len(lock["contracts"]) == expected


class TestLockByteStability:
    """Prove that regenerating the lock produces identical bytes."""

    def test_regeneration_matches_committed_file(self) -> None:
        """Regenerating the lock must produce bytes identical to the committed file."""
        assert LOCK_PATH.exists(), "contracts.lock.json must exist"
        committed = LOCK_PATH.read_text(encoding="utf-8")
        fresh = serialize_lock(generate_lock())
        assert committed == fresh, (
            "contracts.lock.json is not byte-stable — "
            "regenerate with: abi-core-lock"
        )

    def test_serialize_is_idempotent(self) -> None:
        """Serializing the same lock data twice produces identical strings."""
        lock = generate_lock()
        assert serialize_lock(lock) == serialize_lock(lock)


class TestWheelContents:
    """Prove the wheel includes contracts.lock.json."""

    def test_wheel_includes_lock_file(self, tmp_path: Path) -> None:
        """Build a wheel and verify contracts.lock.json is inside."""
        result = subprocess.run(
            [sys.executable, "-m", "build", "--wheel", "--outdir", str(tmp_path)],
            cwd=str(REPO_ROOT),
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            # Fall back to pip wheel if python-build is not installed
            result = subprocess.run(
                [
                    sys.executable, "-m", "pip", "wheel",
                    "--no-deps", "--wheel-dir", str(tmp_path), str(REPO_ROOT),
                ],
                capture_output=True,
                text=True,
            )
        assert result.returncode == 0, f"Wheel build failed:\n{result.stderr}"

        wheels = list(tmp_path.glob("*.whl"))
        assert len(wheels) == 1, f"Expected 1 wheel, found {len(wheels)}"

        with zipfile.ZipFile(wheels[0]) as zf:
            names = zf.namelist()
            lock_entries = [n for n in names if n.endswith("contracts.lock.json")]
            assert lock_entries, (
                "contracts.lock.json not found in wheel. Entries:\n"
                + "\n".join(sorted(names))
            )
