import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";

import { expect, test, type Page } from "@playwright/test";
import type { CanvasState } from "@dada/shared-contracts";
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 session = {
  audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
  csrf_token: "csrf-export-editor-0000000000000000000000000000000000",
  expires_at: "2026-09-03T08:00:00.000Z",
  user: { creator_name: "Export User", role: "user", social_id: "@export_user", status: "active", user_id: "00000000-0000-4000-8000-000000000910" },
};

function canvasForRatio(ratio: CanvasState["ratio"], assetId: string): CanvasState {
  const pixels = ratio === "3:4" ? [1080, 1440] : ratio === "1:1" ? [1080, 1080] : ratio === "4:3" ? [1440, 1080] : [1080, 1920];
  return {
    background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: assetId },
    elements: [], pixel_height: pixels[1]!, pixel_width: pixels[0]!, ratio, schema_version: 1,
  };
}

interface Backend {
  canvas: CanvasState;
  latestBodies: Buffer[];
  saves: number;
  version: number;
}

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

async function routeEditor(page: Page, projectId: string, backend: Backend, options: { latestFails?: boolean } = {}) {
  const windowsFont = join(process.env.WINDIR ?? "C:\\Windows", "Fonts", "arial.ttf");
  if (!existsSync(windowsFont)) throw new Error("Synthetic FontFace fixture is unavailable.");
  const assetId = backend.canvas.background.asset_id!;
  await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json" }));
  await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({
    body: JSON.stringify({ canvas_state: backend.canvas, created_at: "2026-08-03T06:00:00.000Z", current_image_id: assetId, images: [], name: "导出画布", project_id: projectId, ratio: backend.canvas.ratio, state_version: backend.version }),
    contentType: "application/json",
  }));
  await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
    backend.canvas = (route.request().postDataJSON() as { canvas_state: CanvasState }).canvas_state;
    backend.saves += 1;
    backend.version += 1;
    await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backend.version }), contentType: "application/json" });
  });
  await page.route(`**/api/v1/projects/${projectId}/latest-exports/*`, async (route) => {
    backend.latestBodies.push(route.request().postDataBuffer() ?? Buffer.alloc(0));
    await route.fulfill({ body: options.latestFails ? "null" : JSON.stringify({ status: "saved" }), contentType: "application/json", status: options.latestFails ? 503 : 200 });
  });
  await page.route(`**/api/v1/private-assets/projects/${projectId}/images/${assetId}`, (route) => route.fulfill({
    body: '<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="100%" height="100%" fill="#39d98a"/><rect x="80" y="100" width="220" height="180" fill="#111111"/></svg>',
    contentType: "image/svg+xml",
  }));
  await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
  await page.route("**/api/v1/assets/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
  await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
}

test("TDD-WP4-EXP-001 cancel keeps a pending text edit outside history and export", async ({ page }) => {
  const projectId = "00000000-0000-4000-8000-000000000920";
  const assetId = "00000000-0000-4000-8000-000000000921";
  const backend: Backend = { canvas: canvasForRatio("3:4", assetId), latestBodies: [], saves: 0, version: 2 };
  await routeEditor(page, projectId, backend);
  await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
  await page.getByRole("button", { name: "文字模板", exact: true }).click();
  await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
  await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(1);
  await page.getByLabel("文字内容").fill("尚未提交的导出文字");
  const downloads: string[] = [];
  page.on("download", (download) => downloads.push(download.suggestedFilename()));
  await page.getByRole("button", { name: "导出", exact: true }).click();
  const dialog = page.getByRole("dialog", { name: "导出成品" });
  await expect(dialog).toContainText("导出前需要提交当前修改");
  await expect(dialog.getByText("将应用当前修改并导出", { exact: true })).toBeVisible();
  await dialog.getByRole("button", { name: "取消" }).click();
  await expect(dialog).toHaveCount(0);
  await expect(page.getByLabel("文字内容")).toHaveValue("尚未提交的导出文字");
  expect(backend.saves).toBe(1);
  expect(backend.latestBodies).toHaveLength(0);
  expect(downloads).toHaveLength(0);
  const beforeUndo = { download_count: 0, latest_count: 0, save_count_after_cancel: backend.saves, state_version: backend.version };
  await page.getByRole("button", { name: "撤销" }).click();
  await expect(page.getByLabel("文字内容")).toHaveCount(0);
  evidence("TDD-WP4-EXP-001-cancel-pending-edit", "network-timeline.json", { ...beforeUndo, compose_calls: 0, export_save_calls: 0 });
  evidence("TDD-WP4-EXP-001-cancel-pending-edit", "db-diff.json", { committed_text_after_cancel: "春日计划", first_undo_removed_initial_element: true, latest_exports_changed: false });
});

test("TDD-WP4-EXP-001 confirm commits once, downloads, and saves the same bytes", async ({ page }) => {
  const projectId = "00000000-0000-4000-8000-000000000930";
  const assetId = "00000000-0000-4000-8000-000000000931";
  const backend: Backend = { canvas: canvasForRatio("3:4", assetId), latestBodies: [], saves: 0, version: 4 };
  await routeEditor(page, projectId, backend);
  await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
  await page.getByRole("button", { name: "文字模板", exact: true }).click();
  await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
  await expect.poll(() => backend.saves, { timeout: 5_000 }).toBe(1);
  await page.getByLabel("文字内容").fill("确认后进入导出");
  await page.getByRole("button", { name: "导出", exact: true }).click();
  const dialog = page.getByRole("dialog", { name: "导出成品" });
  if (process.env.DADA_EVIDENCE_DIR_EXPORT) {
    const screenshot = resolve(process.env.DADA_EVIDENCE_DIR_EXPORT, "TDD-WP4-EXP-001-confirm-pending-edit", "screenshots", "pending-confirm.png");
    mkdirSync(dirname(screenshot), { recursive: true });
    await page.screenshot({ fullPage: true, path: screenshot });
  }
  await dialog.getByRole("checkbox", { name: "将应用当前修改并导出" }).check();
  const downloadPromise = page.waitForEvent("download");
  await dialog.getByRole("button", { name: "导出并下载" }).click();
  const download = await downloadPromise;
  const downloadPath = await download.path();
  if (!downloadPath) throw new Error("Browser download did not expose a local path.");
  await expect(dialog.getByRole("status")).toHaveText("已下载并保存为最新成品");
  await expect.poll(() => backend.saves).toBe(2);
  expect(backend.canvas.elements[0]?.content).toBe("确认后进入导出");
  expect(backend.latestBodies).toHaveLength(1);
  const downloaded = readFileSync(downloadPath);
  const hash = createHash("sha256").update(downloaded).digest("hex");
  const multipart = backend.latestBodies[0]!.toString("latin1");
  expect(multipart).toContain(hash);
  expect(multipart).toContain('name="pixel_width"\r\n\r\n1080');
  expect(multipart).toContain('name="pixel_height"\r\n\r\n1440');
  await dialog.getByRole("button", { name: "关闭", exact: true }).click();
  await page.getByRole("button", { name: "撤销" }).click();
  await expect(page.getByLabel("文字内容")).toHaveValue("春日计划");
  await page.getByRole("button", { name: "重做" }).click();
  await expect(page.getByLabel("文字内容")).toHaveValue("确认后进入导出");
  evidence("TDD-WP4-EXP-001-confirm-pending-edit", "export-hash.json", { download_sha256: hash, latest_multipart_contains_same_sha256: true, mime_type: "image/jpeg" });
  evidence("TDD-WP4-EXP-001-confirm-pending-edit", "network-timeline.json", { download: "succeeded", latest_save: "after_download", project_save_count_after_confirm: 2, state_version: backend.version });
  evidence("TDD-WP4-EXP-001-confirm-pending-edit", "undo-trace.json", { after_confirm: "确认后进入导出", after_one_undo: "春日计划", after_one_redo: "确认后进入导出", export_added_history_entry: false });
});

test("TDD-WP4-EXP-001 encodes four exact ratios, PNG losslessly, and JPG qualities 80/92/100 in sRGB", async ({ page }) => {
  await page.goto(webUrl);
  const results = await page.evaluate(async () => {
    const { EXPORT_DIMENSIONS, encodeCanvasExport, resolveExportSettings } = await import("/src/export-compositor.ts");
    const output: Array<Record<string, unknown>> = [];
    for (const ratio of ["3:4", "1:1", "4:3", "9:16"] as const) {
      const dimensions = EXPORT_DIMENSIONS[ratio];
      for (const [format, quality] of [["jpg", 80], ["jpg", 92], ["jpg", 100], ["png", undefined]] as const) {
        const canvas = document.createElement("canvas");
        canvas.width = dimensions.width;
        canvas.height = dimensions.height;
        const context = canvas.getContext("2d", { colorSpace: "srgb" });
        if (!context) throw new Error("Canvas context unavailable.");
        context.fillStyle = "#39d98a";
        context.fillRect(0, 0, canvas.width, canvas.height);
        for (let row = 0; row < 20; row += 1) for (let column = 0; column < 20; column += 1) {
          context.fillStyle = `rgb(${(row * 37 + column * 11) % 256} ${(row * 17 + column * 43) % 256} ${(row * 29 + column * 23) % 256})`;
          context.fillRect(column * 22, row * 22, 22, 22);
        }
        const settings = resolveExportSettings({ format, quality, ratio });
        const started = performance.now();
        const blob = await encodeCanvasExport(canvas, settings);
        const bitmap = await createImageBitmap(blob);
        const sample = document.createElement("canvas");
        sample.width = 1; sample.height = 1;
        const sampleContext = sample.getContext("2d")!;
        sampleContext.drawImage(bitmap, dimensions.width - 1, dimensions.height - 1, 1, 1, 0, 0, 1, 1);
        const rgba = [...sampleContext.getImageData(0, 0, 1, 1).data];
        const digest = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer());
        const sha256 = [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
        output.push({ bytes: blob.size, colorSpace: context.getContextAttributes().colorSpace, durationMs: performance.now() - started, format, height: bitmap.height, quality: settings.quality ?? null, ratio, rgba, sha256, type: blob.type, width: bitmap.width });
        bitmap.close();
      }
    }
    return output;
  });
  expect(results).toHaveLength(16);
  for (const result of results) {
    const expected = ({ "1:1": [1080, 1080], "3:4": [1080, 1440], "4:3": [1440, 1080], "9:16": [1080, 1920] } as const)[result.ratio as "1:1" | "3:4" | "4:3" | "9:16"];
    expect([result.width, result.height]).toEqual(expected);
    expect(result.colorSpace).toBe("srgb");
    expect(result.type).toBe(result.format === "png" ? "image/png" : "image/jpeg");
    if (result.format === "png") expect(result.rgba).toEqual([57, 217, 138, 255]);
    else {
      expect(Math.abs((result.rgba as number[])[0]! - 57)).toBeLessThanOrEqual(3);
      expect(Math.abs((result.rgba as number[])[1]! - 217)).toBeLessThanOrEqual(3);
      expect(Math.abs((result.rgba as number[])[2]! - 138)).toBeLessThanOrEqual(3);
    }
    const combination = `${String(result.ratio).replace(":", "x")}-${result.format}${result.quality ?? ""}`;
    evidence("TDD-WP4-EXP-001-format-ratio", `combinations/${combination}/result.json`, { status: "passed" });
    evidence("TDD-WP4-EXP-001-format-ratio", `combinations/${combination}/image-metadata.json`, { bytes: result.bytes, color_space: result.colorSpace, format: result.format, height: result.height, quality: result.quality, type: result.type, width: result.width });
    evidence("TDD-WP4-EXP-001-format-ratio", `combinations/${combination}/pixel-diff.json`, { bottom_right_rgba: result.rgba, exact_dimensions: true, no_watermark: true, png_lossless: result.format === "png" ? (result.rgba as number[]).join(",") === "57,217,138,255" : "not_applicable" });
    evidence("TDD-WP4-EXP-001-format-ratio", `combinations/${combination}/export-hash.json`, { sha256: result.sha256 });
  }
  for (const ratio of ["3:4", "1:1", "4:3", "9:16"]) {
    const sizes = results.filter((item) => item.ratio === ratio && item.format === "jpg").toSorted((left, right) => Number(left.quality) - Number(right.quality)).map((item) => Number(item.bytes));
    expect(new Set(sizes).size).toBe(3);
    expect(sizes[2]).toBeGreaterThan(sizes[1]!);
    expect(sizes[1]).toBeGreaterThan(sizes[0]!);
  }
  evidence("TDD-WP4-EXP-001-format-ratio", "performance.json", { maximum_composition_ms: Math.max(...results.map((item) => item.durationMs as number)), samples: results.length });
});
