import { randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

import { afterEach, describe, expect, it } from "vitest";

import { ProjectError, ProjectService } from "../../apps/api/src/projects.js";

const baseNow = Date.parse("2026-08-02T09:00:00.000Z");
const retentionMs = 720 * 60 * 60 * 1_000;
const roots: string[] = [];
const services: ProjectService[] = [];

function writeEvidence(file: string, value: unknown) {
  const root = process.env.DADA_EVIDENCE_DIR_PROJECT_TRASH;
  if (!root) return;
  mkdirSync(root, { recursive: true });
  writeFileSync(resolve(root, file), `${JSON.stringify(value, null, 2)}\n`);
}

afterEach(() => {
  for (const service of services.splice(0)) service.close();
  for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});

describe("TDD-WP2-PROJ-003-trash-restore-purge", () => {
  it("keeps exact trash timestamps, blocks restore at twenty, and revokes access at purge_at", () => {
    let now = baseNow;
    const root = mkdtempSync(join(tmpdir(), "dada-wp2-03-lifecycle-"));
    roots.push(root);
    const service = new ProjectService({ clock: () => now, databasePath: join(root, "dada.sqlite3") });
    services.push(service);
    const ownerId = randomUUID();
    const target = service.createProjectForGeneration({ ownerId, prompt: "待恢复项目", ratio: "3:4", status: "failed" });

    const firstTrash = service.trashProject(ownerId, target.project.projectId);
    expect(firstTrash).toMatchObject({
      deletedAt: new Date(baseNow).toISOString(),
      purgeAt: new Date(baseNow + retentionMs).toISOString(),
      status: "trashed",
    });
    expect(service.activeProjectCount(ownerId)).toBe(0);

    const fillers = Array.from({ length: 20 }, (_, index) => service.createProjectForGeneration({
      ownerId, prompt: `占位项目 ${index}`, ratio: "1:1", status: "failed",
    }));
    expect(() => service.restoreProject(ownerId, target.project.projectId)).toThrowError(
      expect.objectContaining<ProjectError>({ code: "project_active_limit" }),
    );
    expect(service.listProjects(ownerId, "trashed")[0]).toMatchObject({
      deletedAt: firstTrash.deletedAt, purgeAt: firstTrash.purgeAt, status: "trashed",
    });

    service.trashProject(ownerId, fillers[0].project.projectId);
    const restored = service.restoreProject(ownerId, target.project.projectId);
    expect(restored).toMatchObject({ deletedAt: null, purgeAt: null, status: "failed_empty" });

    now += 60 * 60 * 1_000;
    const secondTrash = service.trashProject(ownerId, target.project.projectId);
    service.purgeProject(ownerId, target.project.projectId);
    expect(() => service.getProject(ownerId, target.project.projectId)).toThrowError(
      expect.objectContaining<ProjectError>({ code: "project_not_found" }),
    );

    const expiryTarget = service.createProjectForGeneration({ ownerId, prompt: "自然到期项目", ratio: "4:3", status: "failed" });
    const expiryTrash = service.trashProject(ownerId, expiryTarget.project.projectId);
    now = Date.parse(expiryTrash.purgeAt!);
    expect(service.listProjects(ownerId, "trashed").some((project) => project.projectId === expiryTarget.project.projectId)).toBe(false);
    expect(() => service.restoreProject(ownerId, expiryTarget.project.projectId)).toThrowError(
      expect.objectContaining<ProjectError>({ code: "project_not_found" }),
    );
    const cleanup = service.database.prepare("SELECT project_id, status FROM project_cleanup_queue ORDER BY created_at, project_id").all();
    expect(cleanup).toEqual(expect.arrayContaining([
      { project_id: target.project.projectId, status: "pending" },
      { project_id: expiryTarget.project.projectId, status: "pending" },
    ]));
    writeEvidence("db-diff.json", {
      active_limit_restore_preserved_timestamps: true,
      cleanup_queue: cleanup,
      explicit_purge_deleted_at: secondTrash.deletedAt,
      natural_expiry_revoked_at_boundary: true,
      retention_milliseconds: retentionMs,
    });
  }, 15_000);
});
