/**
 * MIT License
 *
 * Copyright (c) 2020-present, Elastic NV
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

import {
  chromium,
  ChromiumBrowser,
  BrowserContext,
  request as apiRequest,
  selectors,
} from 'playwright-core';
import { PluginManager } from '../plugins';
import { log } from './logger';
import { Driver, NetworkConditions, RunOptions } from '../common_types';

// Default timeout for Playwright actions and Navigations
const DEFAULT_TIMEOUT = 50000;

/**
 * Purpose of the Gatherer is to set up the necessary browser driver
 * related capabilities for the runner to run all journeys
 */
export class Gatherer {
  static browser: ChromiumBrowser;
  static pluginManager: PluginManager;

  static async launchBrowser(options: RunOptions) {
    if (Gatherer.browser != null) {
      return;
    }

    log('Gatherer: setup browser');
    const { wsEndpoint, playwrightOptions } = options;
    if (wsEndpoint) {
      log(`Gatherer: connecting to WS endpoint: ${wsEndpoint}`);
      Gatherer.browser = await chromium.connect(wsEndpoint);
    } else {
      Gatherer.browser = await chromium.launch({
        ...playwrightOptions,
        args: [
          ...(playwrightOptions?.headless ? ['--disable-gpu'] : []),
          ...(playwrightOptions?.args ?? []),
        ],
      });
    }
    // Register sig int handler to close the browser
    process.on('SIGINT', async () => {
      await Gatherer.stop();
      process.exit(130);
    });
  }

  static async setupDriver(options: RunOptions): Promise<Driver> {
    await Gatherer.launchBrowser(options);
    const { playwrightOptions } = options;
    const context = await Gatherer.browser.newContext({
      ...playwrightOptions,
      userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent),
    });
    // Set timeouts for actions and navigations
    context.setDefaultTimeout(
      playwrightOptions?.actionTimeout ?? DEFAULT_TIMEOUT
    );
    context.setDefaultNavigationTimeout(
      playwrightOptions?.navigationTimeout ?? DEFAULT_TIMEOUT
    );

    // TODO: Network throttling via chrome devtools emulation is disabled for now.
    // See docs/throttling.md for more details.
    // Gatherer.setNetworkConditions(context, networkConditions);
    if (playwrightOptions?.testIdAttribute) {
      selectors.setTestIdAttribute(playwrightOptions.testIdAttribute);
    }

    const page = await context.newPage();
    const client = await context.newCDPSession(page);
    const request = await apiRequest.newContext({ ...playwrightOptions });
    return { browser: Gatherer.browser, context, page, client, request };
  }

  static async getUserAgent(userAgent?: string) {
    if (!userAgent) {
      const session = await Gatherer.browser.newBrowserCDPSession();
      ({ userAgent } = await session.send('Browser.getVersion'));
      return userAgent + ' Elastic/Synthetics';
    }
    return userAgent;
  }

  static setNetworkConditions(
    context: BrowserContext,
    networkConditions: NetworkConditions
  ) {
    if (networkConditions) {
      context.on('page', page => {
        const context = page.context();
        const emulatePromise = context
          .newCDPSession(page)
          .then(client =>
            client.send('Network.emulateNetworkConditions', networkConditions)
          );
        /**
         * Guard against pages that gets closed before the emulation kicks to capture
         * unhandled rejections from accessing the CDP session of closed page
         */
        Promise.race([
          new Promise<void>(resolve => page.on('close', () => resolve())),
          emulatePromise,
        ]);
      });
    }
  }

  /**
   * Starts recording all events related to the v8 devtools protocol
   * https://chromedevtools.github.io/devtools-protocol/v8/
   */
  static async beginRecording(driver: Driver, options: RunOptions) {
    log('Gatherer: started recording');
    const { network, metrics } = options;
    Gatherer.pluginManager = new PluginManager(driver);
    Gatherer.pluginManager.registerAll(options);
    const plugins = [await Gatherer.pluginManager.start('browserconsole')];
    network && plugins.push(await Gatherer.pluginManager.start('network'));
    metrics && plugins.push(await Gatherer.pluginManager.start('performance'));
    await Promise.all(plugins);
    return Gatherer.pluginManager;
  }

  static async endRecording() {
    log('Gatherer: ended recording');
    await Gatherer.pluginManager.unregisterAll();
  }

  static async dispose(driver: Driver) {
    log(`Gatherer: closing all contexts`);
    await driver.request.dispose();
    await driver.context.close();
  }

  static async stop() {
    log(`Gatherer: closing browser`);
    if (Gatherer.browser) {
      await Gatherer.browser.close();
      Gatherer.browser = null;
    }
  }
}
