pulp.Modules.unit = 'UnitTest';

(function() {
  var $p = pulp.base;
  var symbolOpen = '[&ndash;]';
  var symbolClosed = '[+]';
  var symbolNone = '[::]';

  if (!window.console || typeof window.console.log != 'function') {
    window.console = {
      log: function(error) {
        document.getElementsByTagName('body')[0].appendChild($E('p', null, error));
      }
    };
  };
  
  var $ = function(id) {
    return document.getElementById(id);
  };
  
  // create an element
  var $E = function(tag, properties, children) {
    var el = document.createElement(tag);

    // set any properties
    for (var prop in properties || {}) {
      el[prop] = properties[prop];
    }
    // append a single dom node
    if (children && children.nodeType == 1) {
      el.appendChild(children);
    
    // append an array of dom nodes
    } else if ($p.isArray(children)) {
      for (var i = 0, len = children.length; i < len; i++) {
        el.appendChild(children[i]);
      }
    // set innerHTML
    } else if (typeof children === 'string' || typeof children === 'number') {
      el.innerHTML = $p.escapeHTML(children);
    }
    return el;
  };
  
  // build a human-readable string representing the given variable
  var inspect = function(value, level) {
    var result, level = level || 0;
    if (level >= 1) {
      return '';
    }
    // inspect an array
    if ($p.isArray(value)) {
      // show no more than 5 items
      var newVals = [], max = value.length > 5 ? 5 : value.length;
      // inspect each child
      for (var i = 0, len = max; i < len; i++) {
        newVals.push(inspect(value[i]), ++level);
      }
      result = '[' + newVals.join(',') + (value.length > 5 ? ',...' : '') + ']';
    }
    // inspect a dom element
    else if (value && value.nodeType == 1) {
      // show tagName, id, and class
      result = '<' + value.tagName.toLowerCase() + 
        (value.id ? ' id="' + value.id + '"' : '') +
        (value.className ? ' class="' + value.className + '"' : '') + '>';
    }
    // cast to string
    try {
      result = String(value);
    } catch (e) { result = '[object Object]'; }
    // inspect properties
    /*if (typeof value != 'string' && (/object/i).test(result)) {
      // show up to 5 properties
      var newVals = [], i = 1;
      for (var prop in value) {
        newVals.push(prop + ':' + inspect(value[prop]), ++level);
        if (++i > 5) {
          break;
        }
      }
      result = '{' + newVals.join(',') + (i > 5 ? ',...' : '') + '}';
    } else */if (typeof value == 'string') {
      // surround strings with single quotes
      result = "'" + result + "'";
    }
    // limit to 300 characters and escape HTML
    return result.length > 300 ? result.substr(0, 300) + '...' : result;
  };
  
  var objectIsEmpty = function(o) {
    for (var p in o) {
      return false;
    }
    return true;
  };  
  
  var saveResult = function(passed, args) {
    var name = this.currentTestName;
    if (passed) {
      this.assertions.passed++;
      this.assertionsPassed++;
      if (args && args[2]) { // save description if given
        this.assertions.push(args[2]);
      }
    } else {
      this.assertions.failed++;
      this.assertionsFailed++;
      this.assertions.push(args);
    }    
  };
  
  var writeContainers = function(div) {      
    this.tbody = $E('tbody', null, $E('tr', null, [
      $E('th', {className: 'name'}, 'Unit Name'),
      $E('th', {className: 'passed'}, 'Passed'),
      $E('th', {className: 'failed'}, 'Failed'),
      $E('th', {className: 'messages'}, 'Result Messages')
    ]));
    this.summary = $E('span', {id: 'summary'}, 'RUNNING');
    div.appendChild($E('p', {id : 'summary-frame'}, this.summary));
    div.appendChild($E('table', {id: 'unit', cellSpacing: '0'}, this.tbody));
  };

  var writeResultRow = function(tbody, name, assertions, exception, retry) {
    var lis = [], nameContent = [], messageContents = [];
    // build name content
    if (assertions.passed > 0) {
      for (var i = 0, len = assertions.length; i < len; i++) {
        if (typeof assertions[i] === 'string' && assertions[i].length) {
      // pass with message!
          lis.push($E('li', null, assertions[i]));
        }
      }
    }
    if (lis.length) {
      nameContent.push($E('h3', {className: 'expandable closed', onclick: function() { pulp.unit.togglePassed(this) }}, [
        $E('span', {className: 'expander'}, symbolClosed),
        $E('span', null, ' ' + name)
      ]));
      var ul = $E('ul', null, lis);
      ul.style.display = 'none';
      nameContent.push(ul);
    } else {
      nameContent.push($E('h3', {className: 'fixed'}, [
        $E('span', {className: 'expander'}, symbolNone),
        $E('span', null, ' ' + name)
      ]));
    }
    var msgLis = [];
    // build message content
    if (assertions.failed > 0) {
      for (i = 0, len = assertions.length; i < len; i++) {
        if (typeof assertions[i] != 'string') {
          // failed
          expected = assertions[i][0];
          actual = assertions[i][1];
          description = assertions[i][2];
          msgLis.push($E('li', null, 
            (description ? description + '; ' : '') + 'EXPECTED: `' + inspect(expected) + 
            '` ACTUAL: `' + inspect(actual) + '`'
          ));
        }
      }
    }
    messageContents.push($E('ul', null, msgLis));
    var bench = assertions.benchmarks;
    if (bench.length) {
      for (var i = 0, len = bench.length; i < len; i++) {
        messageContents.push($E('p', {className: 'benchmark'}, [
          $E('span', null, 'BENCHMARK: `' + bench[i][2] + '` for ' + bench[i][1] + 's '),
          $E('input', {onclick: (function(fn, seconds, description) {
            if (typeof fn === 'function') {
              // single benchmark
              var run = runBenchmark;
              var write = writeBenchmarkResult;
            } else {
              // hash of functions to compare
              var run = runBenchmarkCompare;
              var write = writeBenchmarkResultCompare;              
            }
            return function(event) {
              event = event || window.event;
              var btn = event.target || event.srcElement;
              btn.disabled = true;
              btn.value = '...';
              // TODO: force dom refresh
              var tmp = document.createElement('span');
              document.body.appendChild(tmp);
              document.body.removeChild(tmp);
              var result = run(fn, seconds, description);
              write(btn, result);
              btn.disabled = false;
              btn.value = 'Run';                
            };
          })(bench[i][0], bench[i][1], bench[i][2]), value: 'Run', type: 'button'})
        ]));
      }
    }
    if (exception) {
      messageContents.push($E('li', {className: 'exception'}, [
        $E('span', null, 'EXCEPTION: ' + exception.name + ': `' + exception.message + '` '),
        $E('input', {type: 'button', value: 'Throw', onclick: function() {
          retry();
        }}) 
      ]));
    }
    var info = assertions.info;
    for (var i = 0, len = info.length; i < len; i++) {
      messageContents.push($E('p', {className: 'info'}, 'INFO: ' + info[i]));
    }

    tbody.appendChild($E('tr', {className: (assertions.failed > 0 || exception ? 'failed' : 'passed')}, [
      $E('td', {className: 'name'}, nameContent),
      $E('td', {className: 'passed'}, assertions.passed),
      $E('td', {className: 'failed'}, assertions.failed),
      $E('td', {className: 'messages'}, messageContents.length ? messageContents : '&nbsp;')
    ]));
  };

  var writeBenchmarkResult = function(btn, time) {
    btn.parentNode.appendChild($E('span', null, ' (' + (time.toFixed(2)) + 'ms) ')); 
  };
  
  var writeBenchmarkResultCompare = function(btn, results) {
    var report = ' (', min = false, ms;
    // find quickest
    for (var testName in results) {
      ms = results[testName];
      if (min === false || min < ms) {
        min = ms;
      }
    }
    // find winner
    for (var testName in results) {
      if (min == results[testName]) {
        report += 'winner=' + testName + ' at ' + results[testName].toFixed(2) + 'ms; ';
        break;
      }
    }
    
    // show quickness as a percent of min
    var percent;
    for (var testName in results) {
      if (min != results[testName]) {
        percent = results[testName] / min;
        if (isNaN(percent)) {
          percent = 100;
        }
        report += testName + '=' + percent.toFixed(1) + '%; ';
      }
    }    
    //report = report.slice(0, -2) + ') ';    
    btn.parentNode.appendChild($E('span', null, report)); 
  };  
  
  var loopOverhead = false;
  var getLoopOverhead = function() {
    if (loopOverhead === false) {
      loopOverhead = runBenchmark(function() {}, 0.5, true);
    }    
    return loopOverhead;
  };
  
  var runBenchmark = function(fn, seconds, ignoreOverhead) {
    seconds = parseFloat(seconds);
    if (seconds <= 0) {
      seconds = 1;
    } else if (seconds > 15) {
      seconds = 15;
    }
    var begin = new Date().getTime();
    var end = begin + (seconds * 1000);
    var i = 0, start, now;
    while (1) {
      i++;
      fn();
      now = new Date().getTime();
      if (now >= end) {
        break;
      }
    }
    var time = ((now - begin) / i) - (ignoreOverhead ? 0 : getLoopOverhead());
    return time > 0 ? time : 0.0;
  };
  
  var runBenchmarkCompare = function(fns, seconds, description) {
    var results = {};
    for (var testName in fns) {
      results[testName] = runBenchmark(fns[testName], seconds); 
    }
    return results;
  };    
  
  var instance;
  
  pulp.unit = function(div) {
    if (!instance) {
      instance = new pulp.Unit(div);
    }    
    return instance;
  };
  
  var delayedCount = 0;
  
  $p.extend(pulp.unit, {
    togglePassed: function(h3) {
      var symbol = h3.getElementsByTagName('span')[0];
      var isOpen = h3.className == 'expandable open';
      symbol.innerHTML = (isOpen ? symbolClosed : symbolOpen);
      h3.className = 'expandable ' + (isOpen ? 'closed' : 'open');
      h3.parentNode.getElementsByTagName('ul')[0].style.display = (isOpen ? 'none' : '');
    },
    inspect: inspect,
    oneTest: function(div, name, fn) {
      var tests = {};
      tests[name] = fn;
      return pulp.unit(div).addTests(tests).run();
    },
    // TODO: record the number of functions waiting in queue and fire the finished signal to the parent frame when all are fired
    delayedTest: function(div, name, fn, delay) {
      delayedCount++;
      window.setTimeout(function() {        
        pulp.unit.oneTest(div, name, fn);
        if (--delayedCount == 0) {
//console.log('delays are done');
          // TODO: fire signal to parent frame
        }
      }, delay || 50);
      return pulp.unit;
    }    
  });
  
  pulp.Unit = pulp.cls.create({
    initialize: function(div) {
      this.resultsDiv = $(div);
      this.tests = {};
      this.ran = {};
      this.reset();
    },
    run: function() {
      if (this.running) {
        return;
      }
      this.running = true;
      
      for (var i = 0; i < this._setups.length; i++) {
        this._setups[i].call(this);
      }
      this._setups = [];
      
      if (objectIsEmpty(this.ran)) {
        writeContainers.call(this, this.resultsDiv);
      }
      
      for (var name in this.tests) {
        if (typeof this.tests[name] === 'function') {      
          this.assertions = [];
          this.assertions.benchmarks = [];
          this.assertions.info = [];
          this.assertions.passed = 0;
          this.assertions.failed = 0;
          try {
            this.tests[name].call(this);
            if (this.assertions.failed == 0) {
              this.testsPassed++;
            } else {
              this.testsFailed++;
            }
            writeResultRow(this.tbody, name, this.assertions);
            /*(function(tbody, name, assertions) {
              window.setTimeout(function() {
                writeResultRow(tbody, name, assertions);
              }, 100);            
            })(this.tbody, name, this.assertions);    */          
          } catch(e) {
            this.testsFailed++;
            this.exceptionsThrown++;
            writeResultRow(this.tbody, name, this.assertions, e, (function(self, runTest) {
              return function() {
                runTest.call(self);
              };
            })(this, this.tests[name]));
            /*(function(tbody, name, assertions) {
              window.setTimeout(function() {
                writeResultRow(tbody, name, assertions, e);
              }, 100);            
            })(this.tbody, name, this.assertions);*/
//throw e;          
//console.log(e);          
          }
        } else {
          this._startSection(name);
        }
        this.ran[name] = this.tests[name];
        delete this.tests[name];          
      }
      this.writeSummary();
      
      for (var i = 0; i < this._teardowns.length; i++) {
        this._teardowns[i].call(this);
      }
      this._teardowns = [];
//console.log(this.benchmarks);
      this.running = false;    
      return this;  
    },
    reset: function() {
      $p.extend(this.tests, this.ran);
      this.ran = {};
      this.running = false;
      this._setups = [];
      this._teardowns = [];
      this.assertionsPassed = 0;
      this.assertionsFailed = 0;
      this.testsPassed = 0;
      this.testsFailed = 0;
      this.exceptionsThrown = 0;            
    },
    setup: function(fn) {
      this._setups.push(fn);
      return this;
    },
    teardown: function(fn) {
      this._teardowns.push(fn);
      return this;
    },
    addTest: function(name, fn) {
      this.tests[name] = fn;
      return this;
    },
    addTests: function(tests) {
      $p.extend(this.tests, tests);
      return this;
    },
    _startSection: function(name) {
      this.tbody.appendChild($E('tr', null, $E('td', {
        colSpan: '4',
        className: 'section'
      }, name)));
    },
    writeSummary: function() {
      var totalAssertions = this.assertionsPassed + this.assertionsFailed;
      var totalTests = this.testsPassed + this.testsFailed;
      var status = (this.testsFailed || this.exceptionsThrown ? 'failed' : 'passed');
      this.summary.className = status;
      this.summary.innerHTML = 'UNITS: Passed ' + this.testsPassed + '/' + totalTests + 
        '; ASSERTIONS: Passed ' + this.assertionsPassed + '/' + totalAssertions + 
        '; EXCEPTIONS: ' + this.exceptionsThrown;
      if (this.setLauncherResult) { // callback launcher frame to indicate success
        this.setLauncherResult(status);
      }
    },    
    info: function(text) {
      this.assertions.info.push(text);
    },
    benchmark: function(fn, seconds, description) {      
      this.assertions.benchmarks.push([fn, seconds, description || fn.toString()]);
    },
    assertEqual: function(expected, actual, description) {
      saveResult.call(this, expected == actual, arguments);
    },
    assertNotEqual: function(expected, actual, description) {
      saveResult.call(this, expected != actual, arguments);
    },
    assertIdentical: function(expected, actual, description) {
      saveResult.call(this, expected === actual, arguments);
    },
    assertNotIdentical: function(expected, actual, description) {
      saveResult.call(this, expected !== actual, arguments);
    },
    assert: function(actual, description) {
      saveResult.call(this, !!actual, [true, actual, description]);
    },
    assertFalse: function(actual, description) {
      saveResult.call(this, !actual, [false, actual, description]);
    },    
    assertFalsy: function(obj, description) {
      saveResult.call(this, (o && o.valueOf ? !o.valueOf() : !o), ['{falsy}', obj, description]); 
    },
    assertTruish: function(o, description) {
      saveResult.call(this, (o && o.valueOf ? !!o.valueOf() : !!o), ['{truish}', obj, description]);       
    },
    assertInstanceOf: function(fn, obj, description) {
      saveResult.call(this, (typeof fn == 'function') && (typeof obj == 'object') && (obj instanceof fn), arguments);
    },
    assertTypeOf: function(expected, actual, description) {
      saveResult.call(this, typeof actual == expected, arguments);
    },    
    assertUndefined: function(actual, description) {
      saveResult.call(this, actual === undefined, [undefined, actual, description]);
    },
    assertOneOf: function(expected, actual, description) {
      saveResult.call(this, $p.inArray(expected, actual), ['one of: ' + expected.join(', '), actual, description]);
    },
    assertEnumEqual: function(expected, actual, description) {
      if (expected && actual && expected.length == actual.length) {
        var same = true;
        try {
          for (var i = 0, len = expected.length; i < len; i++) {
            if (expected[i] != actual[i]) {
              same = false;
              break;
            }
          }
        } catch(e) {
          same = false;
        }
      } else {
        var same = false;
      }
      saveResult.call(this, same, [expected, actual, description]);
    },
    assertEnumIdentical: function(expected, actual, description) {
      if (expected && actual && expected.length == actual.length) {
        var same = true;
        try {
          for (var i = 0, len = expected.length; i < len; i++) {
            if (expected[i] !== actual[i]) {
              same = false;
              break;
            }
          }
        } catch(e) {
          same = false;
        }
      } else {
        var same = false;
      }
      saveResult.call(this, same, [expected, actual, description]);
    },    
    assertEnumTestFunction: function(array, testFn, description) {
      try {
        var pass = true;
        for (var i = 0, len = array.length; i < len; i++) {
          if (testFn.call(this, array[i], i, array) == false) {
            pass = false;
            break;
          }
        }
      } catch(e) {
        pass = false;
      }
      saveResult.call(this, pass, [true, pass, description]);
    },
    assertInspectEqual: function(expected, actual, description) {
      saveResult.call(this, inspect(expected) == inspect(actual), arguments);
    },
    assertPropertiesEqual: function(expected, actual, description) {
      var actualClone = $p.extend({}, actual);
      var same = true;
      for (var prop in expected) {
        if (expected[prop] == actualClone[prop]) {
          delete actualClone[prop];
        } else {
          same = false;
          break;
        }
      }
      if (same) {
        for (var prop in actualClone) {
          same = false;
        }
      }
      saveResult.call(this, same, arguments);
    },
    assertPropertiesMatch: function(expected, actual, description) {
      var same = true;
      for (var prop in expected) {
        if (expected[prop] != actual[prop]) {
          same = false;
          break;
        }
      }
      saveResult.call(this, same, arguments);
    },
    assertRegExpMatch: function(regex, actual, description) {
      var pass = true;
      if (!regex.test(actual)) {
        pass = false;
        regex = String(regex);
      }
      saveResult.call(this, pass, [regex, actual, description]);
    },
    assertInString: function(string, actual, description) {
      saveResult.call(this, actual.indexOf(string) > -1, arguments);
    },
    assertNotInString: function(string, actual, description) {
      saveResult.call(this, actual.indexOf(string) == -1, arguments);
    }, 
    postResults: function(url, allowedSourceDomains) {
      var doPost = false;
      if (allowedSourceDomains === undefined) {
        doPost = true;
      } else if (location.hostname) {
        $p.each($p.isArray(allowedSourceDomains) ? allowedSourceDomains : [allowedSourceDomains], function(domain) {
          if (location.hostname == domain) {
            doPost = true;
            throw pulp.Break;
          }
        });
      }
      if (doPost) {
        var keys = 'userAgent platform appName oscpu cpuClass systemLanguage language cookieEnabled onLine'.split(' ');
        var data = ['pulp.Version=' + pulp.Version];
        $p.each(keys, function(key) {
          data.push('navigator.' + key + '=' + encodeURIComponent(navigator[key]));
        });
        data.push('screen.width=' + screen.width);
        data.push('screen.height=' + screen.height);
        // viewport height and width
        $p.each('Width Height'.split(' '), function(d) {
          if (navigator.userAgent.indexOf('AppleWebKit/') > -1 && !document.evaluate) {
            // Safari <3.0 needs self.innerWidth/Height
            data.push('viewport' + d + '=' + self['inner' + d]);
          }
          else if (window.opera && window.opera.version && parseFloat(window.opera.version()) < 9.5) {
            // Opera <9.5 needs document.body.clientWidth/Height
            data.push('viewport' + d + '=' + document.body['client' + d]);
          }
          else {
            data.push('viewport' + d + '=' + document.documentElement['client' + d]);
          }
        });
        url += '?' + data.join('&');      
        $E('iframe', {className: 'hide', src: url});
      }
    },
    // make private functions "protected"
    '_$E': $E,
    _inspect: inspect,
    _writeContainers: writeContainers,
    _writeResultRow: writeResultRow,
    _writeBenchmarkResult: writeBenchmarkResult,
    _writeBenchmarkResultCompare:writeBenchmarkResultCompare,
    _runBenchmark: runBenchmark,
    _runBenchmarkCompare: runBenchmarkCompare
  });
  
  pulp.Unit.aliasMethods({
    assertTrue: 'assert'
  });
  
})();