import { buildDockerCleanupPlan, type DockerCleanupInventory } from "./src/server-cleanup";

type JsonRecord = Record<string, unknown>;

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

const observedAt = "2026-05-21T14:00:00.000Z";

const fixture: DockerCleanupInventory = {
  observedAt,
  images: [
    {
      id: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
      repoTags: ["unidesk-backend-core:latest"],
      repoDigests: [],
      sizeBytes: 110 * 1024 * 1024,
      createdAt: "2026-05-21T10:00:00.000Z",
      labels: { "unidesk.ai/service-id": "backend-core", "unidesk.ai/source-commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
    },
    {
      id: "sha256:2222222222222222222222222222222222222222222222222222222222222222",
      repoTags: ["127.0.0.1:5000/unidesk/frontend:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
      repoDigests: [],
      sizeBytes: 120 * 1024 * 1024,
      createdAt: "2026-05-21T08:00:00.000Z",
      labels: { "unidesk.ai/service-id": "frontend", "unidesk.ai/source-commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
    },
    {
      id: "sha256:3333333333333333333333333333333333333333333333333333333333333333",
      repoTags: ["old-test-image:local"],
      repoDigests: ["old-test-image@sha256:3333333333333333333333333333333333333333333333333333333333333333"],
      sizeBytes: 500 * 1024 * 1024,
      createdAt: "2026-05-19T14:00:00.000Z",
      labels: {},
    },
    {
      id: "sha256:4444444444444444444444444444444444444444444444444444444444444444",
      repoTags: [],
      repoDigests: [],
      sizeBytes: 1024 * 1024 * 1024,
      createdAt: "2026-05-18T14:00:00.000Z",
      labels: {},
    },
  ],
  containers: [
    {
      id: "container-running-backend-core",
      name: "unidesk-backend-core",
      imageRef: "unidesk-backend-core:latest",
      imageId: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
      state: "running",
      status: "running",
      labels: {},
    },
  ],
  desiredImageRefs: [
    {
      ref: "127.0.0.1:5000/unidesk/frontend:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      source: "CI.json + deploy.json",
      serviceId: "frontend",
      reason: "current commit-pinned registry artifact",
    },
  ],
  desiredCommitsByService: {
    "backend-core": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
    frontend: ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
  },
  protectedStorage: [
    {
      kind: "docker-volume",
      ref: "unidesk_pgdata_10gb",
      risk: "blocked",
      reason: "database named volume",
    },
    {
      kind: "path",
      ref: "/workspace/unidesk/.state/baidu-netdisk",
      risk: "blocked",
      reason: "Baidu Netdisk state",
    },
    {
      kind: "path",
      ref: "/home/ubuntu/.unidesk/registry-storage",
      risk: "blocked",
      reason: "registry storage",
    },
  ],
  collection: {
    dockerAvailable: true,
    readOnlyCommands: [["docker", "image", "ls", "-q", "--no-trunc"]],
    errors: [],
  },
};

export function runServerCleanupPlanContract(): JsonRecord {
  const plan = buildDockerCleanupPlan(fixture, { minAgeHours: 24, limit: 20 });
  const candidateIds = plan.candidateStaleImages.map((image) => image.id);
  const protectedIds = plan.protectedImages.map((image) => image.id);

  assertCondition(plan.ok === true, "plan should be ok", plan);
  assertCondition(plan.dryRun === true && plan.mutation === false, "plan must be dry-run and non-mutating", plan.policy);
  assertCondition(plan.policy.deletionExecuted === false, "plan must not execute deletion", plan.policy);
  assertCondition(plan.policy.dockerPruneUsed === false, "plan must not use docker prune", plan.policy);
  assertCondition(plan.policy.dockerVolumesTouched === false, "plan must not touch docker volumes", plan.policy);
  assertCondition(plan.policy.databaseCleanupIncluded === false, "plan must not include database cleanup", plan.policy);

  assertCondition(candidateIds.includes("sha256:3333333333333333333333333333333333333333333333333333333333333333"), "tagged stale image should be a candidate", plan.candidateStaleImages);
  assertCondition(candidateIds.includes("sha256:4444444444444444444444444444444444444444444444444444444444444444"), "dangling stale image should be a candidate", plan.candidateStaleImages);
  assertCondition(protectedIds.includes("sha256:1111111111111111111111111111111111111111111111111111111111111111"), "running image should be protected", plan.protectedImages);
  assertCondition(protectedIds.includes("sha256:2222222222222222222222222222222222222222222222222222222222222222"), "desired deploy image should be protected", plan.protectedImages);

  assertCondition(plan.candidateStaleImages.length === 2, "only stale non-protected images should be candidates", plan.candidateStaleImages);
  assertCondition(plan.risk.medium === 1, "tagged stale candidate should be medium risk", plan.risk);
  assertCondition(plan.risk.low === 1, "dangling stale candidate should be low risk", plan.risk);
  assertCondition(plan.commandsToReview.length === 2, "commandsToReview should include candidate commands", plan.commandsToReview);
  assertCondition(plan.commandsToReview.every((command) => command.requiresManualApproval === true), "commands must require manual approval", plan.commandsToReview);
  const taggedCommand = plan.commandsToReview.find((command) => command.imageId === "sha256:3333333333333333333333333333333333333333333333333333333333333333");
  assertCondition(taggedCommand?.command.includes("old-test-image:local"), "tagged candidate command should include reviewed tag", plan.commandsToReview);
  assertCondition(taggedCommand?.command.includes("old-test-image@sha256:3333333333333333333333333333333333333333333333333333333333333333"), "tagged candidate command should include reviewed digest", plan.commandsToReview);
  assertCondition(plan.commandsToReview.some((command) => command.command.includes("sha256:4444444444444444444444444444444444444444444444444444444444444444")), "dangling candidate command should use image id", plan.commandsToReview);
  assertCondition(!JSON.stringify(plan.commandsToReview).includes("docker image prune"), "plan must not recommend image prune", plan.commandsToReview);
  assertCondition(!JSON.stringify(plan.commandsToReview).includes("docker system prune"), "plan must not recommend system prune", plan.commandsToReview);
  assertCondition(plan.prohibitedCommands.includes("docker image prune"), "image prune should be explicitly prohibited", plan.prohibitedCommands);
  assertCondition(plan.prohibitedCommands.includes("docker system prune"), "system prune should be explicitly prohibited", plan.prohibitedCommands);
  assertCondition(plan.protectedStorage.some((item) => item.ref === "unidesk_pgdata_10gb"), "database volume must be protected", plan.protectedStorage);
  assertCondition(plan.protectedStorage.some((item) => String(item.ref).includes("baidu-netdisk")), "Baidu Netdisk state must be protected", plan.protectedStorage);
  assertCondition(plan.protectedStorage.some((item) => String(item.ref).includes("registry-storage")), "registry storage must be protected", plan.protectedStorage);
  assertCondition(plan.estimatedReclaimBytes === (500 * 1024 * 1024) + (1024 * 1024 * 1024), "estimated reclaim should sum candidate image sizes", plan.estimates);

  return {
    ok: true,
    checks: [
      "dry-run non-mutating policy",
      "active image protected",
      "deploy/CI desired image protected",
      "stale candidates emitted",
      "risk levels emitted",
      "manual commandsToReview emitted",
      "database/registry/baidu storage protected",
      "prune commands absent",
    ],
  };
}

if (import.meta.main) {
  process.stdout.write(`${JSON.stringify(runServerCleanupPlanContract(), null, 2)}\n`);
}
