from __future__ import annotations

import hashlib
import json
import shutil
import subprocess
import sys
import tarfile
import tempfile
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
CREATE = ROOT / "release-tools" / "verifier-bundle" / "create_verifier_bundle.py"
VERIFY = ROOT / "release-tools" / "verifier-bundle" / "verify_verifier_bundle.py"
ROOT_NAME = "vaultfire-verifier-bundle"
SOURCE_COMMIT = "57f9fe81d7bd7eb9953822daba79797be7e1c2ee"
EVIDENCE_COMMIT = "f0815778912bb832411d26bdc009c5b9ee10375a"


def invoke(*args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run([sys.executable, *args], cwd=ROOT, text=True, capture_output=True, check=False)


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def refresh_checksums(bundle_root: Path) -> None:
    checksum_file = bundle_root / "manifest" / "files.sha256"
    records = []
    for path in sorted(bundle_root.rglob("*")):
        if path.is_file() and path != checksum_file:
            records.append(f"{sha256(path)}  {path.relative_to(bundle_root).as_posix()}")
    checksum_file.write_text("\n".join(records) + "\n", encoding="utf-8")


def rewrite_archive(artifact: Path, mutator, refresh: bool = False) -> Path:
    temporary = Path(tempfile.mkdtemp(prefix="vaultfire-verifier-bundle-test-"))
    with tarfile.open(artifact, "r:gz") as tar:
        tar.extractall(temporary, filter="data")
    bundle_root = temporary / ROOT_NAME
    mutator(bundle_root)
    if refresh:
        refresh_checksums(bundle_root)
    altered = temporary / "altered.tar.gz"
    with tarfile.open(altered, "w:gz", format=tarfile.PAX_FORMAT) as tar:
        tar.add(bundle_root, arcname=ROOT_NAME)
    return altered


class VerifierBundleTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.temporary = tempfile.TemporaryDirectory(prefix="vaultfire-verifier-bundle-test-")
        cls.bundle = Path(cls.temporary.name) / "verifier-bundle.tar.gz"
        cls.bundle_repeat = Path(cls.temporary.name) / "verifier-bundle-repeat.tar.gz"
        args = (
            str(CREATE),
            "--repo",
            str(ROOT),
            "--source-commit",
            SOURCE_COMMIT,
            "--evidence-commit",
            EVIDENCE_COMMIT,
        )
        created = invoke(*args, "--output", str(cls.bundle))
        if created.returncode:
            raise RuntimeError(f"failed to create verifier test artifact:\n{created.stdout}\n{created.stderr}")
        repeated = invoke(*args, "--output", str(cls.bundle_repeat))
        if repeated.returncode:
            raise RuntimeError(f"failed to create repeated verifier test artifact:\n{repeated.stdout}\n{repeated.stderr}")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.temporary.cleanup()

    def test_complete_vendored_bundle_structurally_verifies(self) -> None:
        checked = invoke(str(VERIFY), "--allow-incomplete", str(self.bundle))
        self.assertEqual(checked.returncode, 0, checked.stderr)
        self.assertIn("structural verification: PASS", checked.stdout)
        self.assertIn("SafeERC20", self._bundle_listing())

    def test_exact_source_and_evidence_commits_are_recorded(self) -> None:
        manifest = self._manifest()
        self.assertEqual(manifest["format"], "vaultfire-verifier-bundle/v2")
        self.assertEqual(manifest["source"]["repository_commit"], SOURCE_COMMIT)
        self.assertEqual(manifest["evidence"]["repository_commit"], EVIDENCE_COMMIT)
        self.assertEqual(manifest["evidence"]["direct_parent"], SOURCE_COMMIT)
        self.assertEqual(manifest["source"]["git_bundle"], "git-bundles/source.bundle")
        self.assertEqual(manifest["evidence"]["git_bundle"], "git-bundles/evidence.bundle")

    def test_repeated_generation_is_byte_identical(self) -> None:
        self.assertEqual(sha256(self.bundle), sha256(self.bundle_repeat))
        self.assertEqual(self.bundle.read_bytes(), self.bundle_repeat.read_bytes())

    def test_tampered_vendored_file_is_rejected(self) -> None:
        altered = rewrite_archive(
            self.bundle,
            lambda root: (root / "source/v3-src/contracts/bonds/AccountabilityBondV3Safe.sol").write_text(
                "// tampered\n", encoding="utf-8"
            ),
        )
        checked = invoke(str(VERIFY), "--allow-incomplete", str(altered))
        self.assertNotEqual(checked.returncode, 0)
        self.assertIn("SHA-256 mismatch", checked.stderr)

    def test_missing_safe_erc20_dependency_is_rejected(self) -> None:
        altered = rewrite_archive(
            self.bundle,
            lambda root: (root / "source/v3-src/lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol").unlink(),
        )
        checked = invoke(str(VERIFY), "--allow-incomplete", str(altered))
        self.assertNotEqual(checked.returncode, 0)
        self.assertTrue(
            "checksum manifest does not describe exactly" in checked.stderr
            or "required verifier file is missing" in checked.stderr,
            checked.stderr,
        )

    def test_falsified_evidence_parent_is_rejected_after_rehash(self) -> None:
        def falsify_parent(root: Path) -> None:
            manifest_path = root / "manifest" / "manifest.json"
            manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
            manifest["evidence"]["direct_parent"] = "0" * 40
            manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")

        altered = rewrite_archive(self.bundle, falsify_parent, refresh=True)
        checked = invoke(str(VERIFY), "--allow-incomplete", str(altered))
        self.assertNotEqual(checked.returncode, 0)
        self.assertIn("evidence manifest direct parent does not equal source commit", checked.stderr)

    def _bundle_listing(self) -> str:
        with tarfile.open(self.bundle, "r:gz") as tar:
            return "\n".join(member.name for member in tar.getmembers())

    def _manifest(self) -> dict:
        with tarfile.open(self.bundle, "r:gz") as tar:
            member = tar.getmember(f"{ROOT_NAME}/manifest/manifest.json")
            extracted = tar.extractfile(member)
            self.assertIsNotNone(extracted)
            return json.loads(extracted.read().decode("utf-8"))


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