import test from 'node:test';
import assert from 'node:assert/strict';
import { chromium, type Browser } from 'playwright';
import { readFile, mkdir, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { plannerExamples } from '../../src/tools/planner/core';

// Run against the fresh integrated production build: TOOLS_BROWSER_URL=http://127.0.0.1:4381
const base = process.env.TOOLS_BROWSER_URL;

test('The actual Frappe bridge uses distinct Day, Week and Month date scales', { timeout: 60_000 }, async () => {
  const require = createRequire(import.meta.url);
  const engine = await readFile(resolve(dirname(require.resolve('frappe-gantt')), 'frappe-gantt.umd.js'));
  const css = await readFile(resolve(dirname(require.resolve('frappe-gantt')), 'frappe-gantt.css'));
  const bridge = await readFile(new URL('../../src/tools/planner/chart-bridge.js', import.meta.url));
  const server = createServer((request, response) => {
    if (request.url === '/engine.js' || request.url === '/bridge.js') { response.setHeader('Content-Type', 'text/javascript'); response.end(request.url === '/engine.js' ? engine : bridge); }
    else if (request.url === '/engine.css') { response.setHeader('Content-Type', 'text/css'); response.end(css); }
    else { response.setHeader('Content-Type', 'text/html'); response.end('<!doctype html><html lang="en"><meta charset="utf-8"><title>Planner scale regression</title><body></body></html>'); }
  });
  await new Promise<void>(done => server.listen(0, '127.0.0.1', done));
  let browser: Browser | undefined;
  try {
    browser = await chromium.launch({ headless: true, channel: process.env.TOOLS_BROWSER_CHANNEL || undefined });
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    await page.goto(`http://127.0.0.1:${(server.address() as { port: number }).port}/`);
    const scales: Record<string, { barWidth: number; labels: string[] }> = {};
    for (const view of ['Day', 'Week', 'Month']) {
      await page.evaluate(async view => {
        document.querySelector('iframe')?.remove();
        const frame = document.createElement('iframe'); frame.setAttribute('sandbox', 'allow-scripts'); frame.style.cssText = 'width:800px;height:600px';
        const url = URL.createObjectURL(new Blob([`<!doctype html><html><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src ${location.origin}; style-src ${location.origin} 'unsafe-inline'"><link rel="stylesheet" href="${location.origin}/engine.css"><style>html,body{margin:0}</style><svg id="chart"></svg><script src="${location.origin}/engine.js"></script><script src="${location.origin}/bridge.js"></script></html>`], { type: 'text/html' }));
        const mounted = new Promise<void>((done, reject) => {
          const controller = new AbortController();
          const timer = window.setTimeout(() => { controller.abort(); reject(new Error('Timeline did not mount')); }, 15000);
          addEventListener('message', event => {
            if (event.source !== frame.contentWindow) return;
            if (event.data?.type === 'essential-plan-ready') frame.contentWindow!.postMessage({ type: 'essential-plan-render', view, tasks: [{ id: 'one-day', name: 'One day of work', start: '2026-03-06', end: '2026-03-06', progress: 0, dependencies: [] }, { id: 'last-day', name: 'Later work', start: '2026-04-30', end: '2026-04-30', progress: 0, dependencies: [] }] }, '*');
            if (event.data?.type === 'essential-plan-mounted' || event.data?.type === 'essential-plan-error') { clearTimeout(timer); controller.abort(); event.data.type === 'essential-plan-mounted' ? done() : reject(new Error('Timeline failed')); }
          }, { signal: controller.signal });
        });
        frame.src = url; document.body.append(frame); await mounted; URL.revokeObjectURL(url);
      }, view);
      const renderedFrame = page.frames().find(frame => frame.url().startsWith('blob:'))!;
      await renderedFrame.waitForFunction(view => {
        const container = document.querySelector<HTMLElement>('.gantt-container')!, bar = document.querySelector('.bar-wrapper[data-id="one-day"] .bar')!;
        const viewport = container.getBoundingClientRect(), bounds = bar.getBoundingClientRect();
        return bounds.left >= viewport.left - 1 && bounds.right <= viewport.right + 1 && (view === 'Month' || container.scrollLeft > 0 && bounds.left < viewport.left + 50);
      }, view, { timeout: 5000 });
      scales[view] = await page.frameLocator('iframe').locator('.bar-wrapper[data-id="one-day"] .bar').evaluate(element => ({ barWidth: Number(element.getAttribute('width')), labels: [...document.querySelectorAll('.lower-text')].map(label => label.textContent ?? '') }));
    }
    assert.ok(Math.abs(scales.Day.barWidth - 45) < 0.1, JSON.stringify(scales));
    assert.ok(Math.abs(scales.Week.barWidth - 20) < 0.1, JSON.stringify(scales));
    assert.ok(scales.Month.barWidth > 0 && scales.Month.barWidth < 5, JSON.stringify(scales));
    assert.notDeepEqual(scales.Day.labels, scales.Week.labels);
    assert.notDeepEqual(scales.Week.labels, scales.Month.labels);
    console.info(`Planner one-day bar widths: Day ${scales.Day.barWidth}, Week ${scales.Week.barWidth}, Month ${scales.Month.barWidth}.`);
  } finally { await browser?.close(); await new Promise<void>(done => server.close(() => done())); }
});

test('Planner production UI, sandbox rendering, date changes and exports', { skip: !base, timeout: 120_000 }, async () => {
  const browser = await chromium.launch({ headless: true, channel: process.env.TOOLS_BROWSER_CHANNEL || undefined });
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, timezoneId: 'America/Vancouver', acceptDownloads: true });
    const errors: string[] = []; const requests: string[] = [];
    page.on('pageerror', error => errors.push(error.message));
    page.on('request', request => requests.push(`${request.url()} ${request.postData() ?? ''}`));
    page.on('dialog', dialog => dialog.accept());
    await page.goto(`${base}/tools/project-planner/`);
    await page.locator('[data-total-cost]').waitFor();
    await page.locator('[data-chart-status]').filter({ hasText: 'Timeline ready' }).waitFor({ timeout: 20_000 });
    assert.equal(await page.locator('iframe.et-planner-frame').getAttribute('sandbox'), 'allow-scripts');
    const frame = page.frameLocator('iframe.et-planner-frame');
    assert.equal(await frame.locator('.bar-wrapper').count(), 6);
    await mkdir('output/planner', { recursive: true });
    await page.screenshot({ path: 'output/planner/desktop.png', fullPage: true });

    const fixture = structuredClone(plannerExamples['customer-portal']);
    fixture.title = 'PLANNER-PRIVATE-SENTINEL-9F20';
    fixture.contingencyPercent = '15';
    fixture.tasks = [
      { ...fixture.tasks[0], id: 'task-a', title: 'Task A', hours: '40', rate: '100.00', start: '2026-03-06', end: '2026-03-06', predecessors: [] },
      { ...fixture.tasks[1], id: 'task-b', title: 'Task B', hours: '60', rate: '125.00', start: '2026-03-09', end: '2026-03-13', predecessors: ['task-a'] },
    ];
    await page.locator('[data-import]').setInputFiles({ name: 'fixture.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(fixture)) });
    await page.locator('[data-total-cost]').filter({ hasText: '13,225.00' }).waitFor();
    assert.equal(await page.locator('[data-total-hours]').textContent(), '100');
    await page.locator('[data-chart-status]').filter({ hasText: 'Timeline ready' }).waitFor();
    const scaleWidths: number[] = [];
    for (const scale of ['Day', 'Week', 'Month']) {
      const control = page.locator(`[data-action="view"][data-view="${scale}"]`);
      await control.focus(); await page.keyboard.press('Enter');
      await page.locator('[data-chart-status]').filter({ hasText: 'Timeline ready' }).waitFor();
      assert.equal(await page.frameLocator('iframe.et-planner-frame').locator('.bar-wrapper').count(), 2);
      scaleWidths.push(await page.frameLocator('iframe.et-planner-frame').locator('.bar-wrapper[data-id="task-a"] .bar').evaluate(element => Number(element.getAttribute('width'))));
    }
    assert.ok(scaleWidths[0] > scaleWidths[1] && scaleWidths[1] > scaleWidths[2], `Keyboard scale changes must alter actual date spacing: ${scaleWidths.join(', ')}`);
    await page.locator('[data-action="view"][data-view="Day"]').click();
    await page.locator('[data-chart-status]').filter({ hasText: 'Timeline ready' }).waitFor();
    // Real pointer callback: drag the first one-day task by one 45px Day column.
    const beforeSecond = await page.locator('[data-focus="task-b-start"]').inputValue();
    const bar = page.frameLocator('iframe.et-planner-frame').locator('.bar-wrapper[data-id="task-a"] .bar');
    // Native instant scrolling avoids Playwright waiting on Frappe's SVG width
    // animation while preserving a real page.mouse drag through the iframe.
    await page.locator('iframe.et-planner-frame').evaluate(element => window.scrollTo({ top: element.getBoundingClientRect().top + scrollY - 180, behavior: 'instant' }));
    await bar.evaluate(element => element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }));
    await page.evaluate(() => new Promise(done => requestAnimationFrame(() => requestAnimationFrame(done))));
    const bounds = await bar.boundingBox(); assert.ok(bounds);
    await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
    await page.mouse.down(); await page.mouse.move(bounds.x + bounds.width / 2 + 45, bounds.y + bounds.height / 2, { steps: 8 }); await page.mouse.up();
    await page.waitForFunction(() => (document.querySelector('[data-focus="task-a-start"]') as HTMLInputElement).value !== '2026-03-06');
    assert.equal(await page.locator('[data-focus="task-b-start"]').inputValue(), beforeSecond, 'Dragging must not auto-move dependencies');
    assert.equal(await page.locator('[data-focus="task-a-start"]').inputValue(), await page.locator('[data-focus="task-a-end"]').inputValue(), 'One-day inclusive duration survives Frappe callback');

    for (const action of ['json', 'csv', 'pdf']) {
      const downloadPromise = page.waitForEvent('download', { timeout: 60_000 });
      await page.locator(`[data-action="${action}"]`).click();
      const download = await downloadPromise;
      await download.saveAs(`output/planner/browser-${download.suggestedFilename()}`);
      const bytes = await readFile(`output/planner/browser-${download.suggestedFilename()}`);
      assert.ok(bytes.length > 20);
      if (action === 'json') { const reopened = JSON.parse(bytes.toString()); assert.equal(reopened.title, fixture.title); assert.equal(reopened.tasks[0].hours, '40'); }
      if (action === 'csv') assert.ok(bytes.toString().includes('"4000.00"'));
      if (action === 'pdf') assert.equal(bytes.subarray(0, 5).toString(), '%PDF-');
    }
    assert.deepEqual(errors, []);
    assert.ok(!requests.some(request => request.includes('PLANNER-PRIVATE-SENTINEL-9F20')));
    assert.ok(!requests.some(request => /^https?:/.test(request) && !request.startsWith(base!)), 'Tool operations must not request third-party services');
    await writeFile('output/planner/browser-observations.json', JSON.stringify({ errors, requestCount: requests.length, externalRequests: requests.filter(request => /^https?:/.test(request) && !request.startsWith(base!)), pointerDateCallbackVerified: true }, null, 2));
    await page.close();
  } finally { await browser.close(); }
});

test('Planner mobile, storage failures and invalid imports preserve current data', { skip: !base, timeout: 60_000 }, async () => {
  const browser = await chromium.launch({ headless: true, channel: process.env.TOOLS_BROWSER_CHANNEL || undefined });
  try {
    const page = await browser.newPage({ viewport: { width: 390, height: 844 }, isMobile: true });
    page.on('dialog', dialog => dialog.accept());
    await page.goto(`${base}/tools/project-planner/`);
    await page.locator('[data-total-cost]').waitFor();
    assert.equal(await page.locator('iframe.et-planner-frame').count(), 0, 'Mobile defaults to editable tasks');
    assert.equal(await page.locator('[data-remember]').isChecked(), false);
    assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
    const first = await page.locator('[data-focus="task-1-title"]').inputValue();
    await page.locator('[data-import]').setInputFiles({ name: 'future.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify({ ...plannerExamples['customer-portal'], schemaVersion: 9 })) });
    await page.locator('[data-planner-status]').filter({ hasText: 'could not be imported' }).waitFor();
    assert.equal(await page.locator('[data-focus="task-1-title"]').inputValue(), first);
    await page.evaluate(() => { Storage.prototype.setItem = () => { throw new DOMException('Full', 'QuotaExceededError'); }; });
    await page.locator('[data-remember]').click(); // The failed save intentionally turns the checkbox back off.
    await page.locator('[data-planner-status]').filter({ hasText: 'could not save' }).waitFor();
    assert.equal(await page.locator('[data-remember]').isChecked(), false);
    await page.screenshot({ path: 'output/planner/mobile.png', fullPage: true });
    for (const width of [360, 768, 1024]) {
      await page.setViewportSize({ width, height: 900 });
      assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `No page overflow at ${width}px`);
    }
    await page.close();
  } finally { await browser.close(); }
});
