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


SCRIPT_DIR = Path(__file__).resolve().parent
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
BUILDER = (
    SCRIPT_DIR
    / "build-device-plane-postgres-bootstrap-artifact.mjs"
)
EXPECTED_ENTRIES = [
    "docker-compose.device-plane.yml",
    "deployment/device-postgres-bootstrap-v1.json",
]


def load_runner():
    loader = importlib.machinery.SourceFileLoader(
        "nodedc_device_plane_postgres_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 DevicePlanePostgresBootstrapTest(unittest.TestCase):
    def build(self, artifact_dir, patch_id):
        environment = os.environ.copy()
        environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
        result = subprocess.run(
            ["node", str(BUILDER), patch_id],
            check=True,
            capture_output=True,
            text=True,
            env=environment,
        )
        return json.loads(result.stdout)

    def test_bootstrap_artifact_is_exact_deterministic_and_runner_accepted(self):
        with tempfile.TemporaryDirectory(
            prefix="nodedc-device-plane-postgres-artifact-",
        ) as directory:
            artifact_dir = Path(directory)
            first = self.build(
                artifact_dir,
                "device-plane-postgres-bootstrap-unit-001",
            )
            artifact = Path(first["artifact"])
            first_bytes = artifact.read_bytes()
            second = self.build(
                artifact_dir,
                "device-plane-postgres-bootstrap-unit-001",
            )
            second_bytes = Path(second["artifact"]).read_bytes()

            self.assertEqual(first["entries"], EXPECTED_ENTRIES)
            self.assertEqual(first["services"], ["device-postgres"])
            self.assertEqual(first["mode"], "create-if-absent")
            self.assertEqual(first["rollbackVolumePolicy"], "preserve")
            self.assertEqual(
                first["sha256"],
                hashlib.sha256(first_bytes).hexdigest(),
            )
            self.assertEqual(first_bytes, second_bytes)

            with tarfile.open(artifact, "r:gz") as archive:
                names = {member.name for member in archive.getmembers()}
                self.assertEqual(
                    archive.extractfile("files.txt")
                    .read()
                    .decode("utf-8")
                    .splitlines(),
                    EXPECTED_ENTRIES,
                )
            self.assertEqual(
                names,
                {
                    "manifest.env",
                    "files.txt",
                    "payload",
                    "payload/docker-compose.device-plane.yml",
                    "payload/deployment",
                    "payload/deployment/device-postgres-bootstrap-v1.json",
                },
            )

            with tempfile.TemporaryDirectory(
                prefix="nodedc-device-plane-postgres-load-",
            ) as work_directory:
                manifest, entries, _payload = RUNNER.load_artifact(
                    artifact,
                    Path(work_directory),
                )
            self.assertEqual(manifest["component"], "device-plane")
            self.assertEqual(entries, EXPECTED_ENTRIES)
            self.assertTrue(
                RUNNER.is_device_plane_postgres_bootstrap_slice(
                    manifest["component"],
                    entries,
                ),
            )
            self.assertEqual(
                RUNNER.component_services("device-plane", entries),
                ("device-postgres",),
            )
            self.assertEqual(
                RUNNER.component_builds("device-plane", entries),
                (),
            )

    def test_preflight_accepts_only_absent_container_and_volume(self):
        absent_container = mock.Mock(returncode=0, stdout="")
        absent_volume = mock.Mock(returncode=1)
        with mock.patch.object(
            RUNNER.subprocess,
            "run",
            side_effect=[absent_container, absent_volume],
        ):
            self.assertEqual(
                RUNNER.preflight_device_plane_postgres_bootstrap(),
                "absent",
            )

        existing_container = mock.Mock(
            returncode=0,
            stdout="abc123def456\n",
        )
        with mock.patch.object(
            RUNNER.subprocess,
            "run",
            return_value=existing_container,
        ):
            with self.assertRaisesRegex(
                RUNNER.DeployError,
                "container already exists",
            ):
                RUNNER.preflight_device_plane_postgres_bootstrap()

        with mock.patch.object(
            RUNNER.subprocess,
            "run",
            side_effect=[
                absent_container,
                mock.Mock(returncode=0),
            ],
        ):
            with self.assertRaisesRegex(
                RUNNER.DeployError,
                "volume already exists",
            ):
                RUNNER.preflight_device_plane_postgres_bootstrap()

    def test_plan_selection_is_unambiguous_for_bootstrap_and_application(self):
        self.assertEqual(
            RUNNER.device_plane_postgres_plan_selection(None),
            "preserved-prerequisite:not-selected",
        )
        self.assertEqual(
            RUNNER.device_plane_postgres_plan_selection("absent"),
            "bootstrap-selected:create-if-absent",
        )
        with self.assertRaisesRegex(
            RUNNER.DeployError,
            "plan preflight state is invalid",
        ):
            RUNNER.device_plane_postgres_plan_selection("unknown")

    def test_bootstrap_health_acceptance_is_database_service_only(self):
        with mock.patch.object(
            RUNNER,
            "healthcheck_compose_service",
        ) as health:
            RUNNER.run_healthchecks(
                "device-plane",
                EXPECTED_ENTRIES,
                ("device-postgres",),
            )
        health.assert_called_once_with(
            "device-plane",
            "device-postgres",
        )

        with self.assertRaisesRegex(
            RUNNER.DeployError,
            "service set mismatch",
        ):
            RUNNER.run_healthchecks(
                "device-plane",
                EXPECTED_ENTRIES,
                ("device-control-core",),
            )


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