/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import fs from 'fs';
import os from 'os';
import path from 'path';

import { test as baseTest, expect } from './fixtures';
import { killProcessGroup } from '../config/commonFixtures';
import { inheritAndCleanEnv } from '../config/utils';

import type { Page, Browser } from 'playwright-core';
import type { CommonFixtures } from '../config/commonFixtures';

export { expect } from './fixtures';
export const test = baseTest.extend<{
  boundBrowser: Browser,
  cliEnv: Record<string, string>,
  startDashboardServer: (options?: { cwd?: string, session?: string }) => Promise<Page>,
  connectToDashboard: (bindTitle: string) => Promise<Browser>;
  cli: (...args: any[]) => Promise<{
    output: string,
    error: string,
    exitCode: number | undefined,
    inlineSnapshot?: string,
    snapshot?: string,
    attachments?: { name: string, data: Buffer | null }[],
    daemonPid?: number,
    dashboardPid?: number,
  }>;
}>({
  cliEnv: async ({}, use) => {
    await use(cliEnv());
  },
  startDashboardServer: async ({ childProcess, page }, use) => {
    await use(async (options?: { cwd?: string, session?: string }) => {
      const testInfo = test.info();
      const showArgs = options?.session ? [`-s=${options.session}`, 'show'] : ['show'];
      const serverProcess = childProcess({
        command: [process.execPath, require.resolve('../../packages/playwright-core/lib/tools/cli-client/cli.js'), ...showArgs, '--port=0'],
        cwd: options?.cwd ?? testInfo.outputPath(),
        env: inheritAndCleanEnv(cliEnv()),
      });
      await serverProcess.waitForOutput('Listening on ');
      await page.goto(serverProcess.output.match(/Listening on (http:\/\/\S+)/)![1]);
      return page;
    });
  },
  connectToDashboard: async ({ cli, playwright }, use) => {
    await use(async (bindTitle: string) => {
      let endpoint = '';
      await expect(async () => {
        const { output } = await cli('list', '--all', '--json');
        const { servers } = JSON.parse(output);
        const server = servers.find(s => s.title === bindTitle);
        endpoint = server.endpoint;
      }).toPass();
      return await playwright.chromium.connect(endpoint);
    });
    await cli('show', '--kill');
  },

  cli: async ({ mcpBrowser, mcpHeadless, childProcess }, use) => {
    await fs.promises.mkdir(test.info().outputPath('.playwright'), { recursive: true });
    const allPids: number[] = [];

    await use(async (...args: string[]) => {
      const cliArgs = args.filter(arg => typeof arg === 'string');
      const cliOptions = args.findLast(arg => typeof arg === 'object') || {};
      const result = await runCli(childProcess, cliArgs, cliOptions, { mcpBrowser, mcpHeadless });
      if (result.daemonPid)
        allPids.push(result.daemonPid);
      if (result.dashboardPid)
        allPids.push(result.dashboardPid);
      return result;
    });

    for (const pid of allPids)
      killProcessGroup(pid);

    const daemonDir = test.info().outputPath('daemon');
    for (const dir of await fs.promises.readdir(daemonDir).catch<string[]>(() => [])) {
      if (dir.startsWith('ud-')) {
        await fs.promises.rm(path.join(daemonDir, dir), { recursive: true, force: true }).catch(() => {});
        continue;
      }
    }
  },
  boundBrowser: async ({ mcpBrowser, playwright }, use) => {
    const browserName = (mcpBrowser === 'chrome' || mcpBrowser === 'msedge') ? 'chromium' : mcpBrowser;
    const channel = (mcpBrowser === 'chrome' || mcpBrowser === 'msedge') ? mcpBrowser : undefined;
    const browser = await playwright[browserName].launch({ channel, headless: true });
    for (const [name, value] of Object.entries(cliEnv()))
      process.env[name] = value;
    await browser.bind('default');
    await use(browser);
    for (const name of Object.keys(cliEnv()))
      delete process.env[name];
    await browser.close();
  },
});

function cliEnv() {
  return {
    PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'),
    PWTEST_DASHBOARD_SETTINGS_FILE: test.info().outputPath('dashboard.settings.json'),
    PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'),
    PLAYWRIGHT_SOCKETS_DIR: path.join(os.tmpdir(), 'ds' + String(test.info().workerIndex)),
    PWTEST_CLI_CHANNEL_SCAN_DISABLED_FOR_TEST: '1',
  };
}

async function runCli(childProcess: CommonFixtures['childProcess'], args: string[], cliOptions: { cwd?: string, env?: Record<string, string>, bindTitle?: string }, options: { mcpBrowser: string, mcpHeadless: boolean }) {
  const testInfo = test.info();
  const cli = childProcess({
    command: [process.execPath, require.resolve('../../packages/playwright-core/lib/tools/cli-client/cli.js'), ...args],
    cwd: cliOptions.cwd ?? testInfo.outputPath(),
    env: inheritAndCleanEnv({
      ...cliEnv(),
      PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,
      PLAYWRIGHT_MCP_HEADLESS: String(options.mcpHeadless),
      PWTEST_PRINT_DASHBOARD_PID_FOR_TEST: '1',
      PWTEST_DASHBOARD_APP_BIND_TITLE: cliOptions.bindTitle,
      ...cliOptions.env,
    }),
  });

  // Wait for the CLI to exit so stdout is complete before we parse it.
  const exitCode = await cli.exitCode;

  let snapshot: string | undefined;
  let inlineSnapshot: string | undefined;
  if (cli.stdout.includes('### Snapshot'))
    ({ snapshot, inlineSnapshot } = await loadSnapshot(cli.stdout));
  const attachments = loadAttachments(cli.stdout);

  const browserMatches = cli.stdout.includes('### Browser') ? cli.stdout.match(/Browser `(.+)` opened with pid (\d+)\./) : undefined;
  const daemonPid = browserMatches?.[2] ?? parseJsonPid(cli.stdout);
  const dashboardMatches = cli.stdout.includes('### Dashboard') ? cli.stdout.match(/Dashboard opened with pid (\d+)\./) : undefined;
  const dashboardPid = dashboardMatches?.[1];

  return {
    exitCode,
    output: cli.stdout.trim(),
    error: cli.stderr.trim(),
    snapshot,
    inlineSnapshot,
    attachments,
    daemonPid: daemonPid ? +daemonPid : undefined,
    dashboardPid: dashboardPid ? +dashboardPid : undefined,
  };
}

function parseJsonPid(stdout: string) {
  try {
    return JSON.parse(stdout).pid;
  } catch {
  }
}

function loadAttachments(output: string) {
  // attachments look like md links  - [Page as pdf](.playwright-cli/page-2026-01-22T23-13-56-347Z.pdf)
  const match = output.match(/- \[(.+)\]\((.+)\)/g);
  if (!match)
    return [];

  return match.map(m => {
    const [, name, path] = m.match(/- \[(.+)\]\((.+)\)/)!;
    try {
      const data = fs.readFileSync(test.info().outputPath(path));
      return { name, data };
    } catch (e) {
      return { name, data: null };
    }
  });
}

async function loadSnapshot(output: string): Promise<{ snapshot?: string, inlineSnapshot?: string }> {
  const lines = output.split('\n');
  if (!lines.includes('### Snapshot'))
    throw new Error('Snapshot file not found');
  const snapshotIndex = lines.indexOf('### Snapshot') + 1;
  const fileLine = lines[snapshotIndex];
  if (fileLine.startsWith('```yaml'))
    return { inlineSnapshot: lines.slice(snapshotIndex + 1, lines.indexOf('```', snapshotIndex)).join('\n') };
  const fileName = fileLine.match(/- \[(.+)\]\((.+)\)/)![2];
  try {
    return { snapshot: await fs.promises.readFile(test.info().outputPath(fileName), 'utf8').catch(() => undefined) };
  } catch (e) {
    return {};
  }
}

export const eventsPage = `<!DOCTYPE html>
<html>
  <body style="width: 400px; height: 400px; margin: 0; padding: 0;">
    <div id='square'style="width: 100px; height: 100px;"></div>
    <div id='log'></div>
    <script>
      const logElement = document.querySelector('#log');

      const log = (...args) => {
        const el = document.createElement('div');
        el.textContent = args.join(' ');
        logElement.appendChild(el);
      };
      document.body.addEventListener('mousemove', (event) => {
        log('mouse move', event.clientX, event.clientY);
      });
      document.body.addEventListener('mousedown', () => {
        log('mouse down');
      });
      document.body.addEventListener('mouseup', () => {
        log('mouse up');
      });
      document.body.addEventListener('wheel', (event) => {
        log('wheel', event.deltaX, event.deltaY);
      });
      document.body.addEventListener('click', () => {
        log('click', event.button);
      });
      document.body.addEventListener('dblclick', (event) => {
        log('dblclick', event.button);
      });
    </script>
  </body>
</html>
`;

export async function findDefaultSession() {
  const daemonDir = await daemonFolder();
  const fileName = path.join(daemonDir, 'default.session');
  return await fs.promises.readFile(fileName, 'utf-8').then(JSON.parse).catch(() => null);
}

export async function daemonFolder() {
  const daemonDir = test.info().outputPath('daemon');
  const folders = await fs.promises.readdir(daemonDir);
  for (const folder of folders) {
    const fullName = path.join(daemonDir, folder);
    if (fs.lstatSync(path.join(fullName)).isDirectory())
      return fullName;
  }
  return null;
}
