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


SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
BUILDER_PATH = SCRIPT_DIR / "build-ops-mcp-workspace-tools-artifacts.mjs"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
SOURCE_ROOT = SCRIPT_DIR / "ops-mcp-workspace-tools"
RELEASE = json.loads((SOURCE_ROOT / "release.json").read_text(encoding="utf-8"))


def load_runner():
    loader = importlib.machinery.SourceFileLoader("nodedc_ops_workspace_tools_runner", 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 OpsMcpWorkspaceToolsArtifactTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.temporary = tempfile.TemporaryDirectory(prefix="nodedc-ops-mcp-workspace-tools-")
        cls.root = Path(cls.temporary.name)
        cls.builds = []
        for index in range(2):
            output = cls.root / ("build-" + str(index))
            output.mkdir()
            env = os.environ.copy()
            env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output)
            result = subprocess.run(
                ["node", str(BUILDER_PATH), RELEASE["release"]],
                cwd=PLATFORM_ROOT,
                env=env,
                check=True,
                capture_output=True,
                text=True,
            )
            cls.builds.append(json.loads(result.stdout))

    @classmethod
    def tearDownClass(cls):
        cls.temporary.cleanup()

    def component_result(self, build_index, component):
        return next(
            result
            for result in self.builds[build_index]["results"]
            if result["component"] == component
        )

    def test_builder_updates_existing_components_without_runner_change(self):
        self.assertEqual(self.builds[0]["deployOrder"], ["tasker", "ops-agents"])
        self.assertFalse(self.builds[0]["runnerChanged"])
        self.assertEqual(set(RELEASE["components"]), {"tasker", "ops-agents"})

    def test_artifacts_are_byte_reproducible_and_runner_accepted(self):
        for component, descriptor in RELEASE["components"].items():
            first = Path(self.component_result(0, component)["artifact"])
            second = Path(self.component_result(1, component)["artifact"])
            first_bytes = first.read_bytes()
            self.assertEqual(first_bytes, second.read_bytes())
            self.assertEqual(first_bytes[4:8], bytes(4))
            self.assertEqual(
                self.component_result(0, component)["artifactSha256"],
                hashlib.sha256(first_bytes).hexdigest(),
            )

            with tempfile.TemporaryDirectory() as directory:
                manifest, entries, payload = RUNNER.load_artifact(first, Path(directory))
                self.assertEqual(manifest["component"], component)
                self.assertEqual(manifest["id"], descriptor["artifactId"])
                self.assertEqual(entries, descriptor["files"])
                for relative_path in entries:
                    self.assertTrue((payload / relative_path).is_file())

    def test_predecessor_and_new_path_partition_is_complete(self):
        for component, descriptor in RELEASE["components"].items():
            result = self.component_result(0, component)
            predecessor_path = Path(result["predecessorChecks"])
            new_paths_path = Path(result["newPaths"])
            predecessor_lines = [
                line for line in predecessor_path.read_text(encoding="utf-8").splitlines() if line
            ]
            new_path_lines = [
                line for line in new_paths_path.read_text(encoding="utf-8").splitlines() if line
            ]
            self.assertEqual(len(predecessor_lines), len(descriptor["predecessors"]))
            self.assertEqual(len(new_path_lines), len(descriptor["newPaths"]))
            self.assertEqual(
                set(descriptor["files"]),
                set(descriptor["predecessors"]) | set(descriptor["newPaths"]),
            )
            self.assertEqual(
                hashlib.sha256(predecessor_path.read_bytes()).hexdigest(),
                result["predecessorChecksSha256"],
            )
            self.assertEqual(
                hashlib.sha256(new_paths_path.read_bytes()).hexdigest(),
                result["newPathsSha256"],
            )

    def test_ops_contract_contains_isolated_ontology_and_workspace_tools(self):
        ops = SOURCE_ROOT / "overlays" / "ops-agents"
        runtime = (ops / "src/mcp/tool-runtime.ts").read_text(encoding="utf-8")
        routes = (ops / "src/routes/ontology.ts").read_text(encoding="utf-8")
        migration = (
            ops / "migrations/006_agent_credential_purpose.sql"
        ).read_text(encoding="utf-8")
        installer = (
            ops / "src/assets/codex-npm/bin/nodedc-ops-codex.mjs"
        ).read_text(encoding="utf-8")
        for tool in ("tasker_get_issue", "tasker_create_project", "tasker_attach_file"):
            self.assertIn(tool, runtime)
        self.assertIn('findActiveSessionByToken(token, "ontology")', routes)
        self.assertIn("purpose IN ('ops', 'ontology')", migration)
        self.assertIn("ONTOLOGY_SERVER_NAME", installer)
        self.assertIn("Ops MCP smoke tools", installer)
        self.assertIn("Ontology MCP smoke tools", installer)

    def test_tasker_contract_uses_existing_storage_and_admin_gate(self):
        tasker = SOURCE_ROOT / "overlays" / "tasker"
        adapter = (
            tasker
            / "plane-src/apps/api/plane/authentication/views/nodedc_agent_adapter.py"
        ).read_text(encoding="utf-8")
        urls = (tasker / "plane-src/apps/api/plane/urls.py").read_text(encoding="utf-8")
        self.assertIn("ProjectSerializer", adapter)
        self.assertIn("workspace_admin_required", adapter)
        self.assertIn("MAX_AGENT_ATTACHMENT_BYTES = 5 * 1024 * 1024", adapter)
        self.assertIn("S3Storage(is_server=True)", adapter)
        self.assertIn("get_project_storage_quota_state", adapter)
        self.assertIn("finalize_uploaded_file_asset", adapter)
        self.assertIn("NodeDCAgentIssueAttachmentEndpoint", urls)
        self.assertNotIn("DELETE", adapter)

    def test_existing_outputs_are_not_overwritten(self):
        output = Path(self.component_result(0, "tasker")["artifact"]).parent
        env = os.environ.copy()
        env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output)
        result = subprocess.run(
            ["node", str(BUILDER_PATH), RELEASE["release"]],
            cwd=PLATFORM_ROOT,
            env=env,
            check=False,
            capture_output=True,
            text=True,
        )
        self.assertNotEqual(result.returncode, 0)
        self.assertIn("output_already_exists", result.stderr)


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