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

import { createServer, type Server } from 'node:http';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, extname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ConsoleMessage, Page, Request, Response } from '@playwright/test';

export const REPOSITORY_ROOT = resolve(
  dirname(fileURLToPath(import.meta.url)),
  '../..'
);
export const DIST_ROOT = join(REPOSITORY_ROOT, 'dist');
export const GENERATED_FIXTURES = join(
  REPOSITORY_ROOT,
  'tests/fixtures/generated'
);
export const PRIMARY_FIXTURE = join(GENERATED_FIXTURES, 'pattern-av.mp4');

const NESTED_PREFIX = '/deep/nested/av/';
const SECURITY_HEADERS = {
  'Cross-Origin-Embedder-Policy': 'require-corp',
  'Cross-Origin-Opener-Policy': 'same-origin',
  'Cross-Origin-Resource-Policy': 'same-origin',
};

function contentType(filePath: string): string {
  const types: Readonly<Record<string, string>> = {
    '.css': 'text/css; charset=utf-8',
    '.html': 'text/html; charset=utf-8',
    '.js': 'text/javascript; charset=utf-8',
    '.json': 'application/json; charset=utf-8',
    '.svg': 'image/svg+xml',
    '.wasm': 'application/wasm',
  };
  return types[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
}

function readApplicationManifest(): Record<string, unknown> {
  return JSON.parse(
    readFileSync(join(DIST_ROOT, 'toolbox-app.json'), 'utf8')
  ) as Record<string, unknown>;
}

function writeJson(
  response: import('node:http').ServerResponse,
  status: number,
  value: unknown
): void {
  response.writeHead(status, {
    ...SECURITY_HEADERS,
    'Cache-Control': 'no-store',
    'Content-Type': 'application/json; charset=utf-8',
  });
  response.end(JSON.stringify(value));
}

function serveFile(
  response: import('node:http').ServerResponse,
  filePath: string
): void {
  try {
    const stats = statSync(filePath);
    if (!stats.isFile()) {
      response.writeHead(404, SECURITY_HEADERS);
      response.end('Not found');
      return;
    }
    response.writeHead(200, {
      ...SECURITY_HEADERS,
      'Cache-Control': 'no-store',
      'Content-Length': String(stats.size),
      'Content-Type': contentType(filePath),
    });
    response.end(readFileSync(filePath));
  } catch {
    response.writeHead(404, SECURITY_HEADERS);
    response.end('Not found');
  }
}

function closeServer(server: Server): Promise<void> {
  return new Promise((resolveClose, rejectClose) => {
    server.close((error) => {
      if (error) rejectClose(error);
      else resolveClose();
    });
  });
}

export interface NestedStaticServer {
  origin: string;
  appUrl: string;
  close: () => Promise<void>;
}

/**
 * Serves the built artifact exactly as it will be assembled below a portal
 * path. This catches root-absolute URLs that a root-only preview cannot.
 */
export async function startNestedStaticServer(): Promise<NestedStaticServer> {
  const applicationManifest = readApplicationManifest();
  const otherManifest = {
    ...applicationManifest,
    id: 'de.add-ideas.browser-test-tool',
    name: 'Browser Test Tool',
    description: 'A same-origin catalog peer used by browser validation.',
    entry: './',
    actions: [],
  };
  const catalog = {
    $schema:
      'https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json',
    schemaVersion: 1,
    id: 'de.add-ideas.browser-test-toolbox',
    name: 'Browser Test Toolbox',
    home: './',
    theme: { mode: 'system', brand: 'Browser test' },
    apps: [
      { manifest: './av/toolbox-app.json', enabled: true },
      { manifest: './other/toolbox-app.json', enabled: true },
    ],
  };
  const distWithSeparator = `${resolve(DIST_ROOT)}${sep}`;

  const server = createServer((request, response) => {
    const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
    const { pathname } = requestUrl;

    if (pathname === '/deep/nested/toolbox.catalog.json') {
      writeJson(response, 200, catalog);
      return;
    }
    if (pathname === '/deep/nested/invalid-catalog.json') {
      writeJson(response, 200, {
        schemaVersion: 999,
        id: 'invalid',
      });
      return;
    }
    if (pathname === '/deep/nested/other/toolbox-app.json') {
      writeJson(response, 200, otherManifest);
      return;
    }
    if (pathname === '/deep/nested/favicon.svg') {
      serveFile(response, join(DIST_ROOT, 'favicon.svg'));
      return;
    }
    if (!pathname.startsWith(NESTED_PREFIX)) {
      response.writeHead(404, SECURITY_HEADERS);
      response.end('Not found');
      return;
    }

    const relativePath = decodeURIComponent(
      pathname.slice(NESTED_PREFIX.length)
    );
    const requestedPath = resolve(
      DIST_ROOT,
      relativePath === '' ? 'index.html' : relativePath
    );
    if (
      requestedPath !== resolve(DIST_ROOT) &&
      !requestedPath.startsWith(distWithSeparator)
    ) {
      response.writeHead(400, SECURITY_HEADERS);
      response.end('Invalid path');
      return;
    }
    serveFile(response, requestedPath);
  });

  await new Promise<void>((resolveListen, rejectListen) => {
    server.once('error', rejectListen);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', rejectListen);
      resolveListen();
    });
  });
  const address = server.address();
  if (!address || typeof address === 'string') {
    await closeServer(server);
    throw new Error('Nested test server did not expose a TCP port');
  }
  const origin = `http://127.0.0.1:${address.port}`;
  return {
    origin,
    appUrl: `${origin}${NESTED_PREFIX}`,
    close: () => closeServer(server),
  };
}

export interface RuntimeObservation {
  requests: string[];
  responses: Array<{
    url: string;
    status: number;
    contentType: string | undefined;
  }>;
  failedRequests: string[];
  pageErrors: string[];
  consoleErrors: string[];
  consoleWarnings: string[];
}

export function observeRuntime(page: Page): RuntimeObservation {
  const observation: RuntimeObservation = {
    requests: [],
    responses: [],
    failedRequests: [],
    pageErrors: [],
    consoleErrors: [],
    consoleWarnings: [],
  };

  page.on('request', (request: Request) => {
    observation.requests.push(request.url());
  });
  page.on('response', (response: Response) => {
    observation.responses.push({
      url: response.url(),
      status: response.status(),
      contentType: response.headers()['content-type'],
    });
  });
  page.on('requestfailed', (request: Request) => {
    observation.failedRequests.push(
      `${request.url()}: ${request.failure()?.errorText ?? 'unknown failure'}`
    );
  });
  page.on('pageerror', (error: Error) => {
    observation.pageErrors.push(error.stack ?? error.message);
  });
  page.on('console', (message: ConsoleMessage) => {
    if (message.type() === 'error') {
      observation.consoleErrors.push(message.text());
    }
    if (message.type() === 'warning') {
      observation.consoleWarnings.push(message.text());
    }
  });

  return observation;
}

export interface TemporaryMediaFixture {
  filePath: string;
  dispose: () => void;
}

export function createCancellationFixture(): TemporaryMediaFixture {
  const directory = mkdtempSync(join(tmpdir(), 'av-tools-browser-'));
  const filePath = join(directory, 'long-cancellation-source.mp4');

  try {
    execFileSync(
      'ffmpeg',
      [
        '-hide_banner',
        '-loglevel',
        'error',
        '-y',
        '-stream_loop',
        '14',
        '-i',
        PRIMARY_FIXTURE,
        '-t',
        '30',
        '-vf',
        'scale=1280:720',
        '-r',
        '30',
        '-c:v',
        'libx264',
        '-preset',
        'ultrafast',
        '-crf',
        '32',
        '-c:a',
        'aac',
        '-b:a',
        '64k',
        filePath,
      ],
      { stdio: 'pipe' }
    );
  } catch (error) {
    rmSync(directory, { recursive: true, force: true });
    throw error;
  }

  return {
    filePath,
    dispose: () => rmSync(directory, { recursive: true, force: true }),
  };
}
