#!/usr/bin/env python3
import hashlib
import json
import os
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
BUILDER = SCRIPT_DIR / "build-engine-data-product-publish-grant-artifact.mjs"
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
HISTORICAL_PROVIDER_CATALOG_SHA256 = (
    "9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a"
)
EXPECTED_ENTRIES = [
    "nodedc-source/server/assets/provider-packages/v1/catalog.json",
    "nodedc-source/server/dataProductPublishGrant",
    "nodedc-source/server/engineAgents/store.js",
    "nodedc-source/server/routes/engineAgentGateway.js",
    "nodedc-source/server/routes/n8n.js",
    "nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml",
]
PATCH_ID = "engine-data-product-publish-grant-20990101-001"
SUCCESSFUL_CREDENTIAL_SINK_INDEX_SHA256 = (
    "b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8"
)


class EnginePublishGrantArtifactTest(unittest.TestCase):
    def historical_source_is_current(self):
        catalog = (
            ENGINE_ROOT
            / "nodedc-source/server/assets/provider-packages/v1/catalog.json"
        )
        return hashlib.sha256(catalog.read_bytes()).hexdigest() == HISTORICAL_PROVIDER_CATALOG_SHA256

    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_artifact_is_narrow_secret_free_and_deterministic(self):
        if not self.historical_source_is_current():
            self.skipTest("historical Publish-grant builder source has advanced to its exact successor")
        with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-artifact-") as directory:
            root = Path(directory)
            first_dir = root / "first"
            second_dir = root / "second"
            first_dir.mkdir()
            second_dir.mkdir()
            first = self.build(first_dir, PATCH_ID)
            artifact = Path(first["artifact"])
            first_bytes = artifact.read_bytes()
            second = self.build(second_dir, PATCH_ID)
            second_bytes = Path(second["artifact"]).read_bytes()

            self.assertEqual(first["entries"], EXPECTED_ENTRIES)
            self.assertEqual(first["sha256"], hashlib.sha256(first_bytes).hexdigest())
            self.assertEqual(first["sha256"], second["sha256"])
            self.assertEqual(first_bytes, second_bytes)

            with tarfile.open(artifact, "r:gz") as archive:
                names = {member.name for member in archive.getmembers()}
                files = archive.extractfile("files.txt").read().decode("utf-8").splitlines()
                manifest = archive.extractfile("manifest.env").read().decode("utf-8")
                n8n_source = archive.extractfile(
                    "payload/nodedc-source/server/routes/n8n.js"
                ).read().decode("utf-8")
                gateway_source = archive.extractfile(
                    "payload/nodedc-source/server/routes/engineAgentGateway.js"
                ).read().decode("utf-8")
                runtime_override = archive.extractfile(
                    "payload/nodedc-source/services/backend/data-product-publish-grant/"
                    "docker-compose.immutable-runtime.yml"
                ).read().decode("utf-8")
                catalog = json.loads(archive.extractfile(
                    "payload/nodedc-source/server/assets/provider-packages/v1/catalog.json"
                ).read())
                provider_catalog_source = archive.extractfile(
                    "payload/nodedc-source/server/dataProductPublishGrant/providerCatalog.js"
                ).read().decode("utf-8")
                service_source = archive.extractfile(
                    "payload/nodedc-source/server/dataProductPublishGrant/service.js"
                ).read().decode("utf-8")
                store_source = archive.extractfile(
                    "payload/nodedc-source/server/dataProductPublishGrant/store.js"
                ).read().decode("utf-8")

            self.assertEqual(files, EXPECTED_ENTRIES)
            self.assertEqual(
                manifest,
                f"id={PATCH_ID}\ncomponent=engine\ntype=app-overlay\n",
            )
            forbidden_roots = (
                "payload/docker-compose.yml",
                "payload/nodedc-source/server/index.js",
                "payload/nodedc-source/server/credentialPolicies/ndcPrivateNode.js",
                "payload/nodedc-source/server/credentialSink/",
                "payload/nodedc-source/server/routes/engineCredentialSink.js",
                "payload/nodedc-source/server/middleware/demoAccess.js",
                "payload/nodedc-source/server/routes/ndcAgentMcp.js",
                "payload/nodedc-source/server/tests/",
                "payload/nodedc-source/dist/",
                "payload/nodedc-source/server/data/",
            )
            self.assertFalse(any(
                name == root.rstrip("/") or name.startswith(root)
                for name in names
                for root in forbidden_roots
            ))
            self.assertNotIn("from '../credentialSink/", n8n_source)
            self.assertIn("engineCredentialSinkN8nAdapter", n8n_source)
            self.assertIn("engineDataProductPublishGrantN8nAdapter", n8n_source)
            for tool in (
                "engine_plan_data_product_publish_grant",
                "engine_apply_data_product_publish_grant",
                "engine_accept_data_product_publish_grant",
                "engine_rollback_data_product_publish_grant",
            ):
                self.assertIn(tool, gateway_source)
            self.assertIn("  nodedc-backend:", runtime_override)
            self.assertNotIn("  n8n:", runtime_override)
            self.assertIn(
                "source: /volume2/nodedc-demo/nodedc-control-plane/publish-grants",
                runtime_override,
            )
            self.assertNotIn(
                "source: /volume2/nodedc-demo/nodedc-control-plane\n",
                runtime_override,
            )
            self.assertIn("read_only: true", runtime_override)
            self.assertEqual(runtime_override.count("create_host_path: false"), 2)
            provider = catalog["packages"][0]
            self.assertEqual(first["providerPackage"], "gelios.provider.v4")
            self.assertEqual(first["dataProductId"], "fleet.positions.current.v3")
            self.assertEqual(first["credentialValues"], "preserved")
            self.assertEqual(provider["id"], "gelios.provider.v4")
            self.assertEqual(
                [capability["id"] for capability in provider["capabilities"]],
                [
                    "gelios.monitoring_config.current.read",
                    "gelios.units.current.read",
                ],
            )
            self.assertEqual(
                [capability["request"]["url"] for capability in provider["capabilities"]],
                [
                    "https://api.geliospro.com/api/v1/users/me/monitoring-config",
                    "https://api.geliospro.com/api/v1/units?incltrip=true",
                ],
            )
            self.assertIn("function capabilityRequests(capability)", provider_catalog_source)
            self.assertIn("exactHttpRequestUrl(n8n)", provider_catalog_source)
            self.assertIn("providerRequestNodeIds", service_source)
            self.assertIn("providerRequestNodeIds", store_source)

    def test_builder_requires_a_fresh_never_issued_patch_id(self):
        with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-id-") as directory:
            artifact_dir = Path(directory)
            environment = os.environ.copy()
            environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
            cases = (
                ([], "fresh-patch-id"),
                (["engine-data-product-publish-grant-20260716-001"], "patch_id_already_issued"),
                (["engine-data-product-publish-grant-20260717-001"], "patch_id_already_issued"),
                (["engine-data-product-publish-grant-20260717-002"], "patch_id_already_issued"),
                (["engine-publish-grant-unit-001"], "fresh-patch-id"),
            )
            for arguments, expected in cases:
                with self.subTest(arguments=arguments):
                    result = subprocess.run(
                        ["node", str(BUILDER), *arguments],
                        check=False,
                        capture_output=True,
                        text=True,
                        env=environment,
                    )
                    self.assertNotEqual(result.returncode, 0)
                    self.assertIn(expected, result.stderr)

            if not self.historical_source_is_current():
                return
            self.build(artifact_dir, PATCH_ID)
            duplicate = subprocess.run(
                ["node", str(BUILDER), PATCH_ID],
                check=False,
                capture_output=True,
                text=True,
                env=environment,
            )
            self.assertNotEqual(duplicate.returncode, 0)
            self.assertIn("artifact_already_exists", duplicate.stderr)


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