import { spawnSync } from "node:child_process";

function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
  if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}

function asRecord(value: unknown, label: string): Record<string, unknown> {
  assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
  return value as Record<string, unknown>;
}

function runDeployPlan(environment: "dev" | "prod", serviceId: string): Record<string, unknown> {
  const result = spawnSync("bun", ["scripts/cli.ts", "deploy", "plan", "--env", environment, "--service", serviceId], {
    cwd: process.cwd(),
    encoding: "utf8",
    maxBuffer: 8 * 1024 * 1024,
  });
  assertCondition(result.status === 0, `deploy plan should exit 0 for ${environment}/${serviceId}`, {
    status: result.status,
    stdout: result.stdout.slice(-2000),
    stderr: result.stderr.slice(-2000),
  });
  const envelope = asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
  assertCondition(envelope.ok === true, `deploy plan envelope should be ok for ${environment}/${serviceId}`, envelope);
  const data = asRecord(envelope.data, "deploy plan data");
  const services = Array.isArray(data.services) ? data.services : [];
  assertCondition(services.length === 1, `deploy plan should return one service for ${environment}/${serviceId}`, data);
  return asRecord(services[0], "service plan");
}

function listIncludes(value: unknown, expected: string): boolean {
  return Array.isArray(value) && value.some((item) => item === expected);
}

function assertMainServerComposeConsumer(
  environment: "dev" | "prod",
  serviceId: string,
  expectedComposeService: string,
  expectedContainerName: string,
): void {
  const plan = runDeployPlan(environment, serviceId);
  const artifact = asRecord(plan.artifactConsumer, `${serviceId} ${environment} artifactConsumer`);
  const target = asRecord(plan.target, `${serviceId} ${environment} target`);
  const registry = asRecord(artifact.registry, `${serviceId} ${environment} registry`);
  const build = asRecord(artifact.build, `${serviceId} ${environment} build`);
  assertCondition(plan.deploymentPath === "d601-registry-artifact-consumer", `${serviceId} ${environment} deployment path must be artifact consumer`, plan);
  assertCondition(artifact.consumerKind === "main-server-compose", `${serviceId} ${environment} must be a main-server Compose artifact consumer`, artifact);
  assertCondition(artifact.noRuntimeSourceBuild === true, `${serviceId} ${environment} plan must declare no runtime source build`, artifact);
  assertCondition(artifact.dryRunOnly === false, `${serviceId} ${environment} should not be dry-run-only`, artifact);
  assertCondition(String(artifact.registryImage ?? "").includes(`127.0.0.1:5000/unidesk/${serviceId}:`), `${serviceId} registry image missing`, artifact);
  assertCondition(registry.repository === `unidesk/${serviceId}`, `${serviceId} registry repository mismatch`, registry);
  assertCondition(registry.digest === null, `${serviceId} plan must not fake digest`, registry);
  assertCondition(build.willRunDockerBuild === false, `${serviceId} CD must not run docker build`, build);
  assertCondition(build.willRunDockerComposeBuild === false, `${serviceId} CD must not run docker compose build`, build);
  assertCondition(build.producerBoundary === "ci publish-user-service", `${serviceId} producer boundary mismatch`, build);
  assertCondition(target.runtimeHost === "main-server", `${serviceId} target should be main-server`, target);
  assertCondition(target.composeService === expectedComposeService, `${serviceId} compose service mismatch`, target);
  assertCondition(target.containerName === expectedContainerName, `${serviceId} container mismatch`, target);
  assertCondition(listIncludes(target.forbiddenActions, "docker build"), `${serviceId} plan should forbid docker build`, target);
  assertCondition(listIncludes(target.forbiddenActions, "docker compose build"), `${serviceId} plan should forbid compose build`, target);
  if (serviceId === "baidu-netdisk") {
    const runtimeSecrets = asRecord(artifact.runtimeSecrets, `${serviceId} ${environment} runtimeSecrets`);
    const secretSource = asRecord(runtimeSecrets.secretSource, `${serviceId} ${environment} secretSource`);
    const requirements = Array.isArray(runtimeSecrets.requirements) ? runtimeSecrets.requirements.map((item, index) => asRecord(item, `${serviceId} ${environment} requirement ${index}`)) : [];
    assertCondition(runtimeSecrets.check === "runtime-secret-presence", `${serviceId} should expose secret presence check`, runtimeSecrets);
    assertCondition(secretSource.kind === "compose-env-file", `${serviceId} should name compose env secret source`, secretSource);
    assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, `${serviceId} must not print secret values`, runtimeSecrets);
    assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", `${serviceId} requiredSecretsPresent should be boolean`, runtimeSecrets);
    assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), `${serviceId} missingSecretKeys should be an array`, runtimeSecrets);
    assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, `${serviceId} recommendedAction should be explicit`, runtimeSecrets);
    assertCondition(requirements.length === 3, `${serviceId} should list three required source secrets`, runtimeSecrets);
    assertCondition(requirements.every((item) => item.valuePrinted === false), `${serviceId} requirements must not print values`, requirements);
    assertCondition(!JSON.stringify(runtimeSecrets).includes("0123456789abcdef"), `${serviceId} must not leak secret-looking values`, runtimeSecrets);
  }
}

assertMainServerComposeConsumer("dev", "baidu-netdisk", "baidu-netdisk", "baidu-netdisk-backend");
assertMainServerComposeConsumer("prod", "baidu-netdisk", "baidu-netdisk", "baidu-netdisk-backend");
assertMainServerComposeConsumer("dev", "project-manager", "project-manager", "project-manager-backend");
assertMainServerComposeConsumer("prod", "project-manager", "project-manager", "project-manager-backend");

const findjob = runDeployPlan("dev", "findjob");
const findjobArtifact = asRecord(findjob.artifactConsumer, "findjob artifactConsumer");
const findjobTarget = asRecord(findjob.target, "findjob target");
assertCondition(findjobArtifact.consumerKind === "d601-direct-compose", "findjob dev must be a D601 direct Compose artifact consumer", findjobArtifact);
assertCondition(findjobArtifact.noRuntimeSourceBuild === true, "findjob dry-run must declare no runtime source build", findjobArtifact);
assertCondition(findjobTarget.runtimeHost === "D601", "findjob target should be D601", findjobTarget);
assertCondition(findjobTarget.composeService === "server", "findjob compose service should be server", findjobTarget);
assertCondition(listIncludes(findjobTarget.forbiddenActions, "docker compose build"), "findjob plan should forbid Compose build", findjobTarget);

const backendCoreDev = runDeployPlan("dev", "backend-core");
const backendCoreArtifact = asRecord(backendCoreDev.artifactConsumer, "backend-core dev artifactConsumer");
const backendCoreTarget = asRecord(backendCoreDev.target, "backend-core dev target");
const backendCoreRegistry = asRecord(backendCoreArtifact.registry, "backend-core dev registry");
const backendCoreSource = asRecord(backendCoreArtifact.source, "backend-core dev source");
const backendCoreBuild = asRecord(backendCoreArtifact.build, "backend-core dev build");
assertCondition(backendCoreDev.deploymentPath === "d601-registry-artifact-consumer", "backend-core dev deployment path must be artifact consumer", backendCoreDev);
assertCondition(backendCoreArtifact.consumerKind === "d601-k3s-managed", "backend-core dev must be a D601 k3s-managed artifact consumer", backendCoreArtifact);
assertCondition(backendCoreArtifact.noRuntimeSourceBuild === true, "backend-core dev plan must declare no runtime source build", backendCoreArtifact);
assertCondition(String(backendCoreArtifact.registryImage ?? "").includes("127.0.0.1:5000/unidesk/backend-core:"), "backend-core dev registry image missing", backendCoreArtifact);
assertCondition(backendCoreRegistry.repository === "unidesk/backend-core", "backend-core dev registry repository mismatch", backendCoreRegistry);
assertCondition(backendCoreRegistry.digest === null, "backend-core dev plan must not fake digest", backendCoreRegistry);
assertCondition(String(backendCoreRegistry.digestSource ?? "").includes("manifest HEAD"), "backend-core dev digest source should name manifest HEAD", backendCoreRegistry);
assertCondition(backendCoreSource.repo === "https://github.com/pikasTech/unidesk", "backend-core dev source repo mismatch", backendCoreSource);
assertCondition(backendCoreBuild.willCompile === false, "backend-core dev plan must not compile in CD", backendCoreBuild);
assertCondition(backendCoreBuild.willRunCargoBuild === false, "backend-core dev plan must not run cargo build in CD", backendCoreBuild);
assertCondition(backendCoreBuild.willRunDockerBuild === false, "backend-core dev plan must not run docker build in CD", backendCoreBuild);
assertCondition(backendCoreBuild.producerBoundary === "ci publish-backend-core", "backend-core dev producer boundary mismatch", backendCoreBuild);
assertCondition(backendCoreTarget.namespace === "unidesk-dev", "backend-core dev target namespace should be unidesk-dev", backendCoreTarget);
assertCondition(backendCoreTarget.deployment === "backend-core-dev", "backend-core dev deployment should be backend-core-dev", backendCoreTarget);
assertCondition(backendCoreTarget.service === "backend-core-dev", "backend-core dev service should be backend-core-dev", backendCoreTarget);
assertCondition(backendCoreTarget.targetImage === "unidesk-backend-core:dev", "backend-core dev target image mismatch", backendCoreTarget);
assertCondition(listIncludes(backendCoreTarget.forbiddenActions, "docker build"), "backend-core dev plan should forbid docker build", backendCoreTarget);
assertCondition(listIncludes(backendCoreTarget.forbiddenActions, "docker compose build"), "backend-core dev plan should forbid compose build", backendCoreTarget);

const mdtodo = runDeployPlan("prod", "mdtodo");
const mdtodoArtifact = asRecord(mdtodo.artifactConsumer, "mdtodo artifactConsumer");
const mdtodoTarget = asRecord(mdtodo.target, "mdtodo target");
assertCondition(mdtodoArtifact.consumerKind === "d601-k3s-managed", "mdtodo prod must be a D601 k3s-managed artifact consumer", mdtodoArtifact);
assertCondition(mdtodoArtifact.noRuntimeSourceBuild === true, "mdtodo dry-run must declare no runtime source build", mdtodoArtifact);
assertCondition(mdtodoTarget.namespace === "unidesk", "mdtodo prod target namespace should be unidesk", mdtodoTarget);
assertCondition(listIncludes(mdtodoTarget.forbiddenActions, "NodePort"), "mdtodo plan should forbid NodePort", mdtodoTarget);

const metNonlinear = runDeployPlan("prod", "met-nonlinear");
const metArtifact = asRecord(metNonlinear.artifactConsumer, "met-nonlinear artifactConsumer");
assertCondition(metArtifact.consumerKind === "d601-direct-compose", "met-nonlinear should remain D601 direct Compose", metArtifact);
assertCondition(metArtifact.dryRunOnly === true, "met-nonlinear must remain dry-run only", metArtifact);
assertCondition(String(metArtifact.blockedReason ?? "").includes("runtime-verification-blocked"), "met-nonlinear blocked reason should mention runtime verification", metArtifact);

const k3sctl = runDeployPlan("prod", "k3sctl-adapter");
const k3sctlArtifact = asRecord(k3sctl.artifactConsumer, "k3sctl-adapter artifactConsumer");
const k3sctlTarget = asRecord(k3sctl.target, "k3sctl-adapter target");
assertCondition(k3sctlArtifact.consumerKind === "d601-direct-compose", "k3sctl-adapter should be D601 direct Compose", k3sctlArtifact);
assertCondition(k3sctlArtifact.dryRunOnly === true, "k3sctl-adapter must be dry-run only for worker automation", k3sctlArtifact);
assertCondition(String(k3sctlArtifact.blockedReason ?? "").includes("supervisor"), "k3sctl-adapter blocked reason should mention supervisor confirmation", k3sctlArtifact);
assertCondition(k3sctlTarget.composeService === "k3sctl-adapter", "k3sctl target service should be k3sctl-adapter", k3sctlTarget);

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "deploy plan models dev backend-core as a no-build D601 k3s artifact consumer",
    "dev backend-core plan exposes registry/source/build boundaries and target metadata",
    "baidu-netdisk and project-manager dev/prod plans are no-build main-server Compose artifact consumers",
    "deploy plan distinguishes D601 direct Compose from D601 k3s-managed artifact consumers",
    "deploy plan exposes no-runtime-source-build and forbidden build/public-port actions",
    "met-nonlinear remains runtime-verification-blocked",
    "k3sctl-adapter remains supervisor-only dry-run",
  ],
}, null, 2)}\n`);
