/**
 * Copyright 2022 Google Inc. All rights reserved.
 *
 * 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
 *
 *     https://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 Mocha from 'mocha';
import commonInterface from 'mocha/lib/interfaces/common';
import {
  setLogCapture,
  getCapturedLogs,
} from 'puppeteer-core/internal/common/Debug.js';

import {testIdMatchesExpectationPattern} from './utils.js';

type SuiteFunction = ((this: Mocha.Suite) => void) | undefined;
type ExclusiveSuiteFunction = (this: Mocha.Suite) => void;

const skippedTests: Array<{testIdPattern: string; skip: true}> = process.env[
  'PUPPETEER_SKIPPED_TEST_CONFIG'
]
  ? JSON.parse(process.env['PUPPETEER_SKIPPED_TEST_CONFIG'])
  : [];

const deflakeRetries = Number(
  process.env['PUPPETEER_DEFLAKE_RETRIES']
    ? process.env['PUPPETEER_DEFLAKE_RETRIES']
    : 100
);
const deflakeTestPattern: string | undefined =
  process.env['PUPPETEER_DEFLAKE_TESTS'];

function shouldSkipTest(test: Mocha.Test): boolean {
  // TODO: more efficient lookup.
  const definition = skippedTests.find(skippedTest => {
    return testIdMatchesExpectationPattern(test, skippedTest.testIdPattern);
  });
  if (definition && definition.skip) {
    return true;
  }
  return false;
}

function shouldDeflakeTest(test: Mocha.Test): boolean {
  if (deflakeTestPattern) {
    // TODO: cache if we have seen it already
    return testIdMatchesExpectationPattern(test, deflakeTestPattern);
  }
  return false;
}

function dumpLogsIfFail(this: Mocha.Context) {
  if (this.currentTest?.state === 'failed') {
    console.log(
      `\n"${this.currentTest.fullTitle()}" failed. Here is a debug log:`
    );
    console.log(getCapturedLogs().join('\n') + '\n');
  }
  setLogCapture(false);
}

function customBDDInterface(suite: Mocha.Suite) {
  const suites: [Mocha.Suite] = [suite];

  suite.on(
    Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE,
    function (context, file, mocha) {
      const common = commonInterface(suites, context, mocha);

      context['before'] = common.before;
      context['after'] = common.after;
      context['beforeEach'] = common.beforeEach;
      context['afterEach'] = common.afterEach;
      if (mocha.options.delay) {
        context['run'] = common.runWithSuite(suite);
      }
      function describe(title: string, fn: SuiteFunction) {
        return common.suite.create({
          title: title,
          file: file,
          fn: fn,
        });
      }
      describe.only = function (title: string, fn: ExclusiveSuiteFunction) {
        return common.suite.only({
          title: title,
          file: file,
          fn: fn,
          isOnly: true,
        });
      };

      describe.skip = function (title: string, fn: SuiteFunction) {
        return common.suite.skip({
          title: title,
          file: file,
          fn: fn,
        });
      };

      describe.withDebugLogs = function (
        description: string,
        body: (this: Mocha.Suite) => void
      ): void {
        context['describe']('with Debug Logs', () => {
          context['beforeEach'](() => {
            setLogCapture(true);
          });
          context['afterEach'](dumpLogsIfFail);
          context['describe'](description, body);
        });
      };

      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-expect-error
      context['describe'] = describe;

      function it(title: string, fn: Mocha.TestFunction, itOnly = false) {
        const suite = suites[0]! as Mocha.Suite;
        const test = new Mocha.Test(title, suite.isPending() ? undefined : fn);
        test.file = file;
        test.parent = suite;

        const describeOnly = Boolean(
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error
          suite.parent?._onlySuites.find(child => {
            return child === suite;
          })
        );
        if (shouldDeflakeTest(test)) {
          const deflakeSuit = Mocha.Suite.create(suite, 'with Debug Logs');
          test.file = file;
          deflakeSuit.beforeEach(function () {
            setLogCapture(true);
          });
          deflakeSuit.afterEach(dumpLogsIfFail);
          for (let i = 0; i < deflakeRetries; i++) {
            deflakeSuit.addTest(test.clone());
          }
          return test;
        } else if (!(itOnly || describeOnly) && shouldSkipTest(test)) {
          const test = new Mocha.Test(title);
          test.file = file;
          suite.addTest(test);
          return test;
        } else {
          suite.addTest(test);
          return test;
        }
      }

      it.only = function (title: string, fn: Mocha.TestFunction) {
        return common.test.only(
          mocha,
          (context['it'] as unknown as typeof it)(title, fn, true)
        );
      };

      it.skip = function (title: string) {
        return context['it'](title);
      };

      function wrapDeflake(
        func: Function
      ): (repeats: number, title: string, fn: Mocha.AsyncFunc) => void {
        return (repeats: number, title: string, fn: Mocha.AsyncFunc): void => {
          (context['describe'] as unknown as typeof describe).withDebugLogs(
            'with Debug Logs',
            () => {
              for (let i = 1; i <= repeats; i++) {
                func(`${i}/${title}`, fn);
              }
            }
          );
        };
      }

      it.deflake = wrapDeflake(it);
      it.deflakeOnly = wrapDeflake(it.only);

      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-expect-error
      context.it = it;
    }
  );
}

customBDDInterface.description = 'Custom BDD';

module.exports = customBDDInterface;
