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

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

import { CreditService } from "../../apps/api/src/credits.js";
import {
  GenerationSubmissionError,
  GenerationSubmissionService,
  StaticGenerationModelCatalog,
} from "../../apps/api/src/generation-submission.js";
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
import { ProjectService } from "../../apps/api/src/projects.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";

const now = Date.parse("2026-08-02T13:00:00.000Z");
const modelId = "gemini-3.1-flash-image-preview";
const png = Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), Buffer.alloc(48, 0x41)]);
const roots: string[] = [];
const closeables: Array<{ close(): void }> = [];

function evidence(caseId: string, file: string, value: unknown) {
  const root = process.env.DADA_EVIDENCE_DIR_GENERATION;
  if (!root) return;
  const directory = resolve(root, caseId);
  mkdirSync(directory, { recursive: true });
  writeFileSync(resolve(directory, file), Buffer.isBuffer(value) ? value : `${JSON.stringify(value, null, 2)}\n`);
}

function model(version = 1, cost = 1) {
  return {
    configSetVersion: version,
    configVersion: version,
    contractValidationStatus: "verified" as const,
    creditCost: cost,
    enabled: true,
    modelId,
    promptMaxLength: 1_000,
    referenceLimits: { maxFileBytes: 1_024, maxFiles: 2, maxTotalBytes: 2_048 },
    runtimeAvailability: { availableForNewJobs: true, reason: null },
    supportedRatios: ["3:4", "1:1", "4:3", "9:16"] as const,
  };
}

function harness(input: { available?: number; beforeTransaction?: () => Promise<void>; version?: number; cost?: number } = {}) {
  const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp2-05-generation-"));
  roots.push(dataRoot);
  mkdirSync(join(dataRoot, "db"), { recursive: true });
  const databasePath = join(dataRoot, "db", "dada.sqlite3");
  const registration = new RegistrationService({
    challengePepper: Buffer.alloc(32, 0x71), clock: () => now,
    currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
    invitePepper: Buffer.alloc(32, 0x72), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x73),
  });
  const projects = new ProjectService({ clock: () => now, databasePath });
  const credits = new CreditService({ clock: () => now, databasePath });
  const storage = new ManagedStorage({ dataRoot, databasePath });
  const models = new StaticGenerationModelCatalog([model(input.version ?? 1, input.cost ?? 1)]);
  const submissions = new GenerationSubmissionService({
    ...(input.beforeTransaction ? { beforeTransaction: input.beforeTransaction } : {}),
    clock: () => now, credits, models, storage,
  });
  closeables.push(submissions, storage, credits, projects, registration);
  const userId = randomUUID();
  registration.database.prepare(`INSERT INTO users (
    user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
  ) VALUES (?, ?, 'user', 'active', 1, ?, ?)`).run(userId, `${userId}@example.invalid`, randomUUID(), now);
  registration.database.prepare("INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'Generation User', '@generation')").run(userId);
  registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, ?, 0, ?)")
    .run(userId, input.available ?? 10, now);
  return { credits, dataRoot, models, projects, registration, storage, submissions, userId };
}

function request(userId: string, overrides: Record<string, unknown> = {}) {
  return {
    clientSubmissionId: randomUUID(),
    confirmedCreditCost: 1,
    existingReferenceAssetIds: [],
    idempotencyKey: `generation-${randomUUID()}-${randomUUID()}`,
    mode: "new_project" as const,
    modelConfigVersion: 1,
    modelId,
    newReferences: [],
    prompt: "一张极简海报",
    ratio: "3:4" as const,
    userId,
    ...overrides,
  };
}

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

describe("TASK-WP2-05 generation submission", () => {
  it("keeps exactly one active job when two independent submissions cross the transaction boundary", async () => {
    let arrivals = 0;
    let release!: () => void;
    const barrier = new Promise<void>((resolveBarrier) => { release = resolveBarrier; });
    const fixture = harness({ beforeTransaction: async () => { arrivals += 1; if (arrivals === 2) release(); await barrier; } });
    const leftRequest = request(fixture.userId);
    const rightRequest = request(fixture.userId);
    const [left, right] = await Promise.all([fixture.submissions.submit(leftRequest), fixture.submissions.submit(rightRequest)]);
    expect([left.created, right.created].sort()).toEqual([false, true]);
    expect(left.task.generationId).toBe(right.task.generationId);
    const counts = Object.fromEntries(["projects", "generation_jobs", "credit_reservations", "outbox_events"].map((table) => [
      table,
      (fixture.registration.database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count,
    ]));
    expect(counts).toEqual({ credit_reservations: 1, generation_jobs: 1, outbox_events: 1, projects: 1 });
    expect(fixture.credits.readAccount(fixture.userId)).toMatchObject({ availableBalance: 9, reservedBalance: 1 });
    evidence("TDD-WP2-GEN-001-concurrent-reserve", "response-first.json", left);
    evidence("TDD-WP2-GEN-001-concurrent-reserve", "response-second.json", right);
    evidence("TDD-WP2-GEN-001-concurrent-reserve", "db-diff.json", { counts, credits: fixture.credits.readAccount(fixture.userId) });
    evidence("TDD-WP2-GEN-001-concurrent-reserve", "concurrency-trace.json", { arrivals, same_generation: left.task.generationId === right.task.generationId });
  });

  it("stores the submitted model and reference snapshot only after a complete private file commit", async () => {
    const fixture = harness();
    evidence("TDD-WP2-GEN-001-submit-snapshot", "fs-before.json", { managed_files: 0, staging_files: 0 });
    const input = request(fixture.userId, {
      newReferences: [{ content: Readable.from(png), fileName: "reference.png", mimeType: "image/png", projectedBytes: png.byteLength }],
    });
    const result = await fixture.submissions.submit(input);
    expect(result).toMatchObject({ created: true, task: { confirmedCreditCost: 1, modelConfigVersion: 1, modelId, referenceCount: 1, status: "queued" } });
    const row = fixture.registration.database.prepare(`
      SELECT g.submission_ready, g.config_snapshot_json, r.managed_file_id, mf.relative_path
      FROM generation_jobs g
      JOIN generation_reference_snapshots r ON r.generation_id = g.generation_id
      JOIN managed_files mf ON mf.file_id = r.managed_file_id
      WHERE g.generation_id = ?
    `).get(result.task.generationId) as { config_snapshot_json: string; managed_file_id: string; relative_path: string; submission_ready: number };
    expect(row.submission_ready).toBe(1);
    expect(JSON.parse(row.config_snapshot_json)).toMatchObject({ config_version: 1, credit_cost: 1, model_id: modelId });
    expect(result).not.toHaveProperty("relative_path");
    expect(readFileSync(join(fixture.dataRoot, ...row.relative_path.split("/")))).toEqual(png);
    evidence("TDD-WP2-GEN-001-submit-snapshot", "request.json", { ...input, newReferences: [{ bytes: png.byteLength, file_name: "reference.png", mime_type: "image/png" }] });
    evidence("TDD-WP2-GEN-001-submit-snapshot", "response.json", result);
    evidence("TDD-WP2-GEN-001-submit-snapshot", "db-diff.json", { reference_asset_id: row.managed_file_id, snapshot: JSON.parse(row.config_snapshot_json), submission_ready: row.submission_ready });
    evidence("TDD-WP2-GEN-001-submit-snapshot", "fs-after.json", { managed_files: 1, staging_files: 0 });
  });

  it("returns latest safe config and leaves storage, project, credit and outbox untouched before reconfirmation", async () => {
    const fixture = harness({ cost: 2, version: 2 });
    const stale = request(fixture.userId, { confirmedCreditCost: 1, modelConfigVersion: 1 });
    await expect(fixture.submissions.submit(stale)).rejects.toMatchObject<GenerationSubmissionError>({
      code: "model_config_stale", latest: { configVersion: 2, creditCost: 2, modelId },
    });
    const counts = fixture.storage.inspectCounts();
    expect(counts).toMatchObject({ active_reservations: 0, managed_files: 0, pending_cleanup: 0 });
    expect(fixture.registration.database.prepare("SELECT COUNT(*) AS count FROM projects").get()).toEqual({ count: 0 });
    expect(fixture.registration.database.prepare("SELECT COUNT(*) AS count FROM generation_jobs").get()).toEqual({ count: 0 });
    expect(fixture.credits.readAccount(fixture.userId)).toMatchObject({ availableBalance: 10, reservedBalance: 0 });
    const confirmed = await fixture.submissions.submit(request(fixture.userId, { confirmedCreditCost: 2, modelConfigVersion: 2 }));
    expect(confirmed).toMatchObject({ created: true, task: { confirmedCreditCost: 2, modelConfigVersion: 2 } });
    evidence("TDD-WP2-GEN-003-stale-config", "response.json", { latest: { config_version: 2, credit_cost: 2 }, status: 412 });
    evidence("TDD-WP2-GEN-003-stale-config", "db-diff.json", { before_reconfirm: { jobs: 0, projects: 0, storage: counts }, after_reconfirm: { generation_id: confirmed.task.generationId } });
    evidence("TDD-WP2-GEN-003-stale-config", "external-calls.json", { calls_before_reconfirm: 0 });
  });

  it("keeps immutable per-job reference snapshots and rejects cross-project reuse", async () => {
    const fixture = harness();
    const first = await fixture.submissions.submit(request(fixture.userId, {
      newReferences: [{ content: Readable.from(png), fileName: "first.png", mimeType: "image/png", projectedBytes: png.byteLength }],
    }));
    fixture.projects.markGenerationFailed(first.task.generationId, "upstream_failed");
    fixture.credits.finalizeGeneration({ generationId: first.task.generationId, operationKey: `generation:${first.task.generationId}:finalize`, outcome: "failed" });
    const reference = fixture.registration.database.prepare("SELECT managed_file_id FROM generation_reference_snapshots WHERE generation_id = ?")
      .get(first.task.generationId) as { managed_file_id: string };
    const second = await fixture.submissions.submit(request(fixture.userId, {
      existingReferenceAssetIds: [reference.managed_file_id], mode: "existing_project", projectId: first.task.projectId,
    }));
    expect(fixture.registration.database.prepare("SELECT COUNT(*) AS count FROM generation_reference_snapshots").get()).toEqual({ count: 2 });
    expect(() => fixture.registration.database.prepare("UPDATE generation_reference_snapshots SET managed_file_id = ? WHERE generation_id = ?")
      .run(randomUUID(), first.task.generationId)).toThrow();
    fixture.projects.markGenerationFailed(second.task.generationId, "upstream_failed");
    fixture.credits.finalizeGeneration({ generationId: second.task.generationId, operationKey: `generation:${second.task.generationId}:finalize`, outcome: "failed" });
    const otherProject = fixture.projects.createProjectForGeneration({ ownerId: fixture.userId, prompt: "另一项目", ratio: "3:4", status: "failed" });
    await expect(fixture.submissions.submit(request(fixture.userId, {
      existingReferenceAssetIds: [reference.managed_file_id], mode: "existing_project", projectId: otherProject.project.projectId,
    }))).rejects.toMatchObject({ code: "reference_invalid" });
    evidence("TDD-WP2-REF-001-reference-lifecycle", "response.json", { first: first.task.generationId, retry: second.task.generationId });
    evidence("TDD-WP2-REF-001-reference-lifecycle", "db-diff.json", { immutable_snapshots: 2, reused_asset_id: reference.managed_file_id });
    evidence("TDD-WP2-REF-001-reference-lifecycle", "fs-after.json", { managed_files: 1, private_files: 1 });
    evidence("TDD-WP2-REF-001-reference-lifecycle", "cache-enumeration.json", { cache_storage_private_entries: 0, indexed_db_private_entries: 0, local_storage_private_entries: 0 });
  });
});
