/**
 * 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, expect } from './fixtures';

import { tools } from '../../packages/playwright-core/lib/coreBundle';
import type { Config } from '../../packages/playwright-core/src/tools/mcp/config.d';

const { resolveCLIConfigForCLI, resolveCLIConfigForMCP, isSystemDirectory, outputDir } = tools;

// Empty env to isolate tests from the host environment.
const emptyEnv = {};

test.skip(({ mcpBrowser }) => mcpBrowser !== 'chrome', 'Channel-agnostic tests.');

test.describe('browserName and channel', () => {
  test('no browser option defaults to chromium / chrome', async () => {
    const config = await resolveCLIConfigForMCP({}, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBe('chrome');
  });

  test('--browser=chrome sets chromium with chrome channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chrome' }, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBe('chrome');
  });

  test('--browser=chromium sets chromium with chrome-for-testing channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chromium' }, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBe('chrome-for-testing');
  });

  test('--browser=firefox sets firefox without channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'firefox' }, emptyEnv);
    expect(config.browser.browserName).toBe('firefox');
    expect(config.browser.launchOptions.channel).toBeUndefined();
  });

  test('--browser=webkit sets webkit without channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'webkit' }, emptyEnv);
    expect(config.browser.browserName).toBe('webkit');
    expect(config.browser.launchOptions.channel).toBeUndefined();
  });

  test('--browser=msedge sets chromium with msedge channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'msedge' }, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBe('msedge');
  });

  test('config file browserName chromium does not auto-set channel', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'chromium' } }));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBeUndefined();
  });

  test('config file browserName firefox does not set channel', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'firefox' } }));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
    expect(config.browser.browserName).toBe('firefox');
    expect(config.browser.launchOptions.channel).toBeUndefined();
  });

  test('config file browserName + channel are both preserved', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      browser: { browserName: 'chromium', launchOptions: { channel: 'msedge' } },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
    expect(config.browser.browserName).toBe('chromium');
    expect(config.browser.launchOptions.channel).toBe('msedge');
  });

  test('cli --browser overrides config file browserName', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'firefox' } }));
    const config = await resolveCLIConfigForMCP({ config: configFile, browser: 'webkit' }, emptyEnv);
    expect(config.browser.browserName).toBe('webkit');
  });
});

// ---------------------------------------------------------------------------
// Shared behavior — sandbox
// ---------------------------------------------------------------------------

test.describe('sandbox', () => {
  test('chromium sandbox enabled for chrome channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chrome' }, emptyEnv);
    expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
  });

  test('chromium sandbox for chrome-for-testing channel', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chromium' }, emptyEnv);
    expect(config.browser.launchOptions.channel).toBe('chrome-for-testing');
    if (process.platform === 'linux')
      expect(config.browser.launchOptions.chromiumSandbox).toBe(false);
    else
      expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
  });

  test('sandbox not set for non-chromium browsers', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'firefox' }, emptyEnv);
    expect(config.browser.launchOptions.chromiumSandbox).toBeUndefined();
  });

  test('explicit --sandbox overrides default', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chromium', sandbox: true }, emptyEnv);
    expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
  });

  test('explicit --no-sandbox overrides default', async () => {
    const config = await resolveCLIConfigForMCP({ browser: 'chrome', sandbox: false }, emptyEnv);
    expect(config.browser.launchOptions.chromiumSandbox).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Shared behavior — timeouts
// ---------------------------------------------------------------------------

test.describe('timeouts', () => {
  test('default timeouts', async () => {
    const config = await resolveCLIConfigForMCP({}, emptyEnv);
    expect(config.timeouts.action).toBe(5000);
    expect(config.timeouts.navigation).toBe(60000);
    expect(config.timeouts.expect).toBe(5000);
  });

  test('config file timeouts override defaults', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 7000 } }));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
    expect(config.timeouts.action).toBe(7000);
    expect(config.timeouts.navigation).toBe(60000);
  });
});

// ---------------------------------------------------------------------------
// Shared behavior — viewport
// ---------------------------------------------------------------------------

test.describe('viewport', () => {
  test('headless sets default viewport 1280x720', async () => {
    const config = await resolveCLIConfigForMCP({ headless: true }, emptyEnv);
    expect(config.browser.contextOptions.viewport).toEqual({ width: 1280, height: 720 });
  });

  test('headed sets viewport to null', async () => {
    const config = await resolveCLIConfigForMCP({ headless: false }, emptyEnv);
    expect(config.browser.contextOptions.viewport).toBeNull();
  });

  test('explicit viewport is preserved', async () => {
    const config = await resolveCLIConfigForMCP({ headless: true, viewportSize: { width: 800, height: 600 } }, emptyEnv);
    expect(config.browser.contextOptions.viewport).toEqual({ width: 800, height: 600 });
  });

  test('config file viewport is preserved', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLIConfigForMCP({ config: configFile, headless: true }, emptyEnv);
    expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
  });
});

// ---------------------------------------------------------------------------
// Shared behavior — merge order and config file
// ---------------------------------------------------------------------------

test.describe('merge order', () => {
  test('cli overrides config file', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      timeouts: { action: 1000 },
      browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLIConfigForMCP({ config: configFile, timeoutAction: 9999 }, emptyEnv);
    expect(config.timeouts.action).toBe(9999);
    expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
  });

  test('config file values preserved when cli does not override', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      network: { allowedOrigins: ['https://example.com'] },
      browser: { isolated: true },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv) as any;
    expect(config.network?.allowedOrigins).toEqual(['https://example.com']);
    expect(config.browser.isolated).toBe(true);
  });

  test('env overrides config file', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { navigation: 1000 } }));
    const config = await resolveCLIConfigForMCP({ config: configFile }, {
      PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION: '45000',
    });
    expect(config.timeouts.navigation).toBe(45000);
  });

  test('cli overrides env', async () => {
    const config = await resolveCLIConfigForMCP({ timeoutAction: 3000, browser: 'firefox' }, {
      PLAYWRIGHT_MCP_TIMEOUT_ACTION: '9000',
      PLAYWRIGHT_MCP_BROWSER: 'webkit',
    });
    expect(config.timeouts.action).toBe(3000);
    expect(config.browser.browserName).toBe('firefox');
  });

  test('file browser.cdpHeaders preserved when env unset', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      browser: {
        cdpEndpoint: 'ws://example.invalid',
        cdpHeaders: { Authorization: 'Bearer token-from-file' },
      },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
    expect(config.browser.cdpHeaders).toEqual({ Authorization: 'Bearer token-from-file' });
  });
});

// ---------------------------------------------------------------------------
// Shared behavior — validation
// ---------------------------------------------------------------------------

test.describe('validation', () => {
  test('isolated + userDataDir throws', async () => {
    await expect(resolveCLIConfigForMCP({ isolated: true, userDataDir: '/tmp/data' }, emptyEnv))
        .rejects.toThrow('Browser userDataDir is not supported in isolated mode.');
  });
});

// ---------------------------------------------------------------------------
// Output directory — fallback for unsuitable cwd, throw on explicit system dir
// ---------------------------------------------------------------------------

test.describe('outputDir', () => {
  test.skip(process.platform === 'win32', 'POSIX-specific cases');

  test('falls back to tmpdir when cwd is /', () => {
    const result = outputDir({ config: {}, cwd: '/' });
    expect(result).toBe(path.join(os.tmpdir(), '.playwright-mcp'));
  });

  test('uses cwd-relative .playwright-mcp for normal cwd', ({}, testInfo) => {
    const cwd = testInfo.outputPath('workspace');
    fs.mkdirSync(cwd, { recursive: true });
    const result = outputDir({ config: {}, cwd });
    expect(result).toBe(path.join(cwd, '.playwright-mcp'));
  });

  test('uses .playwright-cli when skillMode is set', ({}, testInfo) => {
    const cwd = testInfo.outputPath('workspace');
    fs.mkdirSync(cwd, { recursive: true });
    const result = outputDir({ config: { skillMode: true }, cwd });
    expect(result).toBe(path.join(cwd, '.playwright-cli'));
  });

  test('skillMode falls back to tmpdir/.playwright-cli when cwd is /', () => {
    const result = outputDir({ config: { skillMode: true }, cwd: '/' });
    expect(result).toBe(path.join(os.tmpdir(), '.playwright-cli'));
  });

  test('explicit outputDir wins regardless of cwd', ({}, testInfo) => {
    const explicit = testInfo.outputPath('explicit');
    const result = outputDir({ config: { outputDir: explicit }, cwd: '/' });
    expect(result).toBe(explicit);
  });

  test('falls back to tmpdir when cwd is not writable', ({}, testInfo) => {
    const cwd = testInfo.outputPath('readonly');
    fs.mkdirSync(cwd, { recursive: true });
    fs.chmodSync(cwd, 0o500);
    try {
      const result = outputDir({ config: {}, cwd });
      expect(result).toBe(path.join(os.tmpdir(), '.playwright-mcp'));
    } finally {
      fs.chmodSync(cwd, 0o700);
    }
  });

  test('isSystemDirectory detects /', () => {
    expect(isSystemDirectory('/')).toBe(true);
    expect(isSystemDirectory('/tmp')).toBe(false);
    expect(isSystemDirectory(os.homedir())).toBe(false);
  });

  test('resolveCLIConfigForMCP throws when --output-dir is /', async () => {
    await expect(resolveCLIConfigForMCP({ outputDir: '/' }, emptyEnv))
        .rejects.toThrow(/--output-dir cannot point to a system directory/);
  });
});

// ---------------------------------------------------------------------------
// MCP-specific: headless platform default, timeout CLI options
// ---------------------------------------------------------------------------

test.describe('resolveCLIConfigForMCP', () => {
  test('headless defaults based on platform', async () => {
    const config = await resolveCLIConfigForMCP({}, emptyEnv);
    expect(config.browser.launchOptions.headless).toBeDefined();
  });

  test('cli timeout overrides defaults', async () => {
    const config = await resolveCLIConfigForMCP({ timeoutAction: 10000, timeoutNavigation: 30000 }, emptyEnv);
    expect(config.timeouts.action).toBe(10000);
    expect(config.timeouts.navigation).toBe(30000);
    expect(config.timeouts.expect).toBe(5000);
  });

  test('cli timeout overrides config file timeout', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 1000 } }));
    const config = await resolveCLIConfigForMCP({ config: configFile, timeoutAction: 9999 }, emptyEnv);
    expect(config.timeouts.action).toBe(9999);
  });
});

// ---------------------------------------------------------------------------
// CLI-specific: headless, isolated, userDataDir, extension, skillMode, snapshot
// ---------------------------------------------------------------------------

function resolveCLI(profilesDir: string, sessionName: string, options: any) {
  return resolveCLIConfigForCLI(profilesDir, sessionName, options, emptyEnv);
}

test.describe('resolveCLIConfigForCLI - headless and viewport', () => {
  test('headless by default', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
    expect(config.browser.launchOptions.headless).toBe(true);
  });

  test('--headed sets headless=false', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { headed: true });
    expect(config.browser.launchOptions.headless).toBe(false);
  });

  test('headless viewport defaults to 1280x720', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
    expect(config.browser.contextOptions.viewport).toEqual({ width: 1280, height: 720 });
  });

  test('headed viewport defaults to null', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { headed: true });
    expect(config.browser.contextOptions.viewport).toBeNull();
  });

  test('config file viewport is preserved', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
    expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
  });
});

test.describe('resolveCLIConfigForCLI - isolated and userDataDir', () => {
  test('defaults to isolated when no profile, persistent, userDataDir, or remoteEndpoint', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
    expect(config.browser.isolated).toBe(true);
  });

  test('not isolated when --profile is set', async ({}, testInfo) => {
    const profileDir = testInfo.outputPath('my-profile');
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { profile: profileDir });
    expect(config.browser.isolated).toBe(false);
    expect(config.browser.userDataDir).toBe(profileDir);
  });

  test('not isolated when --persistent is set', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { persistent: true });
    expect(config.browser.isolated).toBe(false);
  });

  test('not isolated when --attach is set', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { endpoint: 'ws://localhost:1234' });
    expect(config.browser.isolated).toBe(false);
  });

  test('not isolated when config file sets userDataDir', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const userDataDir = testInfo.outputPath('custom-data');
    await fs.promises.writeFile(configFile, JSON.stringify({ browser: { userDataDir } }));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
    expect(config.browser.isolated).toBe(false);
    expect(config.browser.userDataDir).toBe(userDataDir);
  });

  test('auto userDataDir uses daemonProfilesDir with session and browser token', async ({}, testInfo) => {
    const profilesDir = testInfo.outputPath('profiles');
    const config = await resolveCLI(profilesDir, 'mysession', { persistent: true, browser: 'chrome' });
    expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-mysession-chrome'));
  });

  test('auto userDataDir uses browserName when no channel', async ({}, testInfo) => {
    const profilesDir = testInfo.outputPath('profiles');
    const config = await resolveCLI(profilesDir, 'default', { persistent: true, browser: 'firefox' });
    expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-default-firefox'));
  });

  test('auto userDataDir uses undefined token when no browser specified', async ({}, testInfo) => {
    const profilesDir = testInfo.outputPath('profiles');
    const config = await resolveCLI(profilesDir, 'default', { persistent: true });
    expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-default-chrome'));
  });

  test('no auto userDataDir when isolated', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
    expect(config.browser.isolated).toBe(true);
    expect(config.browser.userDataDir).toBeUndefined();
  });

  test('no auto userDataDir when --profile is set', async ({}, testInfo) => {
    const profileDir = testInfo.outputPath('my-profile');
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { profile: profileDir });
    expect(config.browser.userDataDir).toBe(profileDir);
  });

  test('no auto userDataDir when remoteEndpoint is set', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { attach: 'ws://localhost:1234' });
    expect(config.browser.userDataDir).toBeUndefined();
  });
});

test.describe('resolveCLIConfigForCLI - timeouts', () => {
  test('config file timeouts override defaults', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 7000 } }));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
    expect(config.timeouts.action).toBe(7000);
    expect(config.timeouts.navigation).toBe(60000);
  });
});

test.describe('resolveCLIConfigForCLI - skillMode and snapshotMode', () => {
  test('skillMode is always set', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
    expect(config.skillMode).toBe(true);
  });

  test('snapshot mode is full', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {}) as any;
    expect(config.snapshot?.mode).toBe('full');
  });
});

test.describe('resolveCLIConfigForCLI - config file discovery', () => {
  test('explicit config file is used', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 3000 } }));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
    expect(config.timeouts.action).toBe(3000);
    expect(config.configFile).toBe(configFile);
  });

  test('cli overrides env', async ({}, testInfo) => {
    const config = await resolveCLIConfigForCLI(testInfo.outputPath('profiles'), 'default', { browser: 'firefox' }, {
      PLAYWRIGHT_MCP_BROWSER: 'webkit',
    });
    expect(config.browser.browserName).toBe('firefox');
  });

  test('merge order: config file < daemon overrides', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      browser: { browserName: 'firefox' },
      timeouts: { action: 1000 },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile, browser: 'webkit' });
    expect(config.browser.browserName).toBe('webkit');
    expect(config.timeouts.action).toBe(1000);
  });

  test('config file values preserved when cli does not override', async ({}, testInfo) => {
    const configFile = testInfo.outputPath('config.json');
    const fileConfig: Config = {
      network: { allowedOrigins: ['https://example.com'] },
    };
    await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile }) as any;
    expect(config.network?.allowedOrigins).toEqual(['https://example.com']);
  });
});

test.describe('resolveCLIConfigForCLI - extension', () => {
  test('--extension disables isolated', async ({}, testInfo) => {
    const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { extension: true }) as any;
    expect(config.extension).toBe(true);
    expect(config.browser.isolated).toBe(false);
  });
});
