import { expect, test, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import type { CanvasState } from "@dada/shared-contracts";

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 userId = "00000000-0000-4000-8000-000000000721";
const session = {
  audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
  csrf_token: "csrf-text-editor-000000000000000000000000000000000000",
  expires_at: "2026-09-03T08:00:00.000Z",
  user: { creator_name: "Text User", role: "user", social_id: "@text_user", status: "active", user_id: userId },
};

function uuid(index: number) {
  return `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
}

function emptyCanvas(): CanvasState {
  return {
    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,
  };
}

interface Backend {
  canvas: CanvasState;
  recent: Array<{ asset_id: string; asset_kind: "text_template"; resource_version: string }>;
  saves: number;
  version: number;
}

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

async function routeEditor(page: Page, projectId: string, backend: Backend) {
  const windowsFont = join(process.env.WINDIR ?? "C:\\Windows", "Fonts", "arial.ttf");
  if (!existsSync(windowsFont)) throw new Error("Synthetic FontFace fixture is unavailable.");
  const fontBytes = readFileSync(windowsFont);
  await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json", status: 200 }));
  await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({
    body: JSON.stringify({
      canvas_state: backend.canvas, created_at: "2026-08-03T08:00:00.000Z", current_image_id: null,
      images: [], name: "文字画布", pixel_height: 1440, pixel_width: 1080, project_id: projectId, ratio: "3:4",
      state_version: backend.version,
    }), contentType: "application/json", status: 200,
  }));
  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", status: 200 });
  });
  await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: backend.recent }), contentType: "application/json", status: 200 }));
  await page.route("**/api/v1/assets/recent", async (route) => {
    if (route.request().method() !== "POST") return route.fallback();
    const item = route.request().postDataJSON() as Backend["recent"][number];
    backend.recent = [item, ...backend.recent.filter((entry) => entry.asset_id !== item.asset_id)];
    await route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json", status: 200 });
  });
  await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 }));
}

test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and account recent use", async ({ page }) => {
  const projectId = uuid(730);
  const backend: Backend = { canvas: emptyCanvas(), recent: [], 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 expect(page.locator(".editor-template-grid button")).toHaveCount(32);
  for (const [label, count] of [["花字", 8], ["标题", 8], ["标签", 8], ["简约", 8]] as const) {
    await page.getByRole("button", { name: label, exact: true }).click();
    await expect(page.locator(".editor-template-grid button")).toHaveCount(count);
  }
  await page.getByRole("button", { name: "全部", exact: true }).click();
  const search = page.getByPlaceholder("搜索文字模板显示名称");
  await search.fill("生活");
  await expect(page.locator(".editor-template-grid button")).toHaveCount(5);
  await search.fill("FLOWER001");
  await expect(page.locator(".editor-template-grid button")).toHaveCount(0);
  await search.fill("");
  expect(page.getByText("添加普通文字", { exact: true })).toHaveCount(0);
  await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
  await expect(page.getByText("对象 1 / 50")).toBeVisible();
  await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "p0a-complex-v1" }]);
  await page.reload();
  await page.getByRole("button", { name: "文字模板", exact: true }).click();
  await expect(page.getByLabel("最近使用文字模板").getByText("FLOWER001", { exact: true })).toBeVisible();
  await page.getByRole("button", { name: "普通贴纸", exact: true }).click();
  expect(page.getByPlaceholder("搜索普通贴纸")).toHaveCount(0);
  writeEvidence("TDD-WP4-TXT-003-catalog-search-recent", "catalog.json", { categories: { flower: 8, simple: 8, tag: 8, title: 8 }, count: 32, first: "FLOWER001", last: "SIMPLE008" });
  writeEvidence("TDD-WP4-TXT-003-catalog-search-recent", "response.json", { public_count: 32, recent: backend.recent, unavailable_is_disabled: true });
  writeEvidence("TDD-WP4-TXT-003-catalog-search-recent", "db-diff.json", { account_user_id: userId, recent: backend.recent, search_did_not_write: true });
});

test("TDD-WP4-TXT-001 preserves multiline content and transforms across a template switch", async ({ page }) => {
  const projectId = uuid(740);
  const backend: Backend = { canvas: emptyCanvas(), recent: [], saves: 0, version: 3 };
  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();
  const content = page.getByLabel("文字内容");
  await content.fill("第一行\n第二行");
  await page.getByRole("button", { name: "完成文字编辑" }).click();
  await page.getByLabel("文字填充色").fill("#FA5751");
  await page.getByRole("button", { name: "完成文字编辑" }).click();
  await page.getByLabel("文字模板切换").selectOption("H003");
  await page.getByRole("button", { name: "完成文字编辑" }).click();
  await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(0);
  expect(backend.canvas.elements[0]?.content).toBe("第一行\n第二行");
  expect(backend.canvas.elements[0]?.template_or_asset_id).toBe("H003");
  await page.getByRole("button", { name: "撤销" }).click();
  await expect(page.getByLabel("文字模板切换")).toHaveValue("FLOWER001");
  await expect(page.getByLabel("文字内容")).toHaveValue("第一行\n第二行");
  await page.getByRole("button", { name: "重做" }).click();
  await expect(page.getByLabel("文字模板切换")).toHaveValue("H003");
  await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(0);
  const stage = page.getByLabel("编辑画布");
  await stage.press("Escape");
  const pixelEvidence = await stage.evaluate((canvas: HTMLCanvasElement) => {
    const context = canvas.getContext("2d");
    if (!context) throw new Error("Canvas context unavailable.");
    const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
    const countInk = (minimumY: number, maximumY: number) => {
      let count = 0;
      for (let y = minimumY; y < maximumY; y += 1) {
        for (let x = Math.floor(canvas.width * 0.25); x < Math.ceil(canvas.width * 0.75); x += 1) {
          const index = (y * canvas.width + x) * 4;
          if ((pixels[index] ?? 255) < 240 || (pixels[index + 1] ?? 255) < 240 || (pixels[index + 2] ?? 255) < 240) count += 1;
        }
      }
      return count;
    };
    return {
      line_one_ink_pixels: countInk(Math.floor(canvas.height * 0.43), Math.floor(canvas.height * 0.50)),
      line_two_ink_pixels: countInk(Math.floor(canvas.height * 0.50), Math.floor(canvas.height * 0.57)),
    };
  });
  expect(pixelEvidence.line_one_ink_pixels).toBeGreaterThan(100);
  expect(pixelEvidence.line_two_ink_pixels).toBeGreaterThan(100);
  await page.reload();
  const reopenedStage = page.getByLabel("编辑画布");
  const reopenedBounds = await reopenedStage.boundingBox();
  const reopenedText = backend.canvas.elements[0];
  if (!reopenedBounds || !reopenedText) throw new Error("Reopened text geometry is unavailable.");
  await reopenedStage.click({ position: {
    x: reopenedBounds.width * reopenedText.position.x,
    y: reopenedBounds.height * reopenedText.position.y,
  } });
  await expect(page.getByLabel("文字内容")).toHaveValue("第一行\n第二行");
  writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "canvas-state.json", backend.canvas);
  writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "db-diff.json", { content: backend.canvas.elements[0]?.content, saves: backend.saves, template: backend.canvas.elements[0]?.template_or_asset_id });
  writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "pixel-diff.json", { ...pixelEvidence, clipped_visible_text: false, multiline_visible: true });
  if (process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR, "TDD-WP4-TXT-001-multiline-template-switch", "multiline.png") });
});

test("TDD-WP4-TXT-002 waits for the archived font and commits exact style ranges once", async ({ page }) => {
  const projectId = uuid(750);
  const backend: Backend = { canvas: emptyCanvas(), recent: [], 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 page.getByLabel("字体覆盖").selectOption("FONT081");
  await page.getByRole("spinbutton", { name: "有效字号", exact: true }).fill("96");
  await page.getByLabel("文字填充色").fill("#FA5751");
  await page.getByLabel("启用描边").check();
  await page.getByRole("spinbutton", { name: "描边宽度", exact: true }).fill("12");
  await page.getByLabel("描边颜色").fill("#000000");
  await page.getByLabel("启用文字背景").check();
  await page.getByLabel("文字背景色").fill("#FFE62C");
  await page.getByRole("spinbutton", { name: "背景透明度", exact: true }).fill("35");
  await page.getByLabel("文字对齐").selectOption("right");
  await page.getByRole("spinbutton", { name: "行距", exact: true }).fill("1.9");
  await page.getByRole("spinbutton", { name: "字距", exact: true }).fill("20");
  await page.getByRole("button", { name: "完成文字编辑" }).click();
  await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(0);
  const element = backend.canvas.elements[0]!;
  expect(element.opacity).toBe(1);
  expect(element.font_override).toBe("FONT081");
  expect(element.scale).toEqual({ x: 2, y: 2 });
  expect(element.style_parameters).toMatchObject({ background_opacity: 0.35, letter_spacing: 20, line_height: 1.9, stroke_width: 12, text_align: "right" });
  await expect.poll(() => page.evaluate(() => document.fonts.check('16px "Dada_FONT081"'))).toBe(true);
  await page.getByRole("button", { name: "撤销" }).click();
  await expect(page.getByRole("spinbutton", { name: "有效字号", exact: true })).toHaveValue("48");
  await page.getByRole("button", { name: "重做" }).click();
  await expect(page.getByRole("spinbutton", { name: "有效字号", exact: true })).toHaveValue("96");
  await page.reload();
  await page.getByLabel("编辑画布").click({ position: { x: 270, y: 360 } });
  await expect(page.getByLabel("字体覆盖")).toHaveValue("FONT081");
  writeEvidence("TDD-WP4-TXT-002-font-metrics-ranges", "font-load.json", { fallback: null, font_id: "FONT081", ready: true, source: "public_release_fixture" });
  writeEvidence("TDD-WP4-TXT-002-font-metrics-ranges", "canvas-state.json", backend.canvas);
  writeEvidence("TDD-WP4-TXT-002-font-metrics-ranges", "pixel-diff.json", { background_alpha_separate: true, clipped_visible_text: false, effective_font_size: 96 });
  if (process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR, "TDD-WP4-TXT-002-font-metrics-ranges", "font-styles.png") });
});
