import { expect, type Page, test } from '@playwright/test';

async function installLifecycleProbe(page: Page) {
  await page.addInitScript(() => {
    const activeAnimationFrames = new Set<number>();
    const contexts: WebGLRenderingContext[] = [];
    const listeners: Array<{
      listener: EventListenerOrEventListenerObject | null;
      target: EventTarget;
      type: string;
    }> = [];

    const requestAnimationFrameOriginal = window.requestAnimationFrame.bind(window);
    const cancelAnimationFrameOriginal = window.cancelAnimationFrame.bind(window);
    window.requestAnimationFrame = (callback) => {
      let id = 0;
      id = requestAnimationFrameOriginal((time) => {
        activeAnimationFrames.delete(id);
        callback(time);
      });
      activeAnimationFrames.add(id);
      return id;
    };
    window.cancelAnimationFrame = (id) => {
      activeAnimationFrames.delete(id);
      cancelAnimationFrameOriginal(id);
    };

    const addEventListenerOriginal = EventTarget.prototype.addEventListener;
    const removeEventListenerOriginal = EventTarget.prototype.removeEventListener;
    EventTarget.prototype.addEventListener = function (type, listener, options) {
      listeners.push({ listener, target: this, type });
      return addEventListenerOriginal.call(this, type, listener, options);
    };
    EventTarget.prototype.removeEventListener = function (type, listener, options) {
      const index = listeners.findIndex(
        (entry) => entry.target === this && entry.type === type && entry.listener === listener,
      );
      if (index >= 0) listeners.splice(index, 1);
      return removeEventListenerOriginal.call(this, type, listener, options);
    };

    const getContextOriginal = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = function (...args) {
      const context = getContextOriginal.apply(this, args as never);
      if (
        context &&
        (args[0] === 'webgl' || args[0] === 'webgl2' || args[0] === 'experimental-webgl') &&
        !contexts.includes(context as WebGLRenderingContext)
      ) {
        contexts.push(context as WebGLRenderingContext);
      }
      return context;
    } as typeof HTMLCanvasElement.prototype.getContext;

    Object.defineProperty(window, '__lifecycleProbe', {
      value: {
        snapshot: () => ({
          activeAnimationFrames: activeAnimationFrames.size,
          canvasListeners: listeners.filter(
            (entry) => entry.target instanceof HTMLCanvasElement,
          ).length,
          contexts: contexts.length,
          contextsLost:
            contexts.length > 0 && contexts.every((context) => context.isContextLost()),
          keydownListeners: listeners.filter(
            (entry) => entry.target === window && entry.type === 'keydown',
          ).length,
          resizeListeners: listeners.filter(
            (entry) => entry.target === window && entry.type === 'resize',
          ).length,
        }),
      },
    });
  });
}

test('viewer2d releases its canvas when disposed', async ({ page }) => {
  await installLifecycleProbe(page);
  await page.goto('/');
  await page.getByRole('button', { name: 'Sample DXF' }).click();
  await expect(page.locator('#status')).toContainText('✓ simple.dxf');
  await expect(page.locator('canvas')).toHaveCount(1);

  const lifecycle = await page.evaluate(() => {
    const viewer = (
      window as Window & {
        __viewer2d?: { dispose?: () => void };
      }
    ).__viewer2d;
    return {
      exposed: Boolean(viewer),
      disposable: typeof viewer?.dispose === 'function',
    };
  });

  expect(lifecycle).toEqual({ exposed: true, disposable: true });

  await page.evaluate(() => {
    (
      window as Window & {
        __viewer2d?: { dispose?: () => void };
      }
    ).__viewer2d?.dispose?.();
  });
  await page.evaluate(() => {
    (
      window as Window & {
        __viewer2d?: { dispose?: () => void };
      }
    ).__viewer2d?.dispose?.();
  });
  await expect(page.locator('canvas')).toHaveCount(0);
  const resources = await page.evaluate(() =>
    (
      window as Window & {
        __lifecycleProbe: { snapshot: () => Record<string, number | boolean> };
      }
    ).__lifecycleProbe.snapshot(),
  );
  expect(resources).toEqual({
    activeAnimationFrames: 0,
    canvasListeners: 0,
    contexts: 1,
    contextsLost: true,
    keydownListeners: 0,
    resizeListeners: 0,
  });
});

test('viewer3d releases its canvas on pagehide and remains disposable', async ({ page }) => {
  await installLifecycleProbe(page);
  await page.goto('http://127.0.0.1:3333/?model=/samples/Box.glb');
  await page.waitForFunction(
    () =>
      Boolean(
        (
          window as Window & {
            __viewer?: { current?: object };
          }
        ).__viewer?.current,
      ),
  );

  const result = await page.evaluate(() => {
    window.dispatchEvent(new PageTransitionEvent('pagehide'));
    const exposed = (window as Window & { __viewer?: { dispose: () => void } }).__viewer;
    exposed?.dispose();
    const resources = (
      window as Window & {
        __lifecycleProbe: { snapshot: () => Record<string, number | boolean> };
      }
    ).__lifecycleProbe.snapshot();
    return {
      canvases: document.querySelectorAll('canvas').length,
      exposed: Boolean(exposed),
      resources,
    };
  });

  expect(result).toEqual({
    canvases: 0,
    exposed: false,
    resources: {
      activeAnimationFrames: 0,
      canvasListeners: 0,
      contexts: 1,
      contextsLost: true,
      keydownListeners: 0,
      resizeListeners: 0,
    },
  });
});

test('viewer2d and viewer3d survive ten mount transitions without duplicate canvases', async ({
  page,
}) => {
  test.setTimeout(180_000);
  const runtimeErrors: string[] = [];
  page.on('pageerror', (error) => runtimeErrors.push(error.message));

  for (let transition = 0; transition < 10; transition += 1) {
    await page.goto('http://127.0.0.1:5173/');
    await page.getByRole('button', { name: 'Sample DWG' }).click();
    await expect(page.locator('#status')).toContainText('7309 entities', { timeout: 20_000 });
    await expect(page.locator('canvas')).toHaveCount(1);

    await page.goto('http://127.0.0.1:3333/?model=/samples/Box.glb');
    await page.waitForFunction(
      () =>
        Boolean(
          (
            window as Window & {
              __viewer?: { current?: object };
            }
          ).__viewer?.current,
        ),
      undefined,
      { timeout: 20_000 },
    );
    await expect(page.locator('canvas')).toHaveCount(1);
  }

  expect(runtimeErrors).toEqual([]);
});
