/**
 * Copyright Microsoft Corporation. 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 path = require('path');
const Mocha = require('mocha');
const { fixturesUI } = require('./fixturesUI');
const { EventEmitter } = require('events');

global.expect = require('expect');
global.testOptions = require('./testOptions');
const GoldenUtils = require('./GoldenUtils');

class NullReporter {}

class TestRunner extends EventEmitter {
  constructor(file, ordinals, options) {
    super();
    this.mocha = new Mocha({
      forbidOnly: options.forbidOnly,
      reporter: NullReporter,
      timeout: options.timeout,
      ui: fixturesUI.bind(null, this),
    });
    if (options.grep)
      this.mocha.grep(options.grep);
    this._currentOrdinal = -1;
    this._failedWithError = false;
    this._ordinals = new Set(ordinals);
    this._remaining = new Set(ordinals);
    this._trialRun = options.trialRun;
    this._passes = 0;
    this._failures = 0;
    this._pending = 0;
    this._relativeTestFile = path.relative(options.testDir, file);
    this.mocha.addFile(file);
    this.mocha.suite.filterOnly();
    this.mocha.loadFiles();
    this.suite = this.mocha.suite;
  }

  async run() {
    let callback;
    const result = new Promise(f => callback = f);
    const runner = this.mocha.run(callback);

    const constants = Mocha.Runner.constants;
    runner.on(constants.EVENT_TEST_BEGIN, test => {
      relativeTestFile = this._relativeTestFile;
      if (this._failedWithError)
        return;
      const ordinal = ++this._currentOrdinal;
      if (this._ordinals.size && !this._ordinals.has(ordinal))
        return;
      this._remaining.delete(ordinal);
      this.emit('test', { test: serializeTest(test, ordinal) });
    });

    runner.on(constants.EVENT_TEST_PENDING, test => {
      if (this._failedWithError)
        return;
      const ordinal = ++this._currentOrdinal;
      if (this._ordinals.size && !this._ordinals.has(ordinal))
        return;
      this._remaining.delete(ordinal);
      ++this._pending;
      this.emit('pending', { test: serializeTest(test, ordinal) });
    });

    runner.on(constants.EVENT_TEST_PASS, test => {
      if (this._failedWithError)
        return;

      const ordinal = this._currentOrdinal;
      if (this._ordinals.size && !this._ordinals.has(ordinal))
        return;
      ++this._passes;
      this.emit('pass', { test: serializeTest(test, ordinal) });
    });

    runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
      if (this._failedWithError)
        return;
      ++this._failures;
      this._failedWithError = error;
      this.emit('fail', {
        test: serializeTest(test, this._currentOrdinal),
        error: serializeError(error),
      });
    });

    runner.once(constants.EVENT_RUN_END, async () => {
      this.emit('done', {
        stats: this._serializeStats(runner.stats),
        error: this._failedWithError,
        remaining: [...this._remaining],
        total: runner.stats.tests
      });
    });
    await result;
  }

  shouldRunTest(hook) {
    if (this._trialRun || this._failedWithError)
      return false;
    if (hook) {
      // Hook starts before we bump the test ordinal.
      if (!this._ordinals.has(this._currentOrdinal + 1))
        return false;
    } else {
      if (!this._ordinals.has(this._currentOrdinal))
        return false;
    }
    return true;
  }

  grepTotal() {
    let total = 0;
    this.suite.eachTest(test => {
      if (this.mocha.options.grep.test(test.fullTitle()))
        total++;
    });
    return total;
  }

  _serializeStats(stats) {
    return {
      passes: this._passes,
      failures: this._failures,
      pending: this._pending,
      duration: stats.duration || 0,
    }
  }  
}

function createTestSuite() {
  return new Mocha.Suite('', new Mocha.Context(), true);
}

function serializeTest(test, origin) {
  return {
    id: `${test.file}::${origin}`,
    duration: test.duration,
  };
}

function trimCycles(obj) {
  const cache = new Set();
  return JSON.parse(
    JSON.stringify(obj, function(key, value) {
      if (typeof value === 'object' && value !== null) {
        if (cache.has(value))
          return '' + value;
        cache.add(value);
      }
      return value;
    })
  );
}

function serializeError(error) {
  if (error instanceof Error) {
    return {
      message: error.message,
      stack: error.stack
    }
  }
  return trimCycles(error);
}

let relativeTestFile;

function initializeImageMatcher(options) {
  function toMatchImage(received, name, config) {
    const { pass, message } = GoldenUtils.compare(received, name, { ...options, relativeTestFile, config });
    return { pass, message: () => message };
  };
  global.expect.extend({ toMatchImage });
}

module.exports = { TestRunner, createTestSuite, initializeImageMatcher };
