/**
 * Copyright 2017 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
 *
 *     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.
 */

const RED_COLOR = '\x1b[31m';
const GREEN_COLOR = '\x1b[32m';
const YELLOW_COLOR = '\x1b[33m';
const MAGENTA_COLOR = '\x1b[35m';
const RESET_COLOR = '\x1b[0m';

class Reporter {
  constructor(runner, options = {}) {
    const {
      projectFolder = null,
      showSlowTests = 3,
      showSkippedTests = Infinity,
      verbose = false,
      summary = true,
    } = options;
    this._runner = runner;
    this._projectFolder = projectFolder;
    this._showSlowTests = showSlowTests;
    this._showSkippedTests = showSkippedTests;
    this._verbose = verbose;
    this._summary = summary;
    this._testCounter = 0;
    runner.on('started', this._onStarted.bind(this));
    runner.on('finished', this._onFinished.bind(this));
    runner.on('teststarted', this._onTestStarted.bind(this));
    runner.on('testfinished', this._onTestFinished.bind(this));
    this._workersState = new Map();
  }

  _onStarted(runnableTests) {
    this._testCounter = 0;
    this._timestamp = Date.now();
    const allTests = this._runner.tests();
    if (allTests.length === runnableTests.length)
      console.log(`Running all ${YELLOW_COLOR}${runnableTests.length}${RESET_COLOR} tests on ${YELLOW_COLOR}${this._runner.parallel()}${RESET_COLOR} worker(s):\n`);
    else
      console.log(`Running ${YELLOW_COLOR}${runnableTests.length}${RESET_COLOR} focused tests out of total ${YELLOW_COLOR}${allTests.length}${RESET_COLOR} on ${YELLOW_COLOR}${this._runner.parallel()}${RESET_COLOR} worker(s):\n`);
  }

  _printTermination(result, message, error) {
    console.log(`${RED_COLOR}## ${result.toUpperCase()} ##${RESET_COLOR}`);
    console.log('Message:');
    console.log(`  ${RED_COLOR}${message}${RESET_COLOR}`);
    if (error && error.stack) {
      console.log('Stack:');
      console.log(error.stack.split('\n').map(line => '  ' + line).join('\n'));
    }
    console.log('WORKERS STATE');
    const workerIds = Array.from(this._workersState.keys());
    workerIds.sort((a, b) => a - b);
    for (const workerId of workerIds) {
      const {isRunning, test} = this._workersState.get(workerId);
      let description = '';
      if (isRunning)
        description = `${YELLOW_COLOR}RUNNING${RESET_COLOR}`;
      else if (test.result === 'ok')
        description = `${GREEN_COLOR}OK${RESET_COLOR}`;
      else if (test.result === 'skipped')
        description = `${YELLOW_COLOR}SKIPPED${RESET_COLOR}`;
      else if (test.result === 'failed')
        description = `${RED_COLOR}FAILED${RESET_COLOR}`;
      else if (test.result === 'crashed')
        description = `${RED_COLOR}CRASHED${RESET_COLOR}`;
      else if (test.result === 'timedout')
        description = `${RED_COLOR}TIMEDOUT${RESET_COLOR}`;
      else if (test.result === 'terminated')
        description = `${MAGENTA_COLOR}TERMINATED${RESET_COLOR}`;
      else
        description = `${RED_COLOR}<UNKNOWN>${RESET_COLOR}`;
      console.log(`  ${workerId}: [${description}] ${test.fullName} (${formatTestLocation(test)})`);
    }
    process.exitCode = 2;
  }

  _onFinished({result, terminationError, terminationMessage}) {
    this._printTestResults();
    if (terminationMessage || terminationError)
      this._printTermination(result, terminationMessage, terminationError);
    process.exitCode = result === 'ok' ? 0 : 1;
  }

  _printTestResults() {
    // 2 newlines after completing all tests.
    console.log('\n');

    const failedTests = this._runner.failedTests();
    if (this._summary && failedTests.length > 0) {
      console.log('\nFailures:');
      for (let i = 0; i < failedTests.length; ++i) {
        const test = failedTests[i];
        console.log(`${i + 1}) ${test.fullName} (${formatTestLocation(test)})`);
        if (test.result === 'timedout') {
          console.log('  Message:');
          console.log(`    ${RED_COLOR}Timeout Exceeded ${this._runner.timeout()}ms${RESET_COLOR}`);
        } else if (test.result === 'crashed') {
          console.log('  Message:');
          console.log(`    ${RED_COLOR}CRASHED${RESET_COLOR}`);
        } else {
          console.log('  Message:');
          console.log(`    ${RED_COLOR}${test.error.message || test.error}${RESET_COLOR}`);
          console.log('  Stack:');
          if (test.error.stack)
            console.log(this._beautifyStack(test.error.stack));
        }
        if (test.output) {
          console.log('  Output:');
          console.log(test.output.split('\n').map(line => '    ' + line).join('\n'));
        }
        console.log('');
      }
    }

    const skippedTests = this._runner.skippedTests();
    if (this._showSkippedTests && this._summary && skippedTests.length) {
      if (skippedTests.length > 0) {
        console.log('\nSkipped:');
        skippedTests.slice(0, this._showSkippedTests).forEach((test, index) => {
          console.log(`${index + 1}) ${test.fullName} (${formatTestLocation(test)})`);
        });
      }
      if (this._showSkippedTests < skippedTests.length) {
        console.log('');
        console.log(`... and ${YELLOW_COLOR}${skippedTests.length - this._showSkippedTests}${RESET_COLOR} more skipped tests ...`);
      }
    }

    if (this._showSlowTests) {
      const slowTests = this._runner.passedTests().sort((a, b) => {
        const aDuration = a.endTimestamp - a.startTimestamp;
        const bDuration = b.endTimestamp - b.startTimestamp;
        return bDuration - aDuration;
      }).slice(0, this._showSlowTests);
      console.log(`\nSlowest tests:`);
      for (let i = 0; i < slowTests.length; ++i) {
        const test = slowTests[i];
        const duration = test.endTimestamp - test.startTimestamp;
        console.log(`  (${i + 1}) ${YELLOW_COLOR}${duration / 1000}s${RESET_COLOR} - ${test.fullName} (${formatTestLocation(test)})`);
      }
    }

    const tests = this._runner.tests();
    const executedTests = tests.filter(test => test.result);
    const okTestsLength = executedTests.length - failedTests.length - skippedTests.length;
    let summaryText = '';
    if (failedTests.length || skippedTests.length) {
      const summary = [`ok - ${GREEN_COLOR}${okTestsLength}${RESET_COLOR}`];
      if (failedTests.length)
        summary.push(`failed - ${RED_COLOR}${failedTests.length}${RESET_COLOR}`);
      if (skippedTests.length)
        summary.push(`skipped - ${YELLOW_COLOR}${skippedTests.length}${RESET_COLOR}`);
      summaryText = `(${summary.join(', ')})`;
    }

    console.log(`\nRan ${executedTests.length} ${summaryText} of ${tests.length} test(s)`);
    const milliseconds = Date.now() - this._timestamp;
    const seconds = milliseconds / 1000;
    console.log(`Finished in ${YELLOW_COLOR}${seconds}${RESET_COLOR} seconds`);
  }

  _beautifyStack(stack) {
    if (!this._projectFolder)
      return stack;
    const lines = stack.split('\n').map(line => '    ' + line);
    // Find last stack line that include testrunner code.
    let index = 0;
    while (index < lines.length && !lines[index].includes(__dirname))
      ++index;
    while (index < lines.length && lines[index].includes(__dirname))
      ++index;
    if (index >= lines.length)
      return stack;
    const line = lines[index];
    const fromIndex = line.lastIndexOf(this._projectFolder) + this._projectFolder.length;
    let toIndex = line.lastIndexOf(')');
    if (toIndex === -1)
      toIndex = line.length;
    lines[index] = line.substring(0, fromIndex) + YELLOW_COLOR + line.substring(fromIndex, toIndex) + RESET_COLOR + line.substring(toIndex);
    return lines.join('\n');
  }

  _onTestStarted(test, workerId) {
    this._workersState.set(workerId, {test, isRunning: true});
  }

  _onTestFinished(test, workerId) {
    this._workersState.set(workerId, {test, isRunning: false});
    if (this._verbose) {
      ++this._testCounter;
      if (test.result === 'ok') {
        console.log(`${this._testCounter}) ${GREEN_COLOR}[ OK ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
      } else if (test.result === 'terminated') {
        console.log(`${this._testCounter}) ${MAGENTA_COLOR}[ TERMINATED ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
      } else if (test.result === 'crashed') {
        console.log(`${this._testCounter}) ${RED_COLOR}[ CRASHED ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
      } else if (test.result === 'skipped') {
        console.log(`${this._testCounter}) ${YELLOW_COLOR}[SKIP]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
      } else if (test.result === 'failed') {
        console.log(`${this._testCounter}) ${RED_COLOR}[FAIL]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
        console.log('  Message:');
        console.log(`    ${RED_COLOR}${test.error.message || test.error}${RESET_COLOR}`);
        console.log('  Stack:');
        if (test.error.stack)
          console.log(this._beautifyStack(test.error.stack));
        if (test.output) {
          console.log('  Output:');
          console.log(test.output.split('\n').map(line => '    ' + line).join('\n'));
        }
      } else if (test.result === 'timedout') {
        console.log(`${this._testCounter}) ${RED_COLOR}[TIME]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
        console.log('  Message:');
        console.log(`    ${RED_COLOR}Timeout Exceeded ${this._runner.timeout()}ms${RESET_COLOR}`);
      }
    } else {
      if (test.result === 'ok')
        process.stdout.write(`${GREEN_COLOR}.${RESET_COLOR}`);
      else if (test.result === 'skipped')
        process.stdout.write(`${YELLOW_COLOR}*${RESET_COLOR}`);
      else if (test.result === 'failed')
        process.stdout.write(`${RED_COLOR}F${RESET_COLOR}`);
      else if (test.result === 'crashed')
        process.stdout.write(`${RED_COLOR}C${RESET_COLOR}`);
      else if (test.result === 'terminated')
        process.stdout.write(`${MAGENTA_COLOR}.${RESET_COLOR}`);
      else if (test.result === 'timedout')
        process.stdout.write(`${RED_COLOR}T${RESET_COLOR}`);
    }
  }
}

function formatTestLocation(test) {
  const location = test.location;
  if (!location)
    return '';
  return `${YELLOW_COLOR}${location.fileName}:${location.lineNumber}:${location.columnNumber}${RESET_COLOR}`;
}

module.exports = Reporter;
