/**
 * 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 { getSpkiFingerprints } from './certs';
import {
  APIDriver,
  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 {
      /**
       * Chromium on Linux has its own NSS trust store. This flag does not add
       * a CA to that store: it bypasses certificate errors only when a
       * presented certificate's SPKI matches one of the supplied fingerprints.
       * It is narrower than `ignoreHTTPSErrors`, but is not CA trust.
       */
      const spkiFingerprints = getSpkiFingerprints(
        options.certificateErrorSpkiAllowlist
      );
      if (spkiFingerprints.length > 0) {
        log(
          `Gatherer: bypassing certificate errors for ${spkiFingerprints.length} allowlisted SPKI fingerprint(s)`
        );
      }
      Gatherer.browser = await chromium.launch({
        ...playwrightOptions,
        args: [
          ...(playwrightOptions?.headless ? ['--disable-gpu'] : []),
          ...(spkiFingerprints.length > 0
            ? [
                `--ignore-certificate-errors-spki-list=${spkiFingerprints.join(
                  ','
                )}`,
              ]
            : []),
          ...(playwrightOptions?.args ?? []),
        ],
      });
    }
    // Register sig int handler to close the browser
    process.on('SIGINT', async () => {
      await Gatherer.stop();
      process.exit(130);
    });
  }

  static async setupDriver<T extends 'browser' | 'api' = 'browser'>(
    options: RunOptions,
    journeyType: T = 'browser' as T
  ): Promise<T extends 'api' ? APIDriver : Driver> {
    const { playwrightOptions } = options;

    if (journeyType === 'api') {
      const request = await apiRequest.newContext({ ...playwrightOptions });
      return { request } as T extends 'api' ? APIDriver : Driver;
    }

    await Gatherer.launchBrowser(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,
    } as T extends 'api' ? APIDriver : Driver;
  }

  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 | APIDriver, options: RunOptions) {
    log('Gatherer: started recording');
    const { network, metrics } = options;
    Gatherer.pluginManager = new PluginManager(driver);
    Gatherer.pluginManager.registerAll(options);
    const plugins = [];
    if ('browser' in driver) {
      plugins.push(await Gatherer.pluginManager.start('browserconsole'));
      network && plugins.push(await Gatherer.pluginManager.start('network'));
      metrics &&
        plugins.push(await Gatherer.pluginManager.start('performance'));
    } else {
      // Network is the only data source for API journeys, so always capture
      // it regardless of `options.network`.
      plugins.push(await Gatherer.pluginManager.start('network'));
    }
    await Promise.all(plugins);
    return Gatherer.pluginManager;
  }

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

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

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