// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

base.requireStylesheet('base.unittest');
base.require('base.settings');
base.require('base.unittest.test_error');
base.require('base.unittest.assertions');

base.exportTo('base.unittest', function() {
  var TestResults = {
    FAILED: 0,
    PASSED: 1,
    PENDING: 2
  };

  var showCondensed_ = false;
  function showCondensed(val) {
    showCondensed_ = val;
  }

  function logWarningMessage(message) {
    var messagesEl = document.querySelector('#messages');
    messagesEl.setAttribute('hasMessages', true);

    var li = document.createElement('li');
    li.innerText = message;

    var list = document.querySelector('#message-list');
    list.appendChild(li);
  }

  function TestRunner(suitePaths, tests) {
    this.suitePaths_ = suitePaths || [];
    this.suites_ = [];
    this.suiteNames_ = {};
    this.tests_ = tests || [];
    this.moduleCount_ = 0;

    this.stats_ = {
      tests: 0,
      failures: 0,
      exceptions: [],
      duration: 0.0
    };
  }

  TestRunner.prototype = {
    __proto__: Object.prototype,

    loadSuites: function() {
      this.loadSuiteFiles();
    },

    run: function() {
      this.clear_(document.querySelector('#test-results'));
      this.clear_(document.querySelector('#exception-list'));
      this.clear_(document.querySelector('#message-list'));

      this.updateStats_();
      this.runSuites_();
    },

    addSuite: function(suite) {
      if (this.suiteNames_[suite.name] === true)
        logWarningMessage('Duplicate test suite name detected: ' + suite.name);

      this.suites_.push(suite);
      this.suiteNames_[suite.name] = true;

      // This assumes one test suite per file.
      if (this.suites_.length === this.suitePaths_.length)
        this.run();
    },

    loadSuiteFiles: function() {
      var modules = [];
      this.suitePaths_.forEach(function(path) {
        var moduleName = path.slice(5, path.length - 3);
        moduleName = moduleName.replace(/\//g, '.');
        modules.push(moduleName);
      });

      base.require(modules);
    },

    clear_: function(el) {
      while (el.firstChild)
        el.removeChild(el.firstChild);
    },

    runSuites_: function(opt_idx) {
      var idx = opt_idx || 0;

      var suiteCount = this.suites_.length;
      if (idx >= suiteCount) {
        var harness = document.querySelector('#test-results');
        harness.appendChild(document.createElement('br'));
        harness.appendChild(document.createTextNode('Test Run Complete'));
        return;
      }

      var suite = this.suites_[idx];

      suite.showLongResults = (suiteCount === 1);
      suite.displayInfo();
      suite.runTests(this.tests_);

      this.stats_.duration += suite.duration;
      this.stats_.tests += suite.testCount;
      this.stats_.failures += suite.failureCount;

      this.updateStats_();

      // Give the view time to update.
      window.setTimeout(function() {
        this.runSuites_(idx + 1);
      }.bind(this), 1);
    },

    onAnimationFrameError: function(e, opt_stack) {
      if (e.message)
        console.error(e.message, e.stack);
      else
        console.error(e);

      var exception = {e: e, stack: opt_stack};
      this.stats_.exceptions.push(exception);
      this.appendException(exception);
      this.updateStats_();
    },

    updateStats_: function() {
      var statEl = document.querySelector('#stats');
      statEl.innerHTML =
          this.suites_.length + ' suites, ' +
          '<span class="passed">' + this.stats_.tests + '</span> tests, ' +
          '<span class="failed">' + this.stats_.failures +
          '</span> failures, ' +
          '<span class="exception">' + this.stats_.exceptions.length +
          '</span> exceptions,' +
          ' in ' + this.stats_.duration + 'ms.';
    },

    appendException: function(exc) {
      var exceptionsEl = document.querySelector('#exceptions');
      exceptionsEl.setAttribute('hasExceptions', this.stats_.exceptions.length);

      var excEl = document.createElement('li');
      excEl.innerHTML = exc.e + '<pre>' + exc.stack + '</pre>';

      var exceptionsEl = document.querySelector('#exception-list');
      exceptionsEl.appendChild(excEl);
    }
  };

  function TestSuite(name, suite) {
    this.name_ = name;
    this.tests_ = [];
    this.testNames_ = {};
    this.failures_ = [];
    this.results_ = TestResults.PENDING;
    this.showLongResults = false;
    this.duration_ = 0.0;
    this.resultsEl_ = undefined;

    global.setup = function(fn) { this.setupFn_ = fn; }.bind(this);
    global.teardown = function(fn) { this.teardownFn_ = fn; }.bind(this);

    global.test = function(name, test) {
      if (this.testNames_[name] === true)
        logWarningMessage('Duplicate test name detected: ' + name);

      this.tests_.push(new Test(name, test));
      this.testNames_[name] = true;
    }.bind(this);

    suite.call();

    global.setup = undefined;
    global.teardown = undefined;
    global.test = undefined;
  }

  TestSuite.prototype = {
    __proto__: Object.prototype,

    get name() {
      return this.name_;
    },

    get results() {
      return this.results_;
    },

    get testCount() {
      return this.tests_.length;
    },

    get failureCount() {
      return this.failures.length;
    },

    get failures() {
      return this.failures_;
    },

    get duration() {
      return this.duration_;
    },

    displayInfo: function() {
      this.resultsEl_ = document.createElement('div');
      this.resultsEl_.className = 'test-result';

      var resultsPanel = document.querySelector('#test-results');
      resultsPanel.appendChild(this.resultsEl_);

      if (this.showLongResults) {
        this.resultsEl_.innerText = this.name;
      } else {
        var link = '/src/tests.html?suite=';
        link += this.name.replace(/\./g, '/');

        var suiteInfo = document.createElement('a');
        suiteInfo.href = link;
        suiteInfo.innerText = this.name;
        this.resultsEl_.appendChild(suiteInfo);
      }

      var statusEl = document.createElement('span');
      statusEl.classList.add('results');
      statusEl.classList.add('pending');
      statusEl.innerText = 'pending';
      this.resultsEl_.appendChild(statusEl);
    },

    runTests: function(testsToRun) {
      this.testsToRun_ = testsToRun;

      var start = new Date().getTime();
      this.results_ = TestResults.PENDING;
      this.tests_.forEach(function(test) {
        if (this.testsToRun_.length !== 0 &&
            this.testsToRun_.indexOf(test.name) === -1)
          return;

        // Clear settings storage before each test.
        global.sessionStorage.clear();
        base.Settings.setAlternativeStorageInstance(global.sessionStorage);
        base.onAnimationFrameError =
            testRunner.onAnimationFrameError.bind(testRunner);

        if (this.setupFn_ !== undefined)
          this.setupFn_.bind(test).call();

        var testWorkAreaEl_ = document.createElement('div');

        this.resultsEl_.appendChild(testWorkAreaEl_);
        test.run(testWorkAreaEl_);
        this.resultsEl_.removeChild(testWorkAreaEl_);

        if (this.teardownFn_ !== undefined)
          this.teardownFn_.bind(test).call();

        if (test.result === TestResults.FAILED) {
          this.failures_.push({
            error: test.failure,
            test: test.name
          });
          this.results_ = TestResults.FAILED;
        }
      }, this);
      if (this.results_ === TestResults.PENDING)
        this.results_ = TestResults.PASSED;

      this.duration_ = new Date().getTime() - start;
      this.outputResults();
    },

    outputResults: function() {
      if ((this.results === TestResults.PASSED) && showCondensed_ &&
          !this.showLongResults) {
        var parent = this.resultsEl_.parentNode;
        parent.removeChild(this.resultsEl_);
        this.resultsEl_ = undefined;

        parent.appendChild(document.createTextNode('.'));
        return;
      }

      var status = this.resultsEl_.querySelector('.results');
      status.classList.remove('pending');
      if (this.results === TestResults.PASSED) {
        status.innerText = 'passed';
        status.classList.add('passed');
      } else {
        status.innerText = 'FAILED';
        status.classList.add('failed');
      }

      status.innerText += ' (' + this.duration_ + 'ms)';

      var child = this.showLongResults ? this.outputLongResults() :
                                         this.outputShortResults();
      if (child !== undefined)
        this.resultsEl_.appendChild(child);
    },

    outputShortResults: function() {
      if (this.results === TestResults.PASSED)
        return undefined;

      var parent = document.createElement('div');

      var failureList = this.failures;
      for (var i = 0; i < failureList.length; ++i) {
        var fail = failureList[i];

        var preEl = document.createElement('pre');
        preEl.className = 'failure';
        preEl.innerText = 'Test: ' + fail.test + '\n' + fail.error.stack;
        parent.appendChild(preEl);
      }

      return parent;
    },

    outputLongResults: function() {
      var parent = document.createElement('div');

      this.tests_.forEach(function(test) {
        if (this.testsToRun_.length !== 0 &&
            this.testsToRun_.indexOf(test.name) === -1)
          return;

        var testEl = document.createElement('div');
        testEl.className = 'individual-result';

        var link = '/src/tests.html?suite=';
        link += this.name.replace(/\./g, '/');
        link += '&test=' + test.name.replace(/\./g, '/');

        var suiteInfo = document.createElement('a');
        suiteInfo.href = link;
        suiteInfo.innerText = test.name;
        testEl.appendChild(suiteInfo);

        parent.appendChild(testEl);

        var resultEl = document.createElement('span');
        resultEl.classList.add('results');
        testEl.appendChild(resultEl);
        if (test.result === TestResults.PASSED) {
          resultEl.classList.add('passed');
          resultEl.innerText = 'passed';
        } else {
          resultEl.classList.add('failed');
          resultEl.innerText = 'FAILED';

          var preEl = document.createElement('pre');
          preEl.className = 'failure';
          preEl.innerText = test.failure.stack;
          testEl.appendChild(preEl);
        }

        if (test.hasAppendedContent)
          testEl.appendChild(test.appendedContent);

      }.bind(this));

      return parent;
    },

    toString: function() {
      return this.name_;
    }
  };

  function Test(name, test) {
    this.name_ = name;
    this.test_ = test;
    this.result_ = TestResults.FAILED;
    this.failure_ = undefined;

    this.appendedContent_ = undefined;
  }

  Test.prototype = {
    __proto__: Object.prototype,

    run: function(workArea) {
      this.testWorkArea_ = workArea;
      try {
        this.test_.bind(this).call();
        this.result_ = TestResults.PASSED;
      } catch (e) {
        console.error(e, e.stack);
        this.failure_ = e;
      }
    },

    get failure() {
      return this.failure_;
    },

    get name() {
      return this.name_;
    },

    get result() {
      return this.result_;
    },

    get hasAppendedContent() {
      return (this.appendedContent_ !== undefined);
    },

    get appendedContent() {
      return this.appendedContent_;
    },

    addHTMLOutput: function(element) {
      this.testWorkArea_.appendChild(element);
      this.appendedContent_ = element;
    },

    toString: function() {
      return this.name_;
    }
  };

  var testRunner;
  function testSuite(name, suite) {
    testRunner.addSuite(new TestSuite(name, suite));
  }

  function Suites(suitePaths, tests) {
    testRunner = new TestRunner(suitePaths, tests);
    testRunner.loadSuites();
  }

  function runSuites() {
    testRunner.run();
  }

  return {
    showCondensed: showCondensed,
    testSuite: testSuite,
    runSuites: runSuites,
    Suites: Suites
  };
});