import { spawnSync } from "node:child_process";

type JsonRecord = Record<string, unknown>;
type Environment = "dev" | "prod";

type ServiceCase =
  | {
      serviceId: "mdtodo";
      environment: "dev";
      commit: "595de3d320b73ec006794440b32db48b3ad14d2b";
      sourceRepo: "https://github.com/pikasTech/unidesk";
      dockerfile: "src/components/microservices/mdtodo/Dockerfile";
      targetKind: "d601-k3s";
      namespace: "unidesk-dev";
      deployment: "mdtodo-dev";
      service: "mdtodo-dev";
      runtimeImage: "unidesk-mdtodo:595de3d320b73ec006794440b32db48b3ad14d2b";
      expectedValidationSnippets: string[];
      rollbackType: "d601-k3s-previous-commit";
    }
  | {
      serviceId: "claudeqq";
      environment: "dev";
      commit: "203b1f46684c91340ecbbd8a74502bd55e4f2011";
      sourceRepo: "https://gitee.com/lyon1998/agent_skills";
      dockerfile: "claudeqq/Dockerfile";
      targetKind: "d601-k3s";
      namespace: "unidesk-dev";
      deployment: "claudeqq-dev";
      service: "claudeqq-dev";
      runtimeImage: "unidesk-claudeqq:203b1f46684c91340ecbbd8a74502bd55e4f2011";
      expectedValidationSnippets: string[];
      rollbackType: "d601-k3s-previous-commit";
    }
  | {
      serviceId: "todo-note";
      environment: "prod";
      commit: "a14ce0eb855a685fa17b47adacd54623e72cd2ff";
      sourceRepo: "https://gitee.com/Lyon1998/todo_note";
      dockerfile: "Dockerfile";
      targetKind: "compose";
      runtimeHost: "main-server";
      composeService: "todo-note";
      containerName: "todo-note-backend";
      runtimeImage: "todo-note:a14ce0eb855a685fa17b47adacd54623e72cd2ff";
      deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY";
      expectedValidationSnippets: string[];
      rollbackType: "compose-retag-recreate";
    };

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

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

function asArray(value: unknown, label: string): unknown[] {
  assertCondition(Array.isArray(value), `${label} must be an array`, value);
  return value as unknown[];
}

function runDeployApplyDryRun(environment: Environment, serviceId: string): JsonRecord {
  const result = spawnSync("bun", ["scripts/cli.ts", "deploy", "apply", "--env", environment, "--service", serviceId, "--dry-run"], {
    cwd: process.cwd(),
    encoding: "utf8",
    maxBuffer: 8 * 1024 * 1024,
  });
  assertCondition(result.status === 0, `deploy apply dry-run 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 apply dry-run envelope should be ok for ${environment}/${serviceId}`, envelope);
  const data = asRecord(envelope.data, "deploy apply dry-run data");
  assertCondition(data.action === "apply", `deploy apply dry-run should report action=apply for ${environment}/${serviceId}`, data);
  assertCondition(data.environment === environment, `deploy apply dry-run environment mismatch for ${environment}/${serviceId}`, data);
  assertCondition(data.executor === "d601-registry-artifact-consumer", `deploy apply dry-run executor mismatch for ${environment}/${serviceId}`, data);
  assertCondition(data.dryRun === true, `deploy apply dry-run should set dryRun=true for ${environment}/${serviceId}`, data);
  const results = asArray(data.results, `deploy apply dry-run results for ${environment}/${serviceId}`);
  assertCondition(results.length === 1, `deploy apply dry-run should return one service result for ${environment}/${serviceId}`, data);
  return asRecord(results[0], `deploy apply dry-run service result for ${environment}/${serviceId}`);
}

function assertK3sTarget(result: JsonRecord, item: Extract<ServiceCase, { targetKind: "d601-k3s" }>): void {
  const target = asRecord(result.target, `${item.serviceId} target`);
  const deployments = asArray(target.deployments, `${item.serviceId} deployments`).map((deployment, index) => asRecord(deployment, `${item.serviceId} deployment ${index}`));
  assertCondition(target.kind === "d601-k3s", `${item.serviceId} target kind mismatch`, target);
  assertCondition(target.namespace === item.namespace, `${item.serviceId} namespace mismatch`, target);
  assertCondition(target.deployment === item.deployment, `${item.serviceId} deployment mismatch`, target);
  assertCondition(target.service === item.service, `${item.serviceId} service mismatch`, target);
  assertCondition(target.runtimeImage === item.runtimeImage, `${item.serviceId} runtime image mismatch`, target);
  assertCondition(target.stableImage === `unidesk-${item.serviceId}:dev`, `${item.serviceId} stable image mismatch`, target);
  assertCondition(String(target.deployCommandShape ?? "").includes("kubectl set image"), `${item.serviceId} deploy command shape must be k3s`, target);
  assertCondition(deployments.length === 1 && deployments[0]?.name === item.deployment, `${item.serviceId} deployment list mismatch`, deployments);
  assertCondition(deployments[0]?.containerName === item.serviceId, `${item.serviceId} deployment container mismatch`, deployments);
}

function assertComposeTarget(result: JsonRecord, item: Extract<ServiceCase, { targetKind: "compose" }>): void {
  const target = asRecord(result.target, `${item.serviceId} target`);
  assertCondition(target.kind === "compose", `${item.serviceId} target kind mismatch`, target);
  assertCondition(target.runtimeHost === item.runtimeHost, `${item.serviceId} runtime host mismatch`, target);
  assertCondition(target.composeService === item.composeService, `${item.serviceId} compose service mismatch`, target);
  assertCondition(target.containerName === item.containerName, `${item.serviceId} container mismatch`, target);
  assertCondition(target.targetImage === item.composeService, `${item.serviceId} target image mismatch`, target);
  assertCondition(target.runtimeImage === item.runtimeImage, `${item.serviceId} runtime image mismatch`, target);
  assertCondition(target.deployEnvPrefix === item.deployEnvPrefix, `${item.serviceId} deploy env prefix mismatch`, target);
  assertCondition(target.deployCommandShape === "docker compose up -d --no-build --no-deps --force-recreate todo-note", `${item.serviceId} deploy command shape mismatch`, target);
}

function assertCommonContract(result: JsonRecord, item: ServiceCase): void {
  const source = asRecord(result.source, `${item.serviceId} source`);
  const registry = asRecord(result.registry, `${item.serviceId} registry`);
  const build = asRecord(result.build, `${item.serviceId} build`);
  const labels = asRecord(result.requiredLabels, `${item.serviceId} requiredLabels`);
  const registryProbe = asRecord(result.registryProbe, `${item.serviceId} registryProbe`);
  const validation = asArray(result.validation, `${item.serviceId} validation`).map(String);
  const liveApply = asRecord(result.liveApply, `${item.serviceId} liveApply`);
  const rollback = asRecord(result.rollback, `${item.serviceId} rollback`);

  assertCondition(result.ok === true, `${item.serviceId} dry-run service result must be ok`, result);
  assertCondition(result.supported === true, `${item.serviceId} dry-run service result must be supported`, result);
  assertCondition(result.dryRun === true && result.mutation === false, `${item.serviceId} dry-run must be non-mutating`, result);
  assertCondition(result.environment === item.environment, `${item.serviceId} environment mismatch`, result);
  assertCondition(result.providerId === "D601", `${item.serviceId} provider mismatch`, result);
  assertCondition(result.serviceId === item.serviceId, `${item.serviceId} service id mismatch`, result);
  assertCondition(result.commit === item.commit, `${item.serviceId} commit mismatch`, result);
  assertCondition(result.sourceRepo === item.sourceRepo, `${item.serviceId} source repo mismatch`, result);
  assertCondition(result.deployRef === `origin/master:deploy.json#environments.${item.environment}.services.${item.serviceId}`, `${item.serviceId} deployRef mismatch`, result);
  assertCondition(result.sourceImage === `127.0.0.1:5000/unidesk/${item.serviceId}:${item.commit}`, `${item.serviceId} source image mismatch`, result);
  assertCondition(source.repo === item.sourceRepo, `${item.serviceId} source.repo mismatch`, source);
  assertCondition(source.commit === item.commit, `${item.serviceId} source.commit mismatch`, source);
  assertCondition(source.dockerfile === item.dockerfile, `${item.serviceId} source.dockerfile mismatch`, source);
  assertCondition(registry.endpoint === "http://127.0.0.1:5000", `${item.serviceId} registry endpoint mismatch`, registry);
  assertCondition(registry.repository === `unidesk/${item.serviceId}`, `${item.serviceId} registry repository mismatch`, registry);
  assertCondition(registry.tag === item.commit, `${item.serviceId} registry tag mismatch`, registry);
  assertCondition(registry.imageRef === `127.0.0.1:5000/unidesk/${item.serviceId}:${item.commit}`, `${item.serviceId} registry imageRef mismatch`, registry);
  assertCondition(registry.digest === null, `${item.serviceId} dry-run must not fake a digest`, registry);
  assertCondition(String(registry.digestSource ?? "").includes("live apply must read this digest"), `${item.serviceId} digest source mismatch`, registry);
  assertCondition(build.willCompile === false, `${item.serviceId} dry-run must not compile`, build);
  assertCondition(build.willRunCargoBuild === false, `${item.serviceId} dry-run must not run cargo build`, build);
  assertCondition(build.willRunDockerBuild === false, `${item.serviceId} dry-run must not run docker build`, build);
  assertCondition(build.willRunDockerComposeBuild === false, `${item.serviceId} dry-run must not run docker compose build`, build);
  assertCondition(build.producerBoundary === "ci publish-user-service", `${item.serviceId} producer boundary mismatch`, build);
  assertCondition(labels["unidesk.ai/service-id"] === item.serviceId, `${item.serviceId} service label mismatch`, labels);
  assertCondition(labels["unidesk.ai/source-repo"] === item.sourceRepo, `${item.serviceId} source repo label mismatch`, labels);
  assertCondition(labels["unidesk.ai/source-commit"] === item.commit, `${item.serviceId} commit label mismatch`, labels);
  assertCondition(labels["unidesk.ai/dockerfile"] === item.dockerfile, `${item.serviceId} dockerfile label mismatch`, labels);
  assertCondition(registryProbe.method === "HEAD", `${item.serviceId} registry probe must be HEAD`, registryProbe);
  assertCondition(String(registryProbe.url ?? "").includes(`/v2/unidesk/${item.serviceId}/manifests/${item.commit}`), `${item.serviceId} registry probe url mismatch`, registryProbe);
  assertCondition(String(result.boundary ?? "").includes("artifact-consumer only"), `${item.serviceId} boundary should say artifact-consumer only`, result);
  assertCondition(String(result.boundary ?? "").includes("never builds source on the runtime target"), `${item.serviceId} boundary should forbid target builds`, result);
  assertCondition(liveApply.allowed === true, `${item.serviceId} dry-run should remain live-apply eligible`, liveApply);
  assertCondition(liveApply.reason === null, `${item.serviceId} dry-run liveApply reason should be null`, liveApply);
  assertCondition(rollback.type === item.rollbackType, `${item.serviceId} rollback type mismatch`, rollback);
  assertCondition(String(rollback.commandShape ?? "").includes("<previous-full-sha>"), `${item.serviceId} rollback command should keep previous-full-sha placeholder`, rollback);

  for (const snippet of item.expectedValidationSnippets) {
    assertCondition(validation.some((line) => line.includes(snippet)), `${item.serviceId} validation should include ${snippet}`, validation);
  }
  if (item.serviceId === "todo-note") {
    const runtimeProof = asRecord(result.runtimeProof, "todo-note runtimeProof");
    const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, "todo-note runtime proof env keys").map(String);
    assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", "todo-note runtime proof kind mismatch", runtimeProof);
    assertCondition(runtimeProof.sourceDirectoryUsed === false, "todo-note runtime proof must not use source directory guesses", runtimeProof);
    assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_REQUESTED_COMMIT"), "todo-note proof should use generic requested commit env", runtimeProof);
    assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT"), "todo-note proof should use service requested commit env", runtimeProof);
  }
}

const cases: ServiceCase[] = [
  {
    serviceId: "mdtodo",
    environment: "dev",
    commit: "595de3d320b73ec006794440b32db48b3ad14d2b",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/microservices/mdtodo/Dockerfile",
    targetKind: "d601-k3s",
    namespace: "unidesk-dev",
    deployment: "mdtodo-dev",
    service: "mdtodo-dev",
    runtimeImage: "unidesk-mdtodo:595de3d320b73ec006794440b32db48b3ad14d2b",
    expectedValidationSnippets: [
      "D601 registry /v2 manifest exists",
      "native k3s containerd has the commit image and stable runtime image tag",
      "service health via Kubernetes API service proxy returns the same deploy.commit and deploy.requestedCommit",
    ],
    rollbackType: "d601-k3s-previous-commit",
  },
  {
    serviceId: "claudeqq",
    environment: "dev",
    commit: "203b1f46684c91340ecbbd8a74502bd55e4f2011",
    sourceRepo: "https://gitee.com/lyon1998/agent_skills",
    dockerfile: "claudeqq/Dockerfile",
    targetKind: "d601-k3s",
    namespace: "unidesk-dev",
    deployment: "claudeqq-dev",
    service: "claudeqq-dev",
    runtimeImage: "unidesk-claudeqq:203b1f46684c91340ecbbd8a74502bd55e4f2011",
    expectedValidationSnippets: [
      "D601 registry /v2 manifest exists",
      "native k3s containerd has the commit image and stable runtime image tag",
      "service health via Kubernetes API service proxy returns the same deploy.commit and deploy.requestedCommit",
    ],
    rollbackType: "d601-k3s-previous-commit",
  },
  {
    serviceId: "todo-note",
    environment: "prod",
    commit: "a14ce0eb855a685fa17b47adacd54623e72cd2ff",
    sourceRepo: "https://gitee.com/Lyon1998/todo_note",
    dockerfile: "Dockerfile",
    targetKind: "compose",
    runtimeHost: "main-server",
    composeService: "todo-note",
    containerName: "todo-note-backend",
    runtimeImage: "todo-note:a14ce0eb855a685fa17b47adacd54623e72cd2ff",
    deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
    expectedValidationSnippets: [
      "D601 registry /v2 manifest exists for the commit tag",
      "running Compose container image label matches the requested commit",
      "todo-note runtime health proof synthesizes deploy.commit/deploy.requestedCommit from Compose container runtime metadata",
      "not source directory guesses",
    ],
    rollbackType: "compose-retag-recreate",
  },
];

for (const item of cases) {
  const result = runDeployApplyDryRun(item.environment, item.serviceId);
  if (item.targetKind === "d601-k3s") assertK3sTarget(result, item);
  else assertComposeTarget(result, item);
  assertCommonContract(result, item);
}

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "deploy apply --env dev --service mdtodo --dry-run remains a non-mutating D601 k3s artifact consumer",
    "deploy apply --env dev --service claudeqq --dry-run remains a non-mutating D601 k3s artifact consumer",
    "deploy apply --env prod --service todo-note --dry-run remains a non-mutating main-server Compose artifact consumer",
    "dry-run output keeps registry/source/build boundaries and live-apply eligibility explicit",
    "dry-run output keeps rollback hints and health validation snippets intact",
  ],
  services: cases.map((service) => ({
    serviceId: service.serviceId,
    environment: service.environment,
    commit: service.commit,
    targetKind: service.targetKind,
  })),
}, null, 2)}\n`);
