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

import { execFileSync } from 'node:child_process';
import { basename, join } from 'node:path';
import { expect, test } from '@playwright/test';
import {
  createCancellationFixture,
  GENERATED_FIXTURES,
  observeRuntime,
  PRIMARY_FIXTURE,
  type TemporaryMediaFixture,
} from './browser-helpers';

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

async function selectSingleThread(
  page: import('@playwright/test').Page
): Promise<void> {
  await page.getByText('Engine & browser').click();
  await page.getByLabel('Engine mode').selectOption('force-single-thread');
}

function assertLocalOnly(runtime: ReturnType<typeof observeRuntime>): void {
  const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
  expect(
    networkUrls.every((url) => new URL(url).origin === 'http://127.0.0.1:4173')
  ).toBe(true);
  const materialFailures = runtime.failedRequests.filter(
    (failure) =>
      !(failure.startsWith('blob:') && failure.endsWith('net::ERR_ABORTED'))
  );
  // Chromium aborts the superseded local preview blob when React switches from
  // the source object URL to the generated result object URL.
  expect(materialFailures).toEqual([]);
  expect(runtime.pageErrors).toEqual([]);
  expect(runtime.consoleErrors).toEqual([]);
}

test.describe.serial('real pinned FFmpeg browser runtime', () => {
  let cancellationFixture: TemporaryMediaFixture;

  test.beforeAll(() => {
    cancellationFixture = createCancellationFixture();
  });

  test.afterAll(() => {
    cancellationFixture.dispose();
  });

  test('loads and queries the multithread core under cross-origin isolation', async ({
    page,
  }) => {
    const runtime = observeRuntime(page);
    await page.goto('/');
    const environment = await page.evaluate(() => ({
      isolated: globalThis.crossOriginIsolated,
      sharedArrayBuffer: typeof globalThis.SharedArrayBuffer,
    }));
    test.skip(
      !environment.isolated || environment.sharedArrayBuffer !== 'function',
      'Chromium did not expose SharedArrayBuffer in this server context'
    );

    await page.getByText('Engine & browser').click();
    await page.getByRole('button', { name: 'Initialize engine' }).click();

    await expect(
      page.locator('.engine-pill').filter({ hasText: 'Multithreaded' })
    ).toBeVisible({ timeout: 120_000 });
    await expect(page.getByText('0.12.15 / 0.12.10')).toBeVisible();
    await expect(page.getByText('Isolation').locator('..')).toContainText(
      'Enabled'
    );

    const expectedAssets = [
      '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.js',
      '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm',
      '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.worker.js',
    ];
    for (const suffix of expectedAssets) {
      expect(
        runtime.responses.some(
          (response) => response.status === 200 && response.url.endsWith(suffix)
        )
      ).toBe(true);
    }
    const wasm = runtime.responses.find((response) =>
      response.url.endsWith(
        '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm'
      )
    );
    expect(wasm?.contentType).toContain('application/wasm');
    expect(
      runtime.requests.some((url) =>
        url.includes('/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/')
      )
    ).toBe(false);

    await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
    await expect(page.locator('.source-summary .phase--ready')).toHaveText(
      'Ready',
      { timeout: 120_000 }
    );
    await page.getByRole('button', { name: 'Convert', exact: true }).click();
    const result = page.locator('.result-card');
    await expect(result).toContainText('pattern-av-convert.mp4', {
      timeout: 120_000,
    });
    await expect(result).toContainText(/created · verified|completed locally/u);
    const outputBytes = await page
      .locator('.quick-preview video')
      .evaluate(async (element) => {
        const response = await fetch((element as HTMLVideoElement).src);
        return (await response.arrayBuffer()).byteLength;
      });
    expect(outputBytes).toBeGreaterThan(1_000);
    assertLocalOnly(runtime);
  });

  test('falls back to the single-thread core when the preferred multithread core cannot load', async ({
    page,
  }) => {
    const runtime = observeRuntime(page);
    await page.route(
      '**/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/**',
      (route) => route.abort('failed')
    );
    await page.goto('/');
    const environment = await page.evaluate(() => ({
      isolated: globalThis.crossOriginIsolated,
      sharedArrayBuffer: typeof globalThis.SharedArrayBuffer,
    }));
    test.skip(
      !environment.isolated || environment.sharedArrayBuffer !== 'function',
      'Chromium did not expose SharedArrayBuffer in this server context'
    );

    await page.getByText('Engine & browser').click();
    await page.getByLabel('Engine mode').selectOption('prefer-multithread');
    await page.getByRole('button', { name: 'Initialize engine' }).click();

    await expect(
      page
        .locator('.engine-pill')
        .filter({ hasText: 'Single-threaded compatibility mode' })
    ).toBeVisible({ timeout: 120_000 });
    await expect(
      page.getByText(
        'The multithread core failed to load. The app recovered with the single-thread compatibility core.'
      )
    ).toBeVisible();
    expect(
      runtime.requests.some((url) =>
        url.includes('/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/')
      )
    ).toBe(true);
    expect(
      runtime.responses.some(
        (response) =>
          response.status === 200 &&
          response.url.endsWith(
            '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
          )
      )
    ).toBe(true);
    expect(runtime.pageErrors).toEqual([]);
    expect(runtime.consoleErrors).toEqual([]);
  });

  test('probes H.264/AAC media and produces a verified H.264/AAC result with the single-thread core', async ({
    page,
  }) => {
    await page.addInitScript(() => {
      Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
    });
    const runtime = observeRuntime(page);
    await page.goto('/');
    await selectSingleThread(page);

    expect(
      runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
    ).toBe(false);
    await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);

    await expect(page.locator('.source-summary .phase--ready')).toHaveText(
      'Ready',
      { timeout: 120_000 }
    );
    await expect(page.locator('.source-summary')).toContainText(
      'pattern-av.mp4'
    );
    await expect(page.locator('.source-summary')).toContainText('0:02.00');
    await expect(
      page.getByRole('checkbox', { name: /#0 · h264/u })
    ).toBeChecked();
    await expect(
      page.getByRole('checkbox', { name: /#1 · aac/u })
    ).toBeChecked();
    await expect(
      page
        .locator('.engine-pill')
        .filter({ hasText: 'Single-threaded compatibility mode' })
    ).toBeVisible();

    await page.getByRole('button', { name: 'Convert', exact: true }).click();
    const result = page.locator('.result-card');
    await expect(result).toBeVisible({ timeout: 120_000 });
    await expect(result).toContainText('pattern-av-convert.mp4');
    await expect(result).toContainText(/created · verified|completed locally/u);

    const resultVideo = page.locator('.quick-preview video');
    await expect(resultVideo).toHaveAttribute('src', /^blob:/u);
    const outputBytes = await resultVideo.evaluate(async (element) => {
      const video = element as HTMLVideoElement;
      const response = await fetch(video.src);
      return new Uint8Array(await response.arrayBuffer()).length;
    });
    expect(outputBytes).toBeGreaterThan(1_000);

    const downloadPromise = page.waitForEvent('download');
    await page.getByRole('button', { name: 'Save result' }).click();
    const download = await downloadPromise;
    expect(download.suggestedFilename()).toBe('pattern-av-convert.mp4');
    const downloadedPath = await download.path();
    expect(downloadedPath).not.toBeNull();
    const probeJson = execFileSync(
      'ffprobe',
      [
        '-v',
        'error',
        '-show_entries',
        'stream=codec_name,codec_type',
        '-of',
        'json',
        downloadedPath as string,
      ],
      { encoding: 'utf8' }
    );
    const nativeProbe = JSON.parse(probeJson) as {
      streams?: Array<{ codec_name?: string; codec_type?: string }>;
    };
    expect(nativeProbe.streams).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          codec_name: 'h264',
          codec_type: 'video',
        }),
        expect.objectContaining({
          codec_name: 'aac',
          codec_type: 'audio',
        }),
      ])
    );

    expect(
      runtime.requests.some((url) =>
        url.endsWith(
          '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
        )
      )
    ).toBe(true);
    assertLocalOnly(runtime);
  });

  test('refreshes retained tile-filter state before a fourth contact sheet', async ({
    page,
  }) => {
    test.setTimeout(120_000);
    const runtime = observeRuntime(page);
    await page.goto('/');
    await selectSingleThread(page);
    await page.getByRole('tab', { name: 'Edit' }).click();
    await mediaInput(page)
      .first()
      .setInputFiles(join(GENERATED_FIXTURES, 'compatible-a.mp4'));
    await expect(
      page
        .locator('.media-card')
        .filter({ hasText: 'compatible-a.mp4' })
        .locator('.phase--ready')
    ).toBeVisible({ timeout: 120_000 });

    const advanced = page.locator('.advanced-operations');
    await advanced
      .getByRole('tab', { name: 'Thumbnails and contact sheets' })
      .click();
    await advanced
      .getByRole('spinbutton', { name: 'Frames', exact: true })
      .fill('4');
    await advanced
      .getByRole('spinbutton', { name: 'Rows', exact: true })
      .fill('2');
    await advanced
      .getByRole('spinbutton', { name: 'Columns', exact: true })
      .fill('2');
    await advanced
      .getByRole('spinbutton', { name: /^Cell width/u })
      .fill('128');

    const results = page.locator('.result-card');
    const create = advanced.getByRole('button', {
      name: 'Generate contact sheet',
    });
    for (let index = 1; index <= 4; index += 1) {
      await create.click();
      await expect(results).toHaveCount(index, { timeout: 120_000 });
      await expect(page.getByRole('alert')).toHaveCount(0);
    }

    await expect(
      results.filter({ hasText: 'compatible-a-contact-sheet' })
    ).toHaveCount(4);
    expect(
      runtime.requests.filter((url) =>
        url.endsWith(
          '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
        )
      )
    ).toHaveLength(2);
    assertLocalOnly(runtime);
  });

  test('cancels an active transcode, recreates the engine and completes another job', async ({
    page,
  }) => {
    const runtime = observeRuntime(page);
    await page.goto('/');
    await selectSingleThread(page);
    await mediaInput(page).setInputFiles(cancellationFixture.filePath);
    await expect(page.locator('.source-summary .phase--ready')).toHaveText(
      'Ready',
      { timeout: 120_000 }
    );

    await page.getByRole('button', { name: 'Convert', exact: true }).click();
    const cancelButton = page.getByRole('button', {
      name: 'Cancel & recover',
    });
    await expect(cancelButton).toBeVisible({ timeout: 30_000 });
    await cancelButton.click();

    await expect(page.getByRole('alert')).toContainText(
      'The operation was cancelled and the engine was recreated.',
      { timeout: 120_000 }
    );
    await expect(
      page
        .locator('.engine-pill')
        .filter({ hasText: 'Single-threaded compatibility mode' })
    ).toBeVisible({ timeout: 120_000 });

    await page
      .getByRole('button', {
        name: `Remove ${basename(cancellationFixture.filePath)}`,
      })
      .click();
    await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
    await expect(page.locator('.source-summary .phase--ready')).toHaveText(
      'Ready',
      { timeout: 120_000 }
    );
    await page.getByRole('button', { name: 'Convert', exact: true }).click();
    await expect(page.locator('.result-card')).toContainText(
      'pattern-av-convert.mp4',
      { timeout: 120_000 }
    );
    await expect(page.getByRole('alert')).toHaveCount(0);
    assertLocalOnly(runtime);
  });

  test('removes active and queued inspections without applying stale probe results', async ({
    page,
  }) => {
    const runtime = observeRuntime(page);
    await page.goto('/');
    await selectSingleThread(page);

    await mediaInput(page).setInputFiles([
      cancellationFixture.filePath,
      PRIMARY_FIXTURE,
    ]);
    const activeName = basename(cancellationFixture.filePath);
    const activeCard = page.locator('.media-card').filter({
      hasText: activeName,
    });
    const queuedCard = page.locator('.media-card').filter({
      hasText: 'pattern-av.mp4',
    });
    await expect(activeCard.locator('.phase--probing')).toBeVisible({
      timeout: 30_000,
    });
    await expect(queuedCard.locator('.phase--queued')).toBeVisible();

    await page.getByRole('button', { name: 'Remove pattern-av.mp4' }).click();
    await page.getByRole('button', { name: `Remove ${activeName}` }).click();
    await expect(page.locator('.media-card')).toHaveCount(0);
    await expect(
      page.getByRole('button', { name: 'Choose files' })
    ).toBeEnabled({ timeout: 120_000 });

    await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
    await expect(page.locator('.source-summary .phase--ready')).toHaveText(
      'Ready',
      { timeout: 120_000 }
    );
    await expect(page.getByRole('alert')).toHaveCount(0);
    assertLocalOnly(runtime);
  });
});
