from __future__ import annotations

import copy
import json
import os
import stat
import subprocess
import sys
import tarfile
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest import mock

from common import ROOT as PACKAGE_ROOT

from scripts import build_release
from scripts.verify_release import _strict_json_loads as release_strict_json_loads

VERSION = (PACKAGE_ROOT / "VERSION").read_text(encoding="utf-8").strip()
PACKAGE_NAME = f"codex-multimodel-orchestrator-{VERSION}"


class ReleaseIntegrityTests(unittest.TestCase):
    def test_release_manifest_json_rejects_ambiguous_members_and_invalid_unicode(self) -> None:
        for payload in (
            '{"schema_version": 2, "schema_version": 1}',
            '{"path": "\\ud800"}',
            '{"size": 1e400}',
        ):
            with self.subTest(payload=payload), self.assertRaises(ValueError):
                release_strict_json_loads(payload)

    def test_archive_set_publication_rolls_back_as_a_unit(self) -> None:
        with tempfile.TemporaryDirectory(prefix="mmo-release-publish-") as temporary_raw:
            root = Path(temporary_raw)
            source_paths = tuple(root / f"source-{index}" for index in range(4))
            final_paths = tuple(root / f"final-{index}" for index in range(4))
            for index, path in enumerate(source_paths):
                path.write_bytes(f"new {index}".encode())
            for index, path in enumerate(final_paths):
                path.write_bytes(f"old {index}".encode())
            real_replace = os.replace

            def fail_metadata_publish(
                source: str | os.PathLike[str], destination: str | os.PathLike[str]
            ) -> None:
                source_path = Path(source)
                destination_path = Path(destination)
                if destination_path == final_paths[2] and ".publish-" in source_path.name:
                    raise OSError("synthetic metadata publication failure")
                real_replace(source, destination)

            with mock.patch.object(build_release.os, "replace", side_effect=fail_metadata_publish):
                with self.assertRaisesRegex(OSError, "metadata publication"):
                    build_release._publish_artifacts(
                        tuple(zip(source_paths, final_paths, strict=True))
                    )
            for index, path in enumerate(final_paths):
                self.assertEqual(path.read_bytes(), f"old {index}".encode())
            self.assertFalse(any(path.name.startswith(".final") for path in root.iterdir()))

            symlink_target = root / "symlink-target"
            symlink_target.write_bytes(b"must remain untouched")
            symlink_destination = root / "symlink-destination"
            symlink_destination.symlink_to(symlink_target)
            with self.assertRaisesRegex(RuntimeError, "non-regular release artifact"):
                build_release._publish_artifacts(((source_paths[0], symlink_destination),))
            self.assertTrue(symlink_destination.is_symlink())
            self.assertEqual(symlink_target.read_bytes(), b"must remain untouched")

            def interrupt_final_verification() -> None:
                raise KeyboardInterrupt

            with self.assertRaises(KeyboardInterrupt):
                build_release._publish_artifacts(
                    tuple(zip(source_paths, final_paths, strict=True)),
                    verify=interrupt_final_verification,
                )
            for index, path in enumerate(final_paths):
                self.assertEqual(path.read_bytes(), f"old {index}".encode())

            def fail_publish_and_restore(
                source: str | os.PathLike[str], destination: str | os.PathLike[str]
            ) -> None:
                source_path = Path(source)
                destination_path = Path(destination)
                if destination_path == final_paths[2] and ".publish-" in source_path.name:
                    raise OSError("synthetic publication failure")
                if destination_path == final_paths[0] and ".backup-" in source_path.name:
                    raise OSError("synthetic restoration failure")
                real_replace(source, destination)

            with mock.patch.object(
                build_release.os,
                "replace",
                side_effect=fail_publish_and_restore,
            ):
                with self.assertRaisesRegex(RuntimeError, "rollback was incomplete"):
                    build_release._publish_artifacts(
                        tuple(zip(source_paths, final_paths, strict=True))
                    )
            retained = list(root.glob(".final-0.backup-*"))
            self.assertEqual(len(retained), 1)
            self.assertEqual(retained[0].read_bytes(), b"old 0")
            final_paths[0].write_bytes(retained[0].read_bytes())
            retained[0].unlink()

            def fail_final_verification() -> None:
                raise RuntimeError("synthetic final verification failure")

            with self.assertRaisesRegex(RuntimeError, "final verification"):
                build_release._publish_artifacts(
                    tuple(zip(source_paths, final_paths, strict=True)),
                    verify=fail_final_verification,
                )
            for index, path in enumerate(final_paths):
                self.assertEqual(path.read_bytes(), f"old {index}".encode())
            self.assertFalse(any(path.name.startswith(".final") for path in root.iterdir()))

    def test_release_builder_publishes_complete_verified_archives(self) -> None:
        with tempfile.TemporaryDirectory(prefix="mmo-release-test-") as temporary_raw:
            output = Path(temporary_raw)
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "build_release.py"),
                    "--output-dir",
                    str(output),
                    "--skip-validation",
                    "--no-reproducibility-check",
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=120,
            )
            self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
            tar_path = output / f"{PACKAGE_NAME}-linux.tar.gz"
            zip_path = output / f"{PACKAGE_NAME}-linux.zip"
            integrity_path = output / f"{PACKAGE_NAME}-INTEGRITY.json"
            for path in (tar_path, zip_path, integrity_path):
                self.assertTrue(path.is_file(), path)
                self.assertGreater(path.stat().st_size, 1_000)

            verify = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
                    "--source-tree",
                    str(PACKAGE_ROOT),
                    str(tar_path),
                    str(zip_path),
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=120,
            )
            self.assertEqual(verify.returncode, 0, verify.stderr or verify.stdout)
            integrity = json.loads(integrity_path.read_text(encoding="utf-8"))
            self.assertGreaterEqual(integrity["source_files"], 100)
            self.assertEqual(len(integrity["archives"]), 2)
            self.assertEqual(
                {item["manifest_files"] for item in integrity["archives"]},
                {integrity["source_files"]},
            )

    def test_builder_rejects_output_inside_source_tree(self) -> None:
        output = PACKAGE_ROOT / ".forbidden-release-output"
        self.assertFalse(output.exists())
        result = subprocess.run(
            [
                sys.executable,
                str(PACKAGE_ROOT / "scripts" / "build_release.py"),
                "--output-dir",
                str(output),
                "--skip-validation",
                "--no-reproducibility-check",
            ],
            cwd=PACKAGE_ROOT,
            text=True,
            capture_output=True,
            check=False,
            timeout=60,
        )
        self.assertNotEqual(result.returncode, 0)
        self.assertIn("inside the source tree", result.stderr + result.stdout)
        self.assertFalse(output.exists())

    def test_release_verifier_rejects_tampering_and_wrong_root(self) -> None:
        package = PACKAGE_NAME
        with tempfile.TemporaryDirectory(prefix="mmo-release-tamper-") as temporary_raw:
            output = Path(temporary_raw)
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "build_release.py"),
                    "--output-dir",
                    str(output),
                    "--skip-validation",
                    "--no-reproducibility-check",
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=120,
            )
            self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
            original = output / f"{package}-linux.zip"

            tampered = output / "tampered.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(tampered, "w", compression=zipfile.ZIP_DEFLATED) as destination,
            ):
                for info in source.infolist():
                    data = source.read(info.filename)
                    if info.filename == f"{package}/README.md":
                        data += b"\nTAMPERED\n"
                    destination.writestr(copy.copy(info), data)

            wrong_root = output / "wrong-root.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(wrong_root, "w", compression=zipfile.ZIP_DEFLATED) as destination,
            ):
                for info in source.infolist():
                    renamed = copy.copy(info)
                    renamed.filename = info.filename.replace(package, "wrong-package-root", 1)
                    destination.writestr(renamed, source.read(info.filename))

            mode_drift = output / "mode-drift.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(mode_drift, "w", compression=zipfile.ZIP_DEFLATED) as destination,
            ):
                for info in source.infolist():
                    changed = copy.copy(info)
                    if info.filename == f"{package}/README.md":
                        changed.external_attr = (stat.S_IFREG | 0o666) << 16
                    destination.writestr(changed, source.read(info.filename))

            directory_mode_drift = output / "directory-mode-drift.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(
                    directory_mode_drift, "w", compression=zipfile.ZIP_DEFLATED
                ) as destination,
            ):
                for info in source.infolist():
                    changed = copy.copy(info)
                    if info.filename == f"{package}/docs/":
                        changed.external_attr = (stat.S_IFDIR | 0o777) << 16
                    destination.writestr(changed, source.read(info.filename))

            missing_directory = output / "missing-directory-entry.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(
                    missing_directory, "w", compression=zipfile.ZIP_DEFLATED
                ) as destination,
            ):
                for info in source.infolist():
                    if info.filename != f"{package}/docs/":
                        destination.writestr(copy.copy(info), source.read(info.filename))

            extra_directory = output / "extra-empty-directory.zip"
            with (
                zipfile.ZipFile(original) as source,
                zipfile.ZipFile(
                    extra_directory, "w", compression=zipfile.ZIP_DEFLATED
                ) as destination,
            ):
                for info in source.infolist():
                    destination.writestr(copy.copy(info), source.read(info.filename))
                empty = zipfile.ZipInfo(f"{package}/unexpected-empty/")
                empty.create_system = 3
                empty.external_attr = (stat.S_IFDIR | 0o755) << 16
                destination.writestr(empty, b"")

            original_tar = output / f"{package}-linux.tar.gz"
            tar_directory_mode_drift = output / "directory-mode-drift.tar.gz"
            with (
                tarfile.open(original_tar, "r:gz") as source,
                tarfile.open(tar_directory_mode_drift, "w:gz") as destination,
            ):
                for tar_info in source:
                    changed_tar = copy.copy(tar_info)
                    if tar_info.name == f"{package}/docs":
                        changed_tar.mode = 0o777
                    payload = source.extractfile(tar_info) if tar_info.isfile() else None
                    if payload is None:
                        destination.addfile(changed_tar)
                    else:
                        with payload:
                            destination.addfile(changed_tar, payload)

            verify = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
                    str(tampered),
                    str(wrong_root),
                    str(mode_drift),
                    str(directory_mode_drift),
                    str(missing_directory),
                    str(extra_directory),
                    str(tar_directory_mode_drift),
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=120,
            )
            self.assertNotEqual(verify.returncode, 0)
            report = json.loads(verify.stdout)
            self.assertEqual(len(report["errors"]), 7)
            self.assertTrue(any("sha256" in item for item in report["errors"]))
            self.assertTrue(any("top-level directory" in item for item in report["errors"]))
            self.assertTrue(any("mode" in item for item in report["errors"]))
            self.assertEqual(sum("invalid permission mode" in item for item in report["errors"]), 3)
            self.assertEqual(sum("directory set differs" in item for item in report["errors"]), 2)

    def test_release_verifier_rejects_path_traversal(self) -> None:
        with tempfile.TemporaryDirectory(prefix="mmo-release-traversal-") as temporary_raw:
            archive = Path(temporary_raw) / "traversal.zip"
            with zipfile.ZipFile(archive, "w") as handle:
                handle.writestr("../escape.txt", "bad")
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
                    str(archive),
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=60,
            )
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("unsafe archive member path", result.stdout)

    def test_release_verifier_rejects_canonical_path_aliases(self) -> None:
        with tempfile.TemporaryDirectory(prefix="mmo-release-alias-") as temporary_raw:
            archive = Path(temporary_raw) / "alias.zip"
            with zipfile.ZipFile(archive, "w") as handle:
                handle.writestr(f"{PACKAGE_NAME}/nested/file.txt", "first")
                handle.writestr(f"{PACKAGE_NAME}/nested/./file.txt", "second")
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
                    str(archive),
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=60,
            )
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("unsafe archive member path", result.stdout)

    def test_builder_rejects_invalid_source_epoch(self) -> None:
        with tempfile.TemporaryDirectory(prefix="mmo-release-epoch-") as temporary_raw:
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "build_release.py"),
                    "--output-dir",
                    temporary_raw,
                    "--source-date-epoch",
                    "-1",
                    "--skip-validation",
                    "--no-reproducibility-check",
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=60,
            )
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("source-date-epoch", result.stderr + result.stdout)

    def test_one_file_archives_are_rejected(self) -> None:
        package = PACKAGE_NAME
        with tempfile.TemporaryDirectory(prefix="mmo-release-corrupt-") as temporary_raw:
            temporary = Path(temporary_raw)
            zip_path = temporary / "incomplete.zip"
            tar_path = temporary / "incomplete.tar.gz"
            payload = temporary / "PLAN-COVERAGE.md"
            payload.write_text("incomplete\n", encoding="utf-8")
            with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
                archive.write(payload, f"{package}/docs/PLAN-COVERAGE.md")
            with tarfile.open(tar_path, "w:gz") as archive:
                archive.add(payload, arcname=f"{package}/docs/PLAN-COVERAGE.md")
            result = subprocess.run(
                [
                    sys.executable,
                    str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
                    str(zip_path),
                    str(tar_path),
                ],
                cwd=PACKAGE_ROOT,
                text=True,
                capture_output=True,
                check=False,
                timeout=60,
            )
            self.assertNotEqual(result.returncode, 0)
            report = json.loads(result.stdout)
            self.assertFalse(report["passed"])
            self.assertEqual(len(report["errors"]), 2)
            self.assertTrue(
                all("archive directory set differs" in item for item in report["errors"])
            )


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