import { Context, Test } from '@jest/reporters';
import { AggregatedResult, TestResult } from '@jest/test-result';
import { Config } from '@jest/types';
import { bold, green, red } from 'chalk';
import { table } from 'table';

import {
  DEFAULT_MAX_SLOW_TESTS,
  DEFAULT_SHOW_SLOWEST,
  DEFAULT_SHOW_WARNING_SUMMARY,
  DEFAULT_TEST_DURATION,
  DEFAULT_WARN_ON_SLOWER_THAN,
  DEFAULT_WARNINGS_CONFIG,
  TABLE_HEADER_WITH_WARNINGS_DATA,
  TABLE_HEADER_WITHOUT_WARNINGS_DATA,
} from './constants';
import { recursiveLogger } from './recursiveLogger';
import {
  CustomLogEntry,
  IJestReporterConfig,
  LogsObj,
  SlowTest,
  Warning,
} from './types';
import {
  addFilesSetToWarningConfig,
  getFileName,
  log,
  omitWarningsData,
  prepareGreetingMessage,
  prepareTableRow,
  prepareWarnings,
} from './utils';

class CustomReporter {
  private showWarningSummary: boolean;

  private warningsConfig: Array<Warning>;

  private maxSlowTests: number;

  private globalConfig: Config.InitialOptions;

  private logs: LogsObj;

  private showSlowest: boolean;

  private slowTests: Array<SlowTest>;

  private finalTable: Array<Array<string | number>>;

  private warnOnSlowerThan: number;

  constructor(
    public globalConfiguration: Config.InitialOptions,
    private options: IJestReporterConfig,
  ) {
    this.globalConfig = globalConfiguration;
    this.showWarningSummary =
      options.showWarningSummary ?? DEFAULT_SHOW_WARNING_SUMMARY;
    this.warningsConfig =
      options.warningsConfig?.map(addFilesSetToWarningConfig) ??
      DEFAULT_WARNINGS_CONFIG;
    this.maxSlowTests = options.maxSlowTests ?? DEFAULT_MAX_SLOW_TESTS;
    this.logs = {};
    this.showSlowest = options.showSlowest ?? DEFAULT_SHOW_SLOWEST;
    this.slowTests = [];
    this.warnOnSlowerThan =
      options.warnOnSlowerThan ?? DEFAULT_WARN_ON_SLOWER_THAN;
    this.finalTable = omitWarningsData(globalConfiguration)
      ? [TABLE_HEADER_WITHOUT_WARNINGS_DATA]
      : [TABLE_HEADER_WITH_WARNINGS_DATA];
  }

  onRunStart({ numTotalTestSuites }: AggregatedResult): void {
    const greetingMessage = prepareGreetingMessage(numTotalTestSuites);

    console.log(greetingMessage);
  }

  onTestResult(_: Test, testResult: TestResult): void {
    const fileName = getFileName(testResult.testFilePath);
    const warningsPerTest: Record<string, number> = {};
    const tableRow = prepareTableRow(
      testResult,
      this.warnOnSlowerThan,
      this.globalConfig,
    );

    this.logs[fileName] = {
      filePath: testResult.testFilePath,
      warnings: [],
    };

    this.finalTable.push(tableRow);

    if (testResult.console) {
      testResult.console.forEach((value) => {
        let isOther = true;

        this.warningsConfig.forEach(({ regex, name }) => {
          if (regex.test(value.message)) {
            warningsPerTest[name] = warningsPerTest[name] + 1 || 1;
            isOther = false;
          }
        });

        if (isOther) {
          warningsPerTest.Other = warningsPerTest.Other + 1 || 1;
        }

        this.logs[fileName].warnings.push(value);
      });
    }

    if (this.showSlowest) {
      this.collectSlowTests(testResult);
    }

    if (this.showWarningSummary && !this.globalConfig.verbose) {
      const warningsInfo = prepareWarnings(warningsPerTest);

      this.finalTable.push(...warningsInfo);
    }
  }

  onRunComplete(
    _: Set<Context>,
    { numFailedTests, numPassedTests, testResults }: AggregatedResult,
  ): void {
    testResults.forEach(
      ({ testFilePath, testResults: innerTestResults, failureMessage }) => {
        recursiveLogger(testFilePath, innerTestResults, 0, 0);

        if (failureMessage) {
          console.log(failureMessage);
        }
      },
    );

    if (numPassedTests !== 0) {
      console.log(green(`${numPassedTests} passed`));
    }

    if (numFailedTests !== 0) {
      console.log(bold(red(`${numFailedTests} failed`)));
    }

    if (this.globalConfig.verbose || this.globalConfig.watch) {
      Object.entries(this.logs).forEach(([filename, data]) => {
        const { warnings } = data;

        if (warnings.length) {
          console.log(bold(filename));
        }

        (warnings as CustomLogEntry[]).forEach(log);
      });
    }

    const shouldShowTable = this.finalTable.length > 1;

    if (shouldShowTable) {
      console.log(table(this.finalTable));
    }

    if (
      this.showSlowest &&
      !this.globalConfig.watch &&
      !this.globalConfig.watchAll
    ) {
      this.consoleSlowTests();
    }
  }

  // Idea taken from [jest-slow-test-reporter](https://github.com/jodonnell/jest-slow-test-reporter)
  private collectSlowTests(result: TestResult): void {
    result.testResults.forEach((test) => {
      this.slowTests.push({
        duration: test.duration ?? DEFAULT_TEST_DURATION,
        fullName: test.fullName,
        filePath: result.testFilePath,
      });
    });
  }

  private consoleSlowTests(): void {
    this.slowTests.sort(function sort(a, b) {
      return b.duration - a.duration;
    });

    const rootPathRegex = new RegExp(`^${process.cwd()}`);
    const slowestTests = this.slowTests.slice(0, this.maxSlowTests);
    const slowTestTime = this.calcTestTime(slowestTests);
    const allTestTime = this.calcTestTime(this.slowTests);
    const percentTime = (slowTestTime / allTestTime) * 100;

    console.log(
      `Top ${slowestTests.length} slowest examples (${
        slowTestTime / 1000
      } seconds, ${percentTime.toFixed(1)}% of total time):`,
    );

    slowestTests.forEach(({ duration, fullName, filePath }) => {
      const path = filePath.replace(rootPathRegex, '.');

      console.log(`  ${fullName}`);
      console.log(`    ${duration / 1000} seconds ${path}`);
    });
  }

  private calcTestTime(slowestTests: Array<SlowTest>) {
    return slowestTests.reduce((acc, curr) => acc + curr.duration, 0);
  }
}

export default CustomReporter;
