import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";

import { expect, test, type BrowserContext, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";

let vite: ViteDevServer;
let webUrl: string;

test.beforeAll(async () => {
  vite = await createServer({ configFile: resolve("apps/web/vite.config.ts"), root: resolve("apps/web"), server: { host: "127.0.0.1", port: 0 } });
  await vite.listen();
  const address = vite.httpServer?.address();
  if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
  webUrl = `http://127.0.0.1:${address.port}`;
});

test.afterAll(async () => vite.close());

const projectId = "00000000-0000-4000-8000-000000000401";
const canvasState = {
  background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: null },
  elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1,
};
const session = {
  audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
  csrf_token: "csrf-project-state-fixture-000000000000000000000000000000000",
  expires_at: "2026-09-02T08:00:00.000Z",
  user: { creator_name: "State User", role: "user", social_id: "@state_user", status: "active", user_id: "00000000-0000-4000-8000-000000000402" },
};

function projectPayload(name: string, version: number) {
  return {
    canvas_state: canvasState, created_at: "2026-08-02T08:00:00.000Z", current_image_id: null,
    draft_prompt: "多标签项目", generations: [], images: [], name, pixel_height: 1440, pixel_width: 1080,
    project_id: projectId, ratio: "3:4", save_status: "saved", state_version: version,
    status: "failed_empty", successful_image_count: 0, updated_at: "2026-08-02T08:00:00.000Z",
  };
}

async function routeSession(target: Page | BrowserContext) {
  await target.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json", status: 200 }));
}

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

async function screenshot(page: Page, caseId: string, file: string) {
  const root = process.env.DADA_EVIDENCE_DIR_PROJECT_STATE;
  if (!root) return;
  const directory = resolve(root, caseId, "screenshots");
  mkdirSync(directory, { recursive: true });
  await page.screenshot({ fullPage: true, path: resolve(directory, file) });
}

test("TDD-WP2-PROJ-002 recovers a failed debounced save without browser draft persistence", async ({ context, page }) => {
  await routeSession(context);
  let backendName = "后端已保存";
  let backendVersion = 4;
  await context.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify(projectPayload(backendName, backendVersion)), contentType: "application/json", status: 200 }));
  const timeline: Array<{ name: string; version: string | undefined }> = [];
  let attempts = 0;
  let inFlight = 0;
  let maxInFlight = 0;
  let releaseRecovery!: () => void;
  const recoveryGate = new Promise<void>((resolveRecovery) => {
    releaseRecovery = resolveRecovery;
  });
  await context.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
    attempts += 1;
    inFlight += 1;
    maxInFlight = Math.max(maxInFlight, inFlight);
    timeline.push({ name: route.request().postDataJSON().name, version: route.request().headers()["if-match"] });
    if (attempts === 1) await route.fulfill({ body: "null", contentType: "application/json", status: 503 });
    else {
      await recoveryGate;
      backendName = route.request().postDataJSON().name;
      backendVersion += 1;
      await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backendVersion }), contentType: "application/json", status: 200 });
    }
    inFlight -= 1;
  });
  await page.goto(`${webUrl}/app/projects/${projectId}`);
  const input = page.getByRole("textbox", { name: "项目名称" });
  await input.fill("第一次");
  await input.fill("一秒内最终名称");
  await expect(page.getByText("未保存", { exact: true })).toBeVisible({ timeout: 4_000 });
  await screenshot(page, "TDD-WP2-PROJ-002-save-failure-recovery", "save-failed.png");
  await page.getByRole("link", { exact: true, name: "项目" }).click();
  const leaveDialog = page.getByRole("dialog", { name: "有未保存修改" });
  await expect(leaveDialog).toBeVisible();
  await expect(leaveDialog.getByRole("button", { name: "保存并离开" })).toBeVisible();
  await expect(leaveDialog.getByRole("button", { name: "放弃修改" })).toBeVisible();
  await leaveDialog.getByRole("button", { name: "取消" }).click();
  await expect(leaveDialog).toBeHidden();
  releaseRecovery();
  await expect(page.getByText("已保存", { exact: true })).toBeVisible({ timeout: 6_000 });
  expect(timeline).toEqual([
    { name: "一秒内最终名称", version: "4" },
    { name: "一秒内最终名称", version: "4" },
  ]);
  expect(maxInFlight).toBe(1);
  await input.fill("强制关闭前的未保存修改");
  await page.close();
  const reopened = await context.newPage();
  await reopened.goto(`${webUrl}/app/projects/${projectId}`);
  await expect(reopened.getByRole("textbox", { name: "项目名称" })).toHaveValue("一秒内最终名称");
  await expect(reopened.getByText("state version 5")).toBeVisible();
  const storage = await reopened.evaluate(async () => ({
    cache_keys: "caches" in window ? await caches.keys() : [],
    indexed_db: "databases" in indexedDB ? (await indexedDB.databases()).map((entry) => entry.name) : [],
    local_storage: Object.keys(localStorage),
  }));
  expect(JSON.stringify(storage)).not.toContain("强制关闭前的未保存修改");
  writeEvidence("TDD-WP2-PROJ-002-save-failure-recovery", "cache-enumeration.json", storage);
  writeEvidence("TDD-WP2-PROJ-002-debounce", "network-timeline.json", { max_in_flight: maxInFlight, requests: timeline });
});

test("TDD-WP2-PROJ-004 turns the stale tab read-only and consumes one local export", async ({ context }) => {
  await routeSession(context);
  let backendName = "共同版本";
  let backendVersion = 1;
  const network: string[] = [];
  await context.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify(projectPayload(backendName, backendVersion)), contentType: "application/json", status: 200 }));
  await context.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
    network.push(route.request().url());
    const expected = Number(route.request().headers()["if-match"]);
    if (expected !== backendVersion) {
      await route.fulfill({ body: JSON.stringify({ latest_state_version: backendVersion, save_status: "conflicted" }), contentType: "application/json", status: 412 });
      return;
    }
    backendName = route.request().postDataJSON().name;
    backendVersion += 1;
    await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backendVersion }), contentType: "application/json", status: 200 });
  });
  const pageA = await context.newPage();
  const pageB = await context.newPage();
  await Promise.all([pageA.goto(`${webUrl}/app/projects/${projectId}`), pageB.goto(`${webUrl}/app/projects/${projectId}`)]);
  await pageA.getByRole("textbox", { name: "项目名称" }).fill("标签 A 已保存");
  await expect.poll(() => backendVersion, { timeout: 4_000 }).toBe(2);
  await expect(pageA.getByText("已保存", { exact: true })).toBeVisible({ timeout: 4_000 });
  await pageB.getByRole("textbox", { name: "项目名称" }).fill("标签 B 的本地版本");
  await expect(pageB.getByRole("heading", { name: "版本冲突" })).toBeVisible({ timeout: 4_000 });
  await expect(pageB.getByText("本页版本 1")).toBeVisible();
  await expect(pageB.getByText("最新版本 2")).toBeVisible();
  await expect(pageB.getByRole("textbox", { name: "项目名称" })).toBeDisabled();
  await expect(pageB.getByText("另存为")).toHaveCount(0);
  await expect(pageB.getByRole("link", { name: "修改并重试" })).toHaveCount(0);
  await screenshot(pageB, "TDD-WP2-PROJ-004-stale-tab", "conflicted.png");

  const download = pageB.waitForEvent("download");
  await pageB.getByRole("button", { name: "本地导出本页版本" }).click();
  await download;
  await expect(pageB.getByRole("button", { name: "本地导出本页版本" })).toBeDisabled();
  await expect(pageB.getByText("仅下载本页版本，未写入项目")).toBeVisible();
  expect(network.every((url) => !url.includes("latest") && !url.includes("export"))).toBe(true);
  await screenshot(pageB, "TDD-WP2-PROJ-004-conflict-export-once", "conflict-export.png");
  writeEvidence("TDD-WP2-PROJ-004-stale-tab", "response.json", { latest_state_version: 2, save_status: "conflicted" });
  writeEvidence("TDD-WP2-PROJ-004-stale-tab", "db-diff.json", { stale_writes: 0, version: 2 });
  writeEvidence("TDD-WP2-PROJ-004-conflict-export-once", "network-timeline.json", { requests: network });
  writeEvidence("TDD-WP2-PROJ-004-conflict-export-once", "db-diff.json", { latest_export_delta: 0, project_delta: 0, state_delta: 0 });
});
