#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import shutil
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
BUILDER = SCRIPT_DIR / "build-engine-credential-sink-recovery-artifact.mjs"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
SOURCE = Path("/Volumes/docker/nodedc-deploy/applied/nodedc-engine-credential-sink-20260716-001.tgz")
SOURCE_SHA256 = "0a96add05fe59db8f490927f66e07a84490474a7afb3ef7de51e1d6fd96f86a2"
PATCH_ID = "engine-credential-sink-recovery-20990101-001"


def load_runner():
    loader = importlib.machinery.SourceFileLoader(
        "nodedc_recovery_runner_under_test",
        str(RUNNER_PATH),
    )
    spec = importlib.util.spec_from_loader(loader.name, loader)
    module = importlib.util.module_from_spec(spec)
    loader.exec_module(module)
    return module


RUNNER = load_runner()


class EngineCredentialSinkRecoveryArtifactTest(unittest.TestCase):
    def build(self, artifact_dir, patch_id=PATCH_ID, source=SOURCE):
        environment = os.environ.copy()
        environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
        environment["NODEDC_ENGINE_CREDENTIAL_SINK_RECOVERY_SOURCE"] = str(source)
        result = subprocess.run(
            ["node", str(BUILDER), patch_id],
            check=True,
            capture_output=True,
            text=True,
            env=environment,
        )
        return json.loads(result.stdout)

    def test_recovery_changes_only_manifest_id_and_passes_runner_policy(self):
        self.assertTrue(SOURCE.is_file())
        self.assertEqual(hashlib.sha256(SOURCE.read_bytes()).hexdigest(), SOURCE_SHA256)
        with tempfile.TemporaryDirectory(prefix="nodedc-engine-sink-recovery-") as directory:
            root = Path(directory)
            first_dir = root / "first"
            second_dir = root / "second"
            first_dir.mkdir()
            second_dir.mkdir()
            first = self.build(first_dir)
            second = self.build(second_dir)
            first_artifact = Path(first["artifact"])
            second_artifact = Path(second["artifact"])

            self.assertEqual(first["sourceArtifactSha256"], SOURCE_SHA256)
            self.assertEqual(first["changed"], ["manifest.env:id"])
            self.assertEqual(first["sha256"], second["sha256"])
            self.assertEqual(first_artifact.read_bytes(), second_artifact.read_bytes())

            with tarfile.open(SOURCE, "r:gz") as old, tarfile.open(first_artifact, "r:gz") as new:
                old_files = old.extractfile("files.txt").read()
                new_files = new.extractfile("files.txt").read()
                self.assertEqual(old_files, new_files)
                self.assertEqual(
                    new.extractfile("manifest.env").read().decode("utf-8"),
                    f"id={PATCH_ID}\ncomponent=engine\ntype=app-overlay\n",
                )
                for relative_path, expected_sha256 in first["payloadSha256"].items():
                    old_bytes = old.extractfile(f"payload/{relative_path}").read()
                    new_bytes = new.extractfile(f"payload/{relative_path}").read()
                    self.assertEqual(old_bytes, new_bytes)
                    self.assertEqual(hashlib.sha256(new_bytes).hexdigest(), expected_sha256)

            with tempfile.TemporaryDirectory(prefix="nodedc-recovery-runner-") as work:
                manifest, entries, _payload = RUNNER.load_artifact(
                    first_artifact,
                    Path(work),
                )
            self.assertEqual(manifest["id"], PATCH_ID)
            self.assertEqual(tuple(entries), RUNNER.ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES)
            self.assertEqual(RUNNER.component_services("engine", entries), ("nodedc-backend",))

    def test_recovery_rejects_tampered_source_invalid_id_and_overwrite(self):
        with tempfile.TemporaryDirectory(prefix="nodedc-engine-sink-recovery-negative-") as directory:
            root = Path(directory)
            tampered = root / "tampered.tgz"
            shutil.copyfile(SOURCE, tampered)
            with tampered.open("ab") as stream:
                stream.write(b"tampered")

            environment = os.environ.copy()
            environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(root / "artifacts")
            environment["NODEDC_ENGINE_CREDENTIAL_SINK_RECOVERY_SOURCE"] = str(tampered)
            result = subprocess.run(
                ["node", str(BUILDER), PATCH_ID],
                check=False,
                capture_output=True,
                text=True,
                env=environment,
            )
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("source_sha256_mismatch", result.stderr)

            invalid = subprocess.run(
                ["node", str(BUILDER), "engine-credential-sink-20260716-001"],
                check=False,
                capture_output=True,
                text=True,
                env={
                    **os.environ,
                    "NODEDC_DEPLOY_ARTIFACT_DIR": str(root / "invalid"),
                    "NODEDC_ENGINE_CREDENTIAL_SINK_RECOVERY_SOURCE": str(SOURCE),
                },
            )
            self.assertNotEqual(invalid.returncode, 0)
            self.assertIn("fresh-recovery-patch-id", invalid.stderr)

            artifact_dir = root / "duplicate"
            artifact_dir.mkdir()
            self.build(artifact_dir)
            duplicate = subprocess.run(
                ["node", str(BUILDER), PATCH_ID],
                check=False,
                capture_output=True,
                text=True,
                env={
                    **os.environ,
                    "NODEDC_DEPLOY_ARTIFACT_DIR": str(artifact_dir),
                    "NODEDC_ENGINE_CREDENTIAL_SINK_RECOVERY_SOURCE": str(SOURCE),
                },
            )
            self.assertNotEqual(duplicate.returncode, 0)
            self.assertIn("artifact_already_exists", duplicate.stderr)


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