import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { createServer, type Server } from 'node:http';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
import { chromium, type Browser, type Page } from 'playwright';
import { processExamples } from '../../src/tools/mapper/core';

let browser: Browser, server: Server, origin: string;
before(async () => {
  const result = await build({ entryPoints: [fileURLToPath(new URL('../../src/tools/mapper/index.ts', import.meta.url))], bundle: true, minify: true, write: false, outfile: 'mapper.js', format: 'iife', globalName: 'EssentialMapper', platform: 'browser', target: 'es2022', logLevel: 'silent' });
  const javascript = result.outputFiles.find(file => file.path.endsWith('.js'))!.contents;
  const css = result.outputFiles.find(file => file.path.endsWith('.css'))!.contents;
  const font = await readFile(new URL('../../public/tools-assets/fonts/NotoSans-Regular.ttf', import.meta.url));
  server = createServer((request, response) => {
    if (request.url === '/mapper.js') { response.setHeader('Content-Type', 'text/javascript'); response.end(javascript); }
    else if (request.url === '/mapper.css') { response.setHeader('Content-Type', 'text/css'); response.end(css); }
    else if (request.url === '/tools-assets/fonts/NotoSans-Regular.ttf') { response.setHeader('Content-Type', 'font/ttf'); response.end(font); }
    else if (request.url === '/') { response.setHeader('Content-Type', 'text/html'); response.end('<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Process Mapper test</title><link rel="stylesheet" href="/mapper.css"><style>body{margin:0;font:16px Arial}.essential-tools{padding:20px;max-width:1200px;margin:auto}</style><main class="essential-tools"><div id="tool"></div></main><script src="/mapper.js"></script></html>'); }
    else { response.statusCode = 404; response.end(); }
  });
  await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  origin = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
  browser = await chromium.launch({ headless: true, ...(process.env.TOOLS_BROWSER_CHANNEL ? { channel: process.env.TOOLS_BROWSER_CHANNEL } : {}) });
}, { timeout: 120_000 });

after(async () => { await browser?.close(); await new Promise<void>(resolve => server ? server.close(() => resolve()) : resolve()); });

async function page(width = 1280): Promise<Page> {
  const page = await browser.newPage({ viewport: { width, height: 950 } });
  await page.goto(origin); return page;
}
async function ready(page: Page) {
  await page.waitForFunction(() => !(document.querySelector('[data-export="pdf"]') as HTMLButtonElement)?.disabled, undefined, { timeout: 60_000 });
}

test('actual Mermaid engine renders all examples, safe characters, sequence diagrams and real exports', { timeout: 120_000 }, async () => {
  const current = await page(); const requests: string[] = [];
  current.on('request', request => requests.push(request.url()));
  const states = Object.values(processExamples).map(state => structuredClone(state));
  states[0].graph.nodes[0].label = 'Québec "A&B" [request] <review> #1';
  const results = await current.evaluate(async states => {
    const api = (window as any).EssentialMapper;
    const results = [];
    for (const state of states) {
      const rendered = await api.renderProcess(state);
      const parsed = new DOMParser().parseFromString(rendered.svg, 'image/svg+xml');
      results.push({ width: rendered.width, height: rendered.height, valid: !parsed.querySelector('parsererror'), text: [...parsed.querySelectorAll('text')].map(element => element.textContent).join(' | '), active: parsed.querySelectorAll('script,foreignObject,a,image,use').length });
    }
    const sequence = await api.renderProcess({ schemaVersion: 1, tool: 'process-mapper', mode: 'source', preservedGraph: null, source: 'sequenceDiagram\n  participant Customer\n  participant Team\n  Customer->>Team: Request a quote\n  Team-->>Customer: Confirm next steps' });
    const sequenceDocument = new DOMParser().parseFromString(sequence.svg, 'image/svg+xml');
    const sequenceSvg = document.importNode(sequenceDocument.documentElement, true); document.body.append(sequenceSvg);
    const actorStyles = [...sequenceSvg.querySelectorAll('text.actor')].map(text => ({ stroke: getComputedStyle(text).stroke, fill: getComputedStyle(text).fill })); sequenceSvg.remove();
    const png = await api.svgToPng(sequence.svg), pdf = await api.processPdf(sequence);
    return { diagrams: results, actorStyles, pngHeader: Array.from(png.slice(0, 8)), pdfHeader: new TextDecoder().decode(pdf.slice(0, 5)), pdfSize: pdf.byteLength, width: sequence.width, height: sequence.height };
  }, states);
  results.diagrams.forEach(result => { assert.ok(result.width > 100); assert.ok(result.height > 30); assert.equal(result.valid, true); assert.equal(result.active, 0); });
  assert.ok(results.diagrams[0].text?.replace(/\s+/g, '').includes('Québec"A&B"[request]<review>#1'), results.diagrams[0].text);
  assert.deepEqual(results.pngHeader, [137, 80, 78, 71, 13, 10, 26, 10]); assert.equal(results.pdfHeader, '%PDF-'); assert.ok(results.pdfSize > 2000);
  assert.ok(results.actorStyles.length >= 2); results.actorStyles.forEach(style => { assert.equal(style.stroke, 'none'); assert.equal(style.fill, 'rgb(23, 23, 23)'); });
  assert.ok(requests.every(url => url.startsWith(origin) || url.startsWith('blob:')), requests.join('\n'));
  await current.close();
});

test('SVG sanitization removes active elements, handlers, external markers and arbitrary CSS', async () => {
  const current = await page();
  const safe = await current.evaluate(() => (window as any).EssentialMapper.sanitizeSvg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" onload="alert(1)"><style>body{background:url(https://invalid.example/x)}</style><script>alert(1)</script><foreignObject><div>bad</div></foreignObject><a href="https://invalid.example"><text>link</text></a><image href="https://invalid.example/a.png"/><use href="https://invalid.example/a.svg#x"/><path id="arrow" marker-end="url(https://invalid.example/a.svg#x)" style="fill:url(https://invalid.example)"/><text onclick="alert(1)">Allowed text</text></svg>'));
  assert.ok(safe.includes('Allowed text')); assert.doesNotMatch(safe, /https:|onload|onclick|foreignObject|<script|<image|<use|<a\s/); assert.doesNotMatch(safe, /background:/);
  await current.close();
});

test('portable flowchart labels remain centred inside their nodes after renderer CSS is removed', { timeout: 90_000 }, async () => {
  const current = await page();
  const results = await current.evaluate(async examples => {
    const results: { text: string; offset: number; anchor: string }[] = [];
    for (const example of examples) {
      const rendered = await (window as any).EssentialMapper.renderProcess(example);
      const parsed = new DOMParser().parseFromString(rendered.svg, 'image/svg+xml');
      const svg = document.importNode(parsed.documentElement, true); document.body.append(svg);
      await document.fonts.ready;
      for (const text of svg.querySelectorAll<SVGTextElement>('.node .label text')) {
        const bounds = text.getBBox();
        results.push({ text: text.textContent ?? '', offset: bounds.x + bounds.width / 2, anchor: getComputedStyle(text).textAnchor });
      }
      svg.remove();
    }
    return results;
  }, Object.values(processExamples));
  assert.ok(results.length >= 20);
  for (const result of results) { assert.equal(result.anchor, 'middle', result.text); assert.ok(Math.abs(result.offset) < 2, JSON.stringify(result)); }
  await current.close();
});

test('source editing keeps last valid preview, disables stale exports and recovers the preserved graph', { timeout: 90_000 }, async () => {
  const current = await page(); current.on('dialog', dialog => dialog.accept());
  await current.evaluate(() => (window as any).EssentialMapper.mount(document.querySelector('#tool'))); await ready(current);
  await current.getByRole('button', { name: 'Mermaid source', exact: true }).first().click(); await ready(current);
  const imageBefore = await current.locator('[data-image]').getAttribute('src');
  await current.locator('[data-field="source"]').fill('flowchart LR\nINVALID <script>');
  assert.equal(await current.locator('[data-export="pdf"]').isDisabled(), true);
  assert.equal(await current.locator('[data-image]').getAttribute('src'), imageBefore);
  assert.match(await current.locator('[data-preview-status]').innerText(), /last valid preview/i);
  await current.getByRole('button', { name: 'Restore previous step-by-step map' }).click(); await ready(current);
  assert.equal(await current.locator('[data-field="title"]').inputValue(), 'Customer onboarding');
  assert.equal(await current.locator('[data-node-field="label"]').inputValue(), 'New customer enquiry');
  await current.close();
});

test('keyboard form edits, local draft opt-in, revision handling and 390px layout work', { timeout: 90_000 }, async () => {
  const current = await page(390); current.on('dialog', dialog => dialog.accept());
  const external: string[] = []; current.on('request', request => { if (!request.url().startsWith(origin) && !request.url().startsWith('blob:')) external.push(request.url()); });
  await current.evaluate(() => (window as any).EssentialMapper.mount(document.querySelector('#tool'))); await ready(current);
  assert.equal(await current.evaluate(() => localStorage.length), 0);
  await current.locator('[data-node-field="label"]').fill('PRIVATE_PROCESS_QA_é_2026');
  assert.equal(await current.locator('[data-export="pdf"]').isDisabled(), true); await ready(current);
  assert.equal(await current.evaluate(() => localStorage.length), 0);
  await current.locator('[data-remember]').check();
  assert.equal(await current.evaluate(() => localStorage.length), 1);
  await current.locator('[data-remember]').uncheck(); assert.equal(await current.evaluate(() => localStorage.length), 0);
  await current.getByRole('button', { name: '+ Add step', exact: true }).click();
  await current.locator('[data-node-field="label"]').fill('Approve the request'); await ready(current);
  const dimensions = await current.evaluate(() => ({ width: innerWidth, scroll: document.documentElement.scrollWidth })); assert.ok(dimensions.scroll <= dimensions.width + 1, JSON.stringify(dimensions));
  assert.deepEqual(external, []); await current.close();
});

test('unsupported glyphs give an actionable error while editable data remains exportable', { timeout: 90_000 }, async () => {
  const current = await page(); await current.evaluate(() => (window as any).EssentialMapper.mount(document.querySelector('#tool'))); await ready(current);
  await current.locator('[data-node-field="label"]').fill('Launch 🚀');
  await current.waitForFunction(() => document.querySelector('[data-errors]')?.textContent?.includes('U+1F680'));
  assert.equal(await current.locator('[data-export="pdf"]').isDisabled(), true);
  assert.equal(await current.locator('[data-export="svg"]').isDisabled(), true);
  assert.equal(await current.locator('[data-export="json"]').isDisabled(), false);
  assert.equal(await current.locator('[data-export="mmd"]').isDisabled(), false);
  assert.equal(await current.locator('[data-node-field="label"]').inputValue(), 'Launch 🚀');
  await current.close();
});

test('a measured 100-node / 200-edge process renders within the output boundary', { timeout: 120_000 }, async () => {
  const current = await page();
  const result = await current.evaluate(async () => {
    const state = { schemaVersion: 1, tool: 'process-mapper', mode: 'guided', graph: { title: 'Large illustrative process', direction: 'TB', nodes: Array.from({ length: 100 }, (_, index) => ({ id: `n${index}`, type: 'process', label: `Step ${index + 1}`, team: '', description: '' })), edges: Array.from({ length: 200 }, (_, index) => ({ id: `e${index}`, from: `n${index % 100}`, to: `n${(index + 1) % 100}`, label: '' })) } };
    const start = performance.now(), output = await (window as any).EssentialMapper.renderProcess(state);
    return { elapsedMs: Math.round(performance.now() - start), width: output.width, height: output.height, bytes: new TextEncoder().encode(output.svg).length, nodes: new DOMParser().parseFromString(output.svg, 'image/svg+xml').querySelectorAll('g.node').length };
  });
  assert.equal(result.nodes, 100); assert.ok(result.bytes < 5_000_000); assert.ok(result.width <= 20_000 && result.height <= 20_000); assert.ok(result.elapsedMs < 30_000, JSON.stringify(result));
  console.info(`Process Mapper 100-node/200-edge render: ${result.elapsedMs} ms, ${result.width}×${result.height}, ${result.bytes} SVG bytes.`);
  await current.close();
});
