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 { CreditError, CreditService } from "../../apps/api/src/credits.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-02T12:00:00.000Z");
const roots: string[] = [];
const closeables: Array<{ close(): void }> = [];

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

function harness(available: number) {
  const root = mkdtempSync(join(tmpdir(), "dada-wp2-04-credit-"));
  roots.push(root);
  const databasePath = join(root, "dada.sqlite3");
  const registration = new RegistrationService({
    challengePepper: Buffer.alloc(32, 0x31), clock: () => now,
    currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
    invitePepper: Buffer.alloc(32, 0x32), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x33),
  });
  const projects = new ProjectService({ clock: () => now, databasePath });
  const credits = new CreditService({ clock: () => now, databasePath });
  closeables.push(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 (?, 'Credit User', '@credit_user')").run(userId);
  registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, ?, 0, ?)")
    .run(userId, available, now);
  return { credits, databasePath, projects, registration, root, userId };
}

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("TDD-WP2-CRED-001", () => {
  it("reserves available credit exactly once and appends an immutable ledger event", () => {
    const { credits, projects, registration, userId } = harness(10);
    const created = projects.createProjectForGeneration({ ownerId: userId, prompt: "点数冻结", ratio: "3:4", status: "queued" });
    const generationId = created.generation.generationId;
    const operationKey = `generation:${generationId}:reserve`;
    const request = { creditCost: 1, generationId, modelId: "gemini-3.1-flash-image-preview", operationKey, userId };
    const first = credits.reserveGeneration(request);
    expect(() => credits.reserveGeneration({ ...request, modelId: "gemini-3-pro-image-preview" }))
      .toThrowError(expect.objectContaining<CreditError>({ code: "credit_operation_conflict" }));
    const replay = credits.reserveGeneration(request);
    expect(first).toEqual({ availableBalance: 9, reservedBalance: 1, status: "reserved" });
    expect(replay).toEqual(first);
    expect(credits.readAccount(userId)).toMatchObject({ availableBalance: 9, reservedBalance: 1 });
    expect(registration.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger WHERE entry_type = 'generation_reserve'").get()).toEqual({ count: 1 });
    expect(registration.database.prepare("SELECT COUNT(*) AS count FROM outbox_events WHERE aggregate_id = ?").get(generationId)).toEqual({ count: 1 });
    expect(registration.database.prepare("SELECT reserved_credits, confirmed_credit_cost, final_credit_state FROM generation_jobs WHERE generation_id = ?").get(generationId))
      .toEqual({ confirmed_credit_cost: 1, final_credit_state: null, reserved_credits: 1 });
    expect(() => registration.database.prepare("UPDATE credit_ledger SET amount = 99 WHERE operation_key = ?").run(operationKey)).toThrow();
    evidence("TDD-WP2-CRED-001-reserve", "request.json", request);
    evidence("TDD-WP2-CRED-001-reserve", "response.json", first);
    evidence("TDD-WP2-CRED-001-reserve", "db-diff.json", { account: credits.readAccount(userId), ledger_rows: 1, outbox_rows: 1 });
  });

  it("rejects insufficient available credit with no job, ledger, outbox or balance side effect", () => {
    const { credits, registration, userId } = harness(0);
    const generationId = randomUUID();
    expect(() => credits.reserveGeneration({
      creditCost: 1, generationId, modelId: "gemini-3.1-flash-image-preview",
      operationKey: `generation:${generationId}:reserve`, userId,
    })).toThrowError(expect.objectContaining<CreditError>({ code: "credit_insufficient" }));
    const counts = {
      jobs: (registration.database.prepare("SELECT COUNT(*) AS count FROM generation_jobs").get() as { count: number }).count,
      ledger: (registration.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger").get() as { count: number }).count,
      outbox: (registration.database.prepare("SELECT COUNT(*) AS count FROM outbox_events").get() as { count: number }).count,
    };
    expect(counts).toEqual({ jobs: 0, ledger: 0, outbox: 0 });
    expect(credits.readAccount(userId)).toMatchObject({ availableBalance: 0, reservedBalance: 0 });
    evidence("TDD-WP2-CRED-001-insufficient", "response.json", { available_balance: 0, required_credits: 1, status: "insufficient" });
    evidence("TDD-WP2-CRED-001-insufficient", "db-diff.json", { account: credits.readAccount(userId), ...counts });
    evidence("TDD-WP2-CRED-001-insufficient", "external-calls.json", { calls: 0, staging_files: 0, storage_reservations: 0 });
  });
});

describe("TDD-WP2-CRED-002-admin-adjustment", () => {
  it("changes only available, replays by adjustment id, and rolls balance, ledger and audit back together", () => {
    const { credits, projects, registration, userId } = harness(1);
    const generation = projects.createProjectForGeneration({ ownerId: userId, prompt: "活动任务", ratio: "1:1", status: "queued" }).generation;
    credits.reserveGeneration({ creditCost: 1, generationId: generation.generationId, modelId: "gemini-3.1-flash-image-preview", operationKey: `generation:${generation.generationId}:reserve`, userId });
    const adminId = randomUUID();
    registration.database.prepare(`
      INSERT INTO users (user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at)
      VALUES (?, 'admin-credit@example.invalid', 'super_admin', 'active', 0, ?, ?)
    `).run(adminId, randomUUID(), now);
    registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(adminId);
    const adjustmentId = randomUUID();
    const idempotencyKey = `admin-credit-${randomUUID()}-${randomUUID()}`;
    const first = credits.adjustAvailable({ adjustmentId, adminId, amount: -5, idempotencyKey, reason: "测试额度校正", userId });
    const replay = credits.adjustAvailable({ adjustmentId, adminId, amount: -5, idempotencyKey, reason: "测试额度校正", userId });
    expect(first).toEqual({ adjustmentId, availableBalance: -5, reservedBalance: 1, status: "adjusted" });
    expect(replay).toEqual(first);
    expect(registration.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger WHERE entry_type = 'admin_adjustment'").get()).toEqual({ count: 1 });
    expect(registration.database.prepare("SELECT COUNT(*) AS count FROM admin_operation_logs WHERE operation_type = 'credit_adjustment'").get()).toEqual({ count: 1 });
    expect(() => credits.adjustAvailable({ adjustmentId: randomUUID(), adminId, amount: 1, idempotencyKey: `admin-credit-${randomUUID()}-${randomUUID()}`, reason: "   ", userId }))
      .toThrowError(expect.objectContaining<CreditError>({ code: "credit_request_invalid" }));

    registration.database.exec(`CREATE TRIGGER fail_credit_adjustment_audit BEFORE INSERT ON admin_operation_logs
      WHEN NEW.operation_type = 'credit_adjustment' BEGIN SELECT RAISE(ABORT, 'forced_audit_failure'); END;`);
    expect(() => credits.adjustAvailable({ adjustmentId: randomUUID(), adminId, amount: 2, idempotencyKey: `admin-credit-${randomUUID()}-${randomUUID()}`, reason: "审计失败回滚", userId })).toThrow();
    registration.database.exec("DROP TRIGGER fail_credit_adjustment_audit");
    expect(credits.readAccount(userId)).toMatchObject({ availableBalance: -5, reservedBalance: 1 });
    const stored = registration.database.prepare(`
      SELECT amount, reason, available_before, available_after, reserved_before, reserved_after
      FROM credit_ledger WHERE entry_type = 'admin_adjustment'
    `).get();
    evidence("TDD-WP2-CRED-002-admin-adjustment", "db-diff.json", { account: credits.readAccount(userId), audit_rows: 1, ledger: stored });
  });
});
