/* jshint strict: false */
// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2014-2024 ArangoDB GmbH, Cologne, Germany
// / Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
// /
// / Licensed under the Business Source License 1.1 (the "License");
// / you may not use this file except in compliance with the License.
// / You may obtain a copy of the License at
// /
// /     https://github.com/arangodb/arangodb/blob/devel/LICENSE
// /
// / 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.
// /
// / Copyright holder is ArangoDB GmbH, Cologne, Germany
// /
// //////////////////////////////////////////////////////////////////////////////

var internal = require('internal');
var print = internal.print;
var fs = require('fs');
var console = require('console');
var { minimatch } = require('minimatch');

var TOTAL = 0;
var PASSED = 0;
var FAILED = 0;
var DURATION = 0;
var RESULTS = {};
var COMPLETE = {};

var SETUPS = 0;
var TEARDOWNS = 0;

var TOTALSETUPS = 0;
var TOTALTEARDOWNS = 0;

var jsUnity = require('./jsunity/jsunity').jsUnity;
var STARTTEST = 0.0;
var ENDTEST = 0.0;
var STARTSUITE = 0.0;
var ENDTEARDOWN = 0.0;
var testFilter = "undefined";
var suiteName = "undefined";
var currentSuiteName = "undefined";
var testCount = 0;
var startMessage = "";

function setTestFilter(filter) {
  testFilter = filter;
}

jsUnity.results.begin = function (total, suiteName) {
  if (testCount > 0)
  {
    print();
    testCount = 0;
  }
  currentSuiteName = suiteName;
  startMessage = " [------------] " + total + " tests from " + (suiteName || 'unnamed test suite');
  RESULTS = {};

  STARTTEST = jsUnity.env.getDate();
};

jsUnity.results.pass = function (index, testName) {
  var newtime = jsUnity.env.getDate();

  RESULTS[testName].status = true;
  RESULTS[testName].duration = (ENDTEST - STARTTEST);
  RESULTS[testName].finishedAt = new Date().toISOString();

  print(newtime.toISOString() + internal.COLORS.COLOR_GREEN +  ' [     PASSED ] ' +
       testName + internal.COLORS.COLOR_RESET +
       ' (setUp: ' + RESULTS[testName].setUpDuration + 'ms,' +
       ' test: ' + RESULTS[testName].duration + 'ms,' +
       ' tearDown: ' + RESULTS[testName].tearDownDuration + 'ms)');

  STARTTEST = newtime;

  ++testCount;
};

jsUnity.results.fail = function (index, testName, message) {
  var newtime = jsUnity.env.getDate();

  ++testCount;
  let now = newtime.toISOString();
  if (RESULTS[testName] === undefined) {
    if (testCount === 1) {
      print(now + internal.COLORS.COLOR_RED + " [   FAILED   ] " + currentSuiteName +
           internal.COLORS.COLOR_RESET + " (setUpAll: " + (jsUnity.env.getDate() - STARTTEST) + "ms)");

      ENDTEST = newtime;
    }
    print(internal.COLORS.COLOR_RED + message + internal.COLORS.COLOR_RESET);
    if (RESULTS.hasOwnProperty('message')) {
      RESULTS['message'] += `\n${now}\n${currentSuiteName} - failed at: ${message}`;
    } else {
      RESULTS['message'] = `${now}\n${currentSuiteName} - failed at: ${message}`;
    }
    return;
  }

  RESULTS[testName].status = false;
  RESULTS[testName].message = message;
  RESULTS[testName].finishedAt = new Date().toISOString();

  RESULTS[testName].duration = (ENDTEST - STARTTEST);
  if (RESULTS[testName].duration < 0) {
    // if ENDTEST not set...
    RESULTS[testName].duration = 0;
  }

  if (RESULTS[testName].setUpDuration === undefined) {
    RESULTS[testName].setUpDuration = newtime - SETUPS;
    RESULTS[testName].duration = 0;
    RESULTS[testName].tearDownDuration = 0;
  }
  
  if (RESULTS[testName].tearDownDuration === undefined) {
    RESULTS[testName].tearDownDuration = 0;
  }

  print(newtime.toISOString() + internal.COLORS.COLOR_RED + " [   FAILED   ] " +
       testName + internal.COLORS.COLOR_RESET +
       ' (setUp: ' + RESULTS[testName].setUpDuration + 'ms,' +
       ' test: ' + RESULTS[testName].duration + 'ms,' +
       ' tearDown: ' +  RESULTS[testName].tearDownDuration + 'ms)\n' +
       internal.COLORS.COLOR_RED + message + internal.COLORS.COLOR_RESET);

  STARTTEST = newtime;
};

jsUnity.results.end = function (passed, failed, duration) {
  print(jsUnity.env.getDate().toISOString() +
       ((failed > 0) ? internal.COLORS.COLOR_RED : internal.COLORS.COLOR_GREEN) + " [------------] " +
       (passed + failed) + " tests from " + currentSuiteName + " ran" + internal.COLORS.COLOR_RESET +
       " (tearDownAll: " + (jsUnity.env.getDate() - ENDTEST) + "ms)");
  print(jsUnity.env.getDate().toISOString() + internal.COLORS.COLOR_GREEN +
       " [   PASSED   ] " + passed + " tests." + internal.COLORS.COLOR_RESET);
  if(failed > 0)
  {
    print(jsUnity.env.getDate().toISOString() + internal.COLORS.COLOR_RED +
        " [   FAILED   ] " + failed + ' tests.' + internal.COLORS.COLOR_RESET);
  }
};

jsUnity.results.beginSetUpAll = function(index) {
  SETUPS = jsUnity.env.getDate();
};

jsUnity.results.endSetUpAll = function(index) {
  RESULTS.setUpAllDuration = jsUnity.env.getDate() - SETUPS;
  TOTALSETUPS += RESULTS.setUpAllDuration;
};

jsUnity.results.beginSetUp = function(index, testName) {
  if (testCount === 0)
  {
    print(STARTTEST.toISOString() + internal.COLORS.COLOR_GREEN +
         startMessage + internal.COLORS.COLOR_RESET + ' (setUpAll: ' +
         (jsUnity.env.getDate() - STARTTEST) + 'ms)' + internal.COLORS.COLOR_RESET);
  }
  RESULTS[testName] = {};
  RESULTS[testName].startedAt = new Date().toISOString();
  SETUPS = jsUnity.env.getDate();
  print(jsUnity.env.getDate().toISOString() + internal.COLORS.COLOR_GREEN + ' [ RUN        ] ' + testName + internal.COLORS.COLOR_RESET);
};

jsUnity.results.endSetUp = function(index, testName) {
  RESULTS[testName].setUpDuration = jsUnity.env.getDate() - SETUPS;
  TOTALSETUPS += RESULTS[testName].setUpDuration;

  STARTTEST = jsUnity.env.getDate();
};

jsUnity.results.beginTeardown = function(index, testName) {
  TEARDOWNS = jsUnity.env.getDate();

  ENDTEST = jsUnity.env.getDate();
};

jsUnity.results.endTeardown = function(index, testName) {
  RESULTS[testName].tearDownDuration = jsUnity.env.getDate() - TEARDOWNS;
  TOTALTEARDOWNS += RESULTS[testName].tearDownDuration;
};

jsUnity.results.beginTeardownAll = function(index) {
  TEARDOWNS = jsUnity.env.getDate();
};

jsUnity.results.endTeardownAll = function(index) {
  RESULTS.teardownAllDuration = jsUnity.env.getDate() - TEARDOWNS;
  TOTALTEARDOWNS += RESULTS.teardownAllDuration;
};

function matchesTestFilter(suiteName, key) {
  if (testFilter === "undefined" || testFilter === undefined || testFilter === null) {
    return true;
  }
  const matchFilter = (suite, testCase, filter) => {
    const [suiteFilter, testCaseFilter] = filter.includes('::') ? filter.split('::') : [suite, filter];
    return minimatch(suite, suiteFilter) && minimatch(testCase, testCaseFilter);
  };
  if (typeof testFilter === 'string') {
    return matchFilter(suiteName, key, testFilter);
  }
  if (Array.isArray(testFilter)) {
    return testFilter.some(filter => matchFilter(suiteName, key, filter));
  }
  return false;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief runs a test with context
// //////////////////////////////////////////////////////////////////////////////

function Run (testsuite) {
  var suite = jsUnity.compile(testsuite);
  var definition = testsuite();

  var tests = [];
  var setUp;
  var tearDown;
  var setUpAll;
  var tearDownAll;

  if (definition.hasOwnProperty('setUp')) {
    setUp = definition.setUp;
  }

  if (definition.hasOwnProperty('tearDown')) {
    tearDown = definition.tearDown;
  }
  
  if (definition.hasOwnProperty('setUpAll')) {
    setUpAll = definition.setUpAll;
  }

  if (definition.hasOwnProperty('tearDownAll')) {
    tearDownAll = definition.tearDownAll;
  }

  var scope = {};
  scope.setUp = setUp;
  scope.tearDown = tearDown;
  scope.setUpAll = setUpAll;
  scope.tearDownAll = tearDownAll;
  let nonMatchedTests = [];

  for (var key in definition) {
    if (key.indexOf('test') === 0) {
      if (!matchesTestFilter(suite.suiteName, key)) {
        // print(`test "${key}" doesn't match "${testFilter}", skipping`);
        nonMatchedTests.push(key);
        continue;
      }

      var test = { name: key, fn: definition[key]};

      tests.push(test);
    } else if (key !== 'tearDown' && key !== 'setUp' && key !== 'tearDownAll' && key !== 'setUpAll' && key !== 'internal') {
      throw new Error(`function name ${key} not allowed here.`);
    }
  }
  if (tests.length === 0) {
    let res = {
      suiteName: suite.suiteName,
      duration: 0,
      setUpDuration: 0,
      tearDownDuration: 0,
      passed: 0,
      status: true,
      failed: 0,
      total: 0
    };
    COMPLETE[suite.suiteName] = res;
    return res;
  }

  suite = new jsUnity.TestSuite(suite.suiteName, scope);

  suite.tests = tests;
  suite.setUp = setUp;
  suite.tearDown = tearDown;
  suite.setUpAll = setUpAll;
  suite.tearDownAll = tearDownAll;

  var result = jsUnity.run(suite);
  TOTAL += result.total;
  PASSED += result.passed;
  FAILED += result.failed;
  DURATION += result.duration;

  let duplicates = [];
  for (var attrname in RESULTS) {
    if (typeof(RESULTS[attrname]) === 'number') {
      if (!COMPLETE.hasOwnProperty(attrname)) {
        COMPLETE[attrname] = { };
      }
      COMPLETE[attrname][suite.suiteName] = RESULTS[attrname];
    } else if (RESULTS.hasOwnProperty(attrname)) {
      if (COMPLETE.hasOwnProperty(attrname)) {
        if (attrname === 'message') {
          COMPLETE[attrname] += "\n\n" + RESULTS[attrname];
          continue;
        }
        print("Duplicate testsuite '" + attrname + "' - already have: " + JSON.stringify(COMPLETE[attrname]) + "");
        duplicates.push(attrname);
      }
      COMPLETE[attrname] = RESULTS[attrname];
    }
  }
  if (duplicates.length !== 0) {
    throw("Duplicate testsuite '" + duplicates + "'");
  }
  return result;
}

// //////////////////////////////////////////////////////////////////////////////
// / @brief done with all tests
// //////////////////////////////////////////////////////////////////////////////

function Done (suiteName) {
  let newtime = jsUnity.env.getDate();

  var ok = FAILED === 0;

  print(newtime.toISOString() + (ok ? internal.COLORS.COLOR_GREEN : internal.COLORS.COLOR_RED) +
       " [============] " + "Ran: " + TOTAL + " tests (" + PASSED + " passed, " + FAILED + " failed)" +
       internal.COLORS.COLOR_RESET+ " (" + DURATION + "ms total)");
  print();

  COMPLETE.duration = DURATION;
  COMPLETE.status = ok;
  COMPLETE.failed = FAILED;
  COMPLETE.total = TOTAL;
  COMPLETE.suiteName = suiteName;
  COMPLETE.totalSetUp = TOTALSETUPS;
  COMPLETE.totalTearDown = TOTALTEARDOWNS;

  TOTAL = 0;
  PASSED = 0;
  FAILED = 0;
  DURATION = 0;

  var ret = COMPLETE;
  COMPLETE = {};
  return ret;
}

// //////////////////////////////////////////////////////////////////////////////
// / @brief done with all tests
// //////////////////////////////////////////////////////////////////////////////

function WriteDone (suiteName) {
  var ret = Done(suiteName);
  let outPath = fs.join(require("internal").options()['temp.path'], 'testresult.json');
  fs.write(outPath, JSON.stringify(ret));
  return ret;
}

// //////////////////////////////////////////////////////////////////////////////
// / @brief runs a JSUnity test file
// //////////////////////////////////////////////////////////////////////////////

function RunTest (path, outputReply, filter) {
  // re-reset our globlas, on module loading may be cached.
  TOTAL = 0;
  PASSED = 0;
  FAILED = 0;
  DURATION = 0;
  RESULTS = {};
  COMPLETE = {};
  
  SETUPS = 0;
  TEARDOWNS = 0;
  
  TOTALSETUPS = 0;
  TOTALTEARDOWNS = 0;
  STARTTEST = 0.0;
  ENDTEST = 0.0;
  STARTSUITE = 0.0;
  ENDTEARDOWN = 0.0;
  STARTTEST = 0.0;
  ENDTEST = 0.0;
  STARTSUITE = 0.0;
  ENDTEARDOWN = 0.0;

  var content;
  var f;

  if (path.includes("test-module-")) {
    content = `return require(${JSON.stringify(path)}).run({ runSetup, getOptions });`;
  } else {
    content = fs.read(path);
  }

  // NOTE: this is intentionally a single long line to ensure that any line information
  // refers to the correct line in the original source file.
  content = `(function(){ require('jsunity').jsUnity.attachAssertions(); return (function() { require('jsunity').setTestFilter(${JSON.stringify(filter)}); const runSetup = false; const getOptions = false; ${content} }()); });`;
  f = internal.executeScript(content, undefined, path);

  if (f === undefined) {
    throw 'cannot create context function';
  }

  var rc = f(path);
  if (outputReply === true) {
    return rc;
  } else {
    return rc.status;
  }
}

exports.setTestFilter = setTestFilter;
exports.jsUnity = jsUnity;
exports.run = Run;
exports.done = Done;
exports.writeDone = WriteDone;
exports.runTest = RunTest;
