/// <reference types="node" />

import { readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { expect, test } from '@playwright/test';
import { GENERATED_FIXTURES, PRIMARY_FIXTURE } from './browser-helpers';

test('covers drop, editor keyboard/drag controls, cache, and project reattachment', async ({
  page,
}) => {
  test.setTimeout(240_000);
  await page.addInitScript(() => {
    Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
      configurable: true,
      value: () => '',
    });
    Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
  });
  await page.goto('/');
  await page.getByText('Engine & browser').click();
  await page.getByLabel('Engine mode').selectOption('force-single-thread');

  await dropFixture(page, PRIMARY_FIXTURE, 'video/mp4');
  await expect(page.locator('.source-summary .phase--ready')).toHaveText(
    'Ready',
    { timeout: 120_000 }
  );
  await expect(page.locator('.quick-preview video')).toHaveAttribute(
    'src',
    /^blob:/u
  );

  await page.getByRole('button', { name: 'Create proxy' }).click();
  await expect(
    page.locator('.result-card').filter({ hasText: 'pattern-av-preview.mp4' })
  ).toContainText(/created · verified|completed locally/u, {
    timeout: 120_000,
  });
  await page.getByText('Derived cache').click();
  const clearCache = page.getByRole('button', { name: 'Clear cache' });
  await expect(clearCache).toBeEnabled({ timeout: 10_000 });
  await clearCache.click();
  await expect(page.getByRole('status')).toContainText(
    'Derived cache cleared.'
  );

  await page.getByRole('tab', { name: 'Edit' }).click();
  const compatibleB = join(GENERATED_FIXTURES, 'compatible-b.mp4');
  await mediaInput(page).setInputFiles(compatibleB);
  await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
    timeout: 120_000,
  });

  const clips = page.locator('.timeline-clip');
  await expect(clips).toHaveCount(2);
  const secondName = await clips.nth(1).locator('strong').textContent();
  await clips.nth(1).dragTo(clips.nth(0));
  await expect(clips.nth(0).locator('strong')).toHaveText(
    secondName ?? 'compatible-b.mp4'
  );

  await clips
    .filter({ hasText: 'compatible-b.mp4' })
    .locator('.timeline-clip__main')
    .click();
  const inspector = page.locator('.inspector');
  await inspector.getByRole('tab', { name: 'Transform' }).click();
  const crop = inspector.locator('.crop-editor__numeric');
  await crop.getByRole('spinbutton', { name: 'Width' }).fill('100');
  await crop.getByRole('spinbutton', { name: 'Height' }).fill('60');
  const cropMove = inspector.getByRole('button', {
    name: 'Move crop rectangle',
  });
  await cropMove.focus();
  await page.keyboard.press('Shift+ArrowRight');
  await expect(crop.getByRole('spinbutton', { name: 'X' })).toHaveValue('10');

  const resizeRight = inspector.getByRole('button', {
    name: 'Resize crop from right',
  });
  const handleBox = await resizeRight.boundingBox();
  expect(handleBox).not.toBeNull();
  const widthBefore = Number(
    await crop.getByRole('spinbutton', { name: 'Width' }).inputValue()
  );
  const handleCenterX = (handleBox?.x ?? 0) + (handleBox?.width ?? 0) / 2;
  const handleCenterY = (handleBox?.y ?? 0) + (handleBox?.height ?? 0) / 2;
  await page.mouse.move(handleCenterX, handleCenterY);
  await page.mouse.down();
  await page.mouse.move(handleCenterX + 16, handleCenterY);
  await page.mouse.up();
  await expect
    .poll(async () =>
      Number(await crop.getByRole('spinbutton', { name: 'Width' }).inputValue())
    )
    .not.toBe(widthBefore);

  await crop.getByRole('spinbutton', { name: 'X' }).fill('999');
  await expect(inspector.locator('.crop-editor__errors')).toHaveAttribute(
    'role',
    'alert'
  );
  await inspector.getByRole('button', { name: 'Reset crop' }).click();

  const advanced = page.locator('.advanced-operations');
  await advanced.getByRole('tab', { name: 'Waveform and loudness' }).click();
  await advanced.getByRole('button', { name: 'Analyze peak waveform' }).click();
  const inHandle = page.getByRole('slider', { name: 'In trim handle' });
  await expect(inHandle).toBeVisible({ timeout: 120_000 });
  const inBefore = Number(await inHandle.getAttribute('aria-valuenow'));
  await inHandle.focus();
  await page.keyboard.press('ArrowRight');
  await expect
    .poll(async () => Number(await inHandle.getAttribute('aria-valuenow')))
    .toBeGreaterThan(inBefore);

  const projectDownload = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Save project' }).click();
  const projectPath = await (await projectDownload).path();
  expect(projectPath).not.toBeNull();
  await page.getByRole('button', { name: 'New', exact: true }).click();
  await page
    .locator('.project-actions input[type="file"]')
    .setInputFiles(projectPath as string);
  await expect(page.locator('.timeline-clip')).toHaveCount(2);
  await expect(page.locator('.media-card')).toHaveCount(0);

  page.on('dialog', (dialog) => void dialog.accept());
  await mediaInput(page).setInputFiles([PRIMARY_FIXTURE, compatibleB]);
  await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
    timeout: 120_000,
  });
  await expect(page.locator('.timeline-clip')).toHaveCount(2);
});

function mediaInput(page: import('@playwright/test').Page) {
  return page.locator('input[type="file"][accept*="audio"]').first();
}

async function dropFixture(
  page: import('@playwright/test').Page,
  filePath: string,
  mimeType: string
): Promise<void> {
  const bytes = readFileSync(filePath).toString('base64');
  const stats = statSync(filePath);
  await page
    .locator('.drop-zone')
    .first()
    .evaluate(
      (element, fixture) => {
        const binary = atob(fixture.base64);
        const data = Uint8Array.from(binary, (character) =>
          character.charCodeAt(0)
        );
        const transfer = new DataTransfer();
        transfer.items.add(
          new File([data], fixture.name, {
            type: fixture.mimeType,
            lastModified: fixture.lastModified,
          })
        );
        element.dispatchEvent(
          new DragEvent('dragenter', {
            bubbles: true,
            cancelable: true,
            dataTransfer: transfer,
          })
        );
        element.dispatchEvent(
          new DragEvent('drop', {
            bubbles: true,
            cancelable: true,
            dataTransfer: transfer,
          })
        );
      },
      {
        base64: bytes,
        name: filePath.split('/').at(-1) ?? 'fixture.mp4',
        mimeType,
        lastModified: Math.trunc(stats.mtimeMs),
      }
    );
}
