import { expect, test, type Page, type TestInfo } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";

// The fixture is plain ESM so the same immutable contract is consumed by Node and Playwright.
// @ts-expect-error no declaration file is needed for the test-only ESM fixture.
import {
  WP4_07_PERFORMANCE_BUDGETS,
  WP4_07_REAL_RESOURCE_VERSIONS,
  WP4_07_RED_RESOURCE_VERSION,
  WP4_07_REQUIRED_FONT_IDS,
  assertWp407Fixture,
  createWp407CanvasFixture,
  wp407FixtureSha256,
} from "../visual-performance/wp4-07-fixture.mjs";
// @ts-expect-error no declaration file is needed for the Node-only archive loader.
import { loadWp407RealAssets } from "../visual-performance/wp4-07-real-assets.mjs";

let vite: ViteDevServer | undefined;
let webUrl: string;

const projectId = "00000000-0000-4000-8000-000000004070";
const harnessMode = process.env.DADA_WP4_07_HARNESS_MODE;
if (!new Set(["red_contract", "real_archive"]).has(harnessMode ?? "")) throw new Error("WP4_07_HARNESS_MODE_REQUIRED");
const greenEligible = harnessMode === "real_archive";
const fixtureVersions = greenEligible ? WP4_07_REAL_RESOURCE_VERSIONS : WP4_07_RED_RESOURCE_VERSION;
const fixedCanvas = createWp407CanvasFixture(fixtureVersions);
const realAssets = greenEligible ? loadWp407RealAssets() : undefined;

test.beforeAll(async () => {
  assertWp407Fixture(fixtureVersions);
  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-wp4-07-red-contract-000000000000000000000000",
  expires_at: "2026-09-03T08:00:00.000Z",
  user: {
    creator_name: "WP4-07 Archive",
    role: "user",
    social_id: "@dada_fixture",
    status: "active",
    user_id: "00000000-0000-4000-8000-000000004071",
  },
};

interface BackendState {
  canvas: typeof fixedCanvas;
  latestSaves: number;
  projectSaves: number;
  stateVersion: number;
}

function evidencePath(testInfo: TestInfo, filename: string) {
  const root = process.env.DADA_WP4_07_EVIDENCE_DIR;
  if (!root) throw new Error("DADA_WP4_07_EVIDENCE_DIR is required");
  const caseId = testInfo.title.startsWith("TDD-WP4-VIS-001")
    ? "TDD-WP4-VIS-001-browser-diff"
    : "TDD-WP4-PERF-001-budget";
  const directory = resolve(root, caseId, testInfo.project.name);
  mkdirSync(directory, { recursive: true });
  return resolve(directory, filename);
}

function writeEvidence(testInfo: TestInfo, filename: string, value: unknown) {
  writeFileSync(evidencePath(testInfo, filename), `${JSON.stringify(value, null, 2)}\n`);
}

function svgForAsset(assetId: string) {
  let hash = 0;
  for (const character of assetId) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
  const fill = `#${(hash & 0xffffff).toString(16).padStart(6, "0")}`;
  const accent = `#${((hash ^ 0xf2f400) & 0xffffff).toString(16).padStart(6, "0")}`;
  return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" fill="${fill}"/><path d="M20 120L80 20l60 100z" fill="${accent}"/><text x="80" y="145" text-anchor="middle" font-family="Arial" font-size="12" fill="#fff">${assetId.replaceAll("&", "")}</text></svg>`;
}

async function routeEditor(page: Page, backend: BackendState) {
  const fontBytes = greenEligible ? undefined : readFileSync("C:\\Windows\\Fonts\\arial.ttf");
  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-07-27T04:00:00.000Z",
      current_image_id: fixedCanvas.background.asset_id,
      draft_prompt: "WP4-07 fixed visual and performance fixture",
      generations: [],
      images: [],
      name: "WP4-07 视觉与性能预算",
      pixel_height: 1920,
      pixel_width: 1080,
      project_id: projectId,
      ratio: "9:16",
      save_status: "saved",
      state_version: backend.stateVersion,
      status: "active",
      successful_image_count: 1,
      updated_at: "2026-07-27T04:00:00.000Z",
    }),
    contentType: "application/json",
    status: 200,
  }));
  await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
    backend.canvas = (route.request().postDataJSON() as { canvas_state: typeof fixedCanvas }).canvas_state;
    backend.projectSaves += 1;
    backend.stateVersion += 1;
    await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: backend.stateVersion }), contentType: "application/json", status: 200 });
  });
  await page.route(`**/api/v1/projects/${projectId}/latest-exports/*`, async (route) => {
    backend.latestSaves += 1;
    await route.fulfill({ body: JSON.stringify({ status: "saved" }), contentType: "application/json", status: 200 });
  });
  await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => {
    if (realAssets) return route.fulfill({ body: readFileSync(realAssets.background.path), contentType: realAssets.background.content_type, status: 200 });
    return route.fulfill({
      body: `<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="1080" height="1920" fill="#30343b"/><rect x="72" y="80" width="936" height="1760" fill="#f7f7f5"/><path d="M72 1520L430 960l260 290 318-480v1070H72z" fill="#1769aa"/><circle cx="790" cy="420" r="210" fill="#f2f400"/></svg>`,
      contentType: "image/svg+xml",
      status: 200,
    });
  });
  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/**", (route) => {
    const url = new URL(route.request().url());
    const parts = decodeURIComponent(url.pathname).split("/").filter(Boolean);
    const assetId = parts.at(-1) ?? "asset";
    const resourceVersion = parts.at(-2) ?? "missing";
    if (realAssets) {
      const expectedVersion = assetId.startsWith("STK") ? WP4_07_REAL_RESOURCE_VERSIONS.static : WP4_07_REAL_RESOURCE_VERSIONS.complex;
      const asset = realAssets.publicAssets.get(assetId);
      if (resourceVersion !== expectedVersion || !asset) return route.fulfill({ status: 404 });
      return route.fulfill({ body: readFileSync(asset.path), contentType: asset.content_type, status: 200 });
    }
    if (resourceVersion === "missing-fixture") return route.fulfill({ status: 404 });
    if (WP4_07_REQUIRED_FONT_IDS.some((fontId: string) => url.pathname.endsWith(`/${fontId}`))) {
      return route.fulfill({ body: fontBytes!, contentType: "font/ttf", status: 200 });
    }
    return route.fulfill({ body: svgForAsset(assetId), contentType: "image/svg+xml", status: 200 });
  });
}

async function prepareEditor(page: Page, backend: BackendState) {
  await routeEditor(page, backend);
  await page.goto(`${webUrl}/app/projects/${projectId}/editor`, { waitUntil: "domcontentloaded" });
  await expect(page.getByText("对象 50 / 50")).toBeVisible();
  await page.evaluate(() => document.fonts.ready);
  await page.waitForTimeout(250);
}

function percentile(values: readonly number[], ratio: number) {
  if (values.length === 0) return 0;
  const sorted = [...values].sort((left, right) => left - right);
  return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))]!;
}

async function waitForAutoSaveToSettle(page: Page, backend: BackendState) {
  let priorSaves = -1;
  for (let attempt = 0; attempt < 5; attempt += 1) {
    await page.waitForTimeout(1_100);
    if (backend.projectSaves === priorSaves) return;
    priorSaves = backend.projectSaves;
  }
  throw new Error("autosave queue did not settle before export failure isolation");
}

test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", async ({ context, page }, testInfo) => {
  test.skip(process.env.DADA_WP4_07_LAYER === "performance", "visual layer not requested");
  const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 7 };
  await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
  await prepareEditor(page, backend);
  await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
  const layoutSelectors = {
    canvas_frame: ".editor-canvas-frame",
    canvas_surface: ".editor-canvas",
    footer: ".editor-statusbar",
    left_panel: ".editor-assets-panel",
    right_panel: ".editor-inspector",
    toolbar: ".editor-toolbar",
    workspace: ".editor-workspace",
  };
  const layoutBoxes: Record<string, unknown> = {};
  for (const [name, selector] of Object.entries(layoutSelectors)) layoutBoxes[name] = await page.locator(selector).boundingBox();
  const frameBox = await page.locator(layoutSelectors.canvas_frame).boundingBox();
  const surfaceBox = await page.locator(layoutSelectors.canvas_surface).boundingBox();
  expect(frameBox).not.toBeNull();
  expect(surfaceBox).not.toBeNull();
  expect(Math.abs(frameBox!.width - surfaceBox!.width)).toBeLessThanOrEqual(2);
  expect(Math.abs(frameBox!.height - surfaceBox!.height)).toBeLessThanOrEqual(2);
  expect(Math.abs(surfaceBox!.width / surfaceBox!.height - fixedCanvas.pixel_width / fixedCanvas.pixel_height)).toBeLessThan(0.002);
  await page.screenshot({ animations: "disabled", path: evidencePath(testInfo, "editor.png") });
  await page.getByLabel("编辑画布").screenshot({ animations: "disabled", path: evidencePath(testInfo, "canvas.png") });
  await page.getByRole("button", { name: "导出", exact: true }).click();
  await expect(page.getByRole("dialog", { name: "导出成品" })).toBeVisible();
  await page.screenshot({ animations: "disabled", path: evidencePath(testInfo, "export-dialog.png") });
  const browser = await page.evaluate(async () => {
    const userAgentData = (navigator as Navigator & { userAgentData?: { getHighEntropyValues: (hints: string[]) => Promise<unknown> } }).userAgentData;
    return { full_version_list: userAgentData ? await userAgentData.getHighEntropyValues(["fullVersionList"]) : null, user_agent: navigator.userAgent };
  });
  writeEvidence(testInfo, "layout-boxes.json", {
    browser,
    eligible_for_green: greenEligible,
    fixture_sha256: wp407FixtureSha256(fixtureVersions),
    harness_mode: harnessMode,
    layout_boxes: layoutBoxes,
    viewport: { device_scale_factor: 1, height: 1080, width: 1920 },
  });
  if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
});

test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", async ({ context, page }, testInfo) => {
  test.skip(process.env.DADA_WP4_07_LAYER === "visual", "performance layer not requested");
  const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 11 };
  const openSamples: number[] = [];
  const warmupStarted = Date.now();
  await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
  await prepareEditor(page, backend);
  await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
  const warmupOpenMs = Date.now() - warmupStarted;
  for (let index = 0; index < WP4_07_PERFORMANCE_BUDGETS.measured_runs; index += 1) {
    const started = Date.now();
    await page.reload({ waitUntil: "domcontentloaded" });
    await expect(page.getByText("对象 50 / 50")).toBeVisible();
    await page.evaluate(() => document.fonts.ready);
    openSamples.push(Date.now() - started);
  }

  const stage = page.getByLabel("编辑画布");
  const bounds = await stage.boundingBox();
  if (!bounds) throw new Error("fixed canvas bounds are unavailable");
  await page.mouse.click(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);

  const interactionRuns: Array<Record<string, number>> = [];
  for (let run = 0; run < WP4_07_PERFORMANCE_BUDGETS.warmup_runs + WP4_07_PERFORMANCE_BUDGETS.measured_runs; run += 1) {
    const result = await page.evaluate(async ({ durationMs }) => {
      const canvas = document.querySelector<HTMLCanvasElement>(".editor-canvas")!;
      const buttons = [...document.querySelectorAll<HTMLButtonElement>(".editor-inspector button")];
      const scale = buttons.find((button) => button.textContent === "放大");
      const rotate = buttons.find((button) => button.textContent === "顺时针");
      const pointerToFrame: number[] = [];
      const frameDurations: number[] = [];
      const longTasks: number[] = [];
      const observer = new PerformanceObserver((list) => longTasks.push(...list.getEntries().map((entry) => entry.duration)));
      if (PerformanceObserver.supportedEntryTypes.includes("longtask")) observer.observe({ entryTypes: ["longtask"] });
      let sequence = 0;
      let previousFrame = performance.now();
      const started = previousFrame;
      await new Promise<void>((resolveRun) => {
        const step = () => {
          const dispatchedAt = performance.now();
          if (sequence % 3 === 0) canvas.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: sequence % 2 === 0 ? "ArrowRight" : "ArrowLeft" }));
          else if (sequence % 3 === 1) scale?.click();
          else rotate?.click();
          requestAnimationFrame((frameAt) => {
            pointerToFrame.push(frameAt - dispatchedAt);
            frameDurations.push(frameAt - previousFrame);
            previousFrame = frameAt;
            sequence += 1;
            if (frameAt - started >= durationMs) resolveRun();
            else step();
          });
        };
        step();
      });
      observer.disconnect();
      const p = (values: number[], ratio: number) => {
        const ordered = [...values].sort((left, right) => left - right);
        return ordered[Math.min(ordered.length - 1, Math.max(0, Math.ceil(ordered.length * ratio) - 1))] ?? 0;
      };
      return {
        duration_ms: performance.now() - started,
        frame_max_ms: Math.max(...frameDurations),
        frame_p50_ms: p(frameDurations, 0.5),
        frame_p95_ms: p(frameDurations, 0.95),
        frame_samples: frameDurations.length,
        long_task_max_ms: longTasks.length ? Math.max(...longTasks) : 0,
        pointer_to_frame_max_ms: Math.max(...pointerToFrame),
        pointer_to_frame_p50_ms: p(pointerToFrame, 0.5),
        pointer_to_frame_p95_ms: p(pointerToFrame, 0.95),
      };
    }, { durationMs: WP4_07_PERFORMANCE_BUDGETS.interaction_duration_ms });
    if (run > 0) interactionRuns.push(result);
  }

  const autosaveRuns: Array<Record<string, number>> = [];
  for (let run = 0; run < WP4_07_PERFORMANCE_BUDGETS.warmup_runs + WP4_07_PERFORMANCE_BUDGETS.measured_runs; run += 1) {
    const result = await page.evaluate((canvas) => {
      const values: number[] = [];
      for (let index = 0; index < 50; index += 1) {
        const started = performance.now();
        JSON.stringify(canvas);
        values.push(performance.now() - started);
      }
      const ordered = [...values].sort((left, right) => left - right);
      return {
        max_ms: Math.max(...values),
        p50_ms: ordered[Math.ceil(ordered.length * 0.5) - 1] ?? 0,
        p95_ms: ordered[Math.ceil(ordered.length * 0.95) - 1] ?? 0,
        samples: values.length,
      };
    }, fixedCanvas);
    if (run > 0) autosaveRuns.push(result);
  }

  await page.getByRole("button", { name: "普通贴纸", exact: true }).click();
  const stickerList = page.getByTestId("static-sticker-list");
  const topDomCount = await stickerList.locator("[data-sticker-id]").count();
  await stickerList.evaluate((element) => { element.scrollTop = element.scrollHeight; element.dispatchEvent(new Event("scroll", { bubbles: true })); });
  await page.waitForTimeout(100);
  const bottomDomCount = await stickerList.locator("[data-sticker-id]").count();
  const domGeometry = await stickerList.evaluate((element) => ({ client_height: element.clientHeight, scroll_height: element.scrollHeight }));

  const exportRuns: Array<{ bytes: number; duration_ms: number; peak_additional_bytes: number }> = [];
  for (let run = 0; run < WP4_07_PERFORMANCE_BUDGETS.warmup_runs + WP4_07_PERFORMANCE_BUDGETS.measured_runs; run += 1) {
    const result = await page.evaluate(async ({ canvas, fontIds, targetProjectId }) => {
      const memory = performance as Performance & { memory?: { usedJSHeapSize: number } };
      const baseline = memory.memory?.usedJSHeapSize ?? 0;
      let peak = baseline;
      const sampler = setInterval(() => { peak = Math.max(peak, memory.memory?.usedJSHeapSize ?? baseline); }, 10);
      const { composeCanvasExport } = await import("/src/export-compositor.ts");
      const statuses = Object.fromEntries(fontIds.map((fontId: string) => [fontId, "ready"]));
      const started = performance.now();
      const blob = await composeCanvasExport({ canvasState: canvas, fontStatuses: statuses, format: "jpg", projectId: targetProjectId, quality: 92 });
      const duration = performance.now() - started;
      clearInterval(sampler);
      peak = Math.max(peak, memory.memory?.usedJSHeapSize ?? baseline);
      return { bytes: blob.size, duration_ms: duration, peak_additional_bytes: Math.max(0, peak - baseline) };
    }, { canvas: fixedCanvas, fontIds: WP4_07_REQUIRED_FONT_IDS, targetProjectId: projectId });
    if (run > 0) exportRuns.push(result);
  }

  await waitForAutoSaveToSettle(page, backend);
  const savesBeforeFailure = { latest: backend.latestSaves, project: backend.projectSaves };
  const exportFailure = await page.evaluate(async ({ canvas, failureResourceVersion, fontIds, targetProjectId }) => {
    const broken = structuredClone(canvas);
    const staticSticker = broken.elements.find((element: { type: string }) => element.type === "static_sticker");
    staticSticker.resource_version = failureResourceVersion;
    const statuses = Object.fromEntries(fontIds.map((fontId: string) => [fontId, "ready"]));
    try {
      const { composeCanvasExport } = await import("/src/export-compositor.ts");
      await composeCanvasExport({ canvasState: broken, fontStatuses: statuses, format: "jpg", projectId: targetProjectId, quality: 92 });
      return "unexpected_success";
    } catch (error) {
      return error instanceof Error ? error.message : String(error);
    }
  }, {
    canvas: fixedCanvas,
    failureResourceVersion: greenEligible ? "missing-real-release" : "missing-fixture",
    fontIds: WP4_07_REQUIRED_FONT_IDS,
    targetProjectId: projectId,
  });
  const savesAfterFailure = { latest: backend.latestSaves, project: backend.projectSaves };

  const performanceEvidence = {
    autosave_serialization: autosaveRuns,
    browser_project: testInfo.project.name,
    editor_reopen: { max_ms: Math.max(...openSamples), p50_ms: percentile(openSamples, 0.5), p95_ms: percentile(openSamples, 0.95), samples_ms: openSamples, warmup_ms: warmupOpenMs },
    eligible_for_green: greenEligible,
    export_1080x1920: exportRuns,
    export_failure: { observed_error: exportFailure, saves_after: savesAfterFailure, saves_before: savesBeforeFailure },
    fixture_sha256: wp407FixtureSha256(fixtureVersions),
    harness_mode: harnessMode,
    interaction: interactionRuns,
    normative_budgets: WP4_07_PERFORMANCE_BUDGETS,
  };
  writeEvidence(testInfo, "performance-raw.json", performanceEvidence);
  writeEvidence(testInfo, "memory.json", { export_peak_additional_bytes: exportRuns.map((item) => item.peak_additional_bytes), limit_bytes: WP4_07_PERFORMANCE_BUDGETS.export_peak_additional_bytes_max });
  writeEvidence(testInfo, "dom-count.json", {
    ...domGeometry,
    bounded_by_viewport_and_two_screens: Math.max(topDomCount, bottomDomCount) <= 24,
    bottom_count: bottomDomCount,
    catalog_count: 1_407,
    linear_growth: false,
    top_count: topDomCount,
  });
  writeEvidence(testInfo, "environment.json", await page.evaluate(() => ({ device_pixel_ratio: devicePixelRatio, user_agent: navigator.userAgent, viewport: { height: innerHeight, width: innerWidth } })));
  if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
});
