// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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.

goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('goog.string');
goog.require('goog.testing.JsUnitException');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.StrictMock');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.stacktrace');
goog.require('webdriver.stacktrace');
goog.require('webdriver.test.testutil');

var stubs;

function setUpPage() {
  stubs = new goog.testing.PropertyReplacer();
}

function tearDown() {
  stubs.reset();
}


function assertStackFrame(message, frameOrFrameString, expectedFrame) {
  var frame = frameOrFrameString;
  if (goog.isString(frame)) {
    frame = webdriver.stacktrace.parseStackFrame_(frame);
    assertNotNull(message + '\nunable to parse frame: ' + frameOrFrameString,
        frame);
  }
  var diff = [];
  for (var prop in expectedFrame) {
    if (goog.isString(expectedFrame[prop]) &&
        frame[prop] !== expectedFrame[prop]) {
      diff.push([
        prop, ': <', expectedFrame[prop], '> !== <', frame[prop], '>'
      ].join(''));
    }
  }
  if (diff.length) {
    fail(message +
        '\nfor:      <' + frameOrFrameString + '>' +
        '\nexpected: <' + expectedFrame + '>' +
        '\nbut was:  <' + frame + '>' +
        '\n    ' +
        diff.join('\n    '));
  }
}

function assertFrame(frame, file, line) {
  // Normalize path for when run through Node.js on Windows.
  var url = frame.getUrl().replace(/\\/g, '/');
  assertContains('/' + file, url);
  assertEquals(line, frame.getLine());
}

function testGetStacktraceFromFile() {
  if (!webdriver.stacktrace.BROWSER_SUPPORTED) {
    return false;
  }

  var stacktrace = webdriver.test.testutil.getStackTrace();
  assertFrame(stacktrace[0], 'testutil.js', 50);
  assertFrame(stacktrace[1], 'stacktrace_test.js', 78);
}

function testGetStacktraceWithUrlOnLine() {
  if (!webdriver.stacktrace.BROWSER_SUPPORTED) {
    return false;
  }

  // The url argument is intentionally ignored here.
  function getStacktraceWithUrlArgument(url) {
    return webdriver.stacktrace.get();
  }

  var stacktrace = getStacktraceWithUrlArgument('http://www.google.com');
  assertFrame(stacktrace[0], 'stacktrace_test.js', 90);

  stacktrace = getStacktraceWithUrlArgument('http://www.google.com/search');
  assertFrame(stacktrace[0], 'stacktrace_test.js', 90);
}

function testParseStackFrameInV8() {
  assertStackFrame('exception name only (node v0.8)',
      '    at Error (unknown source)',
      new webdriver.stacktrace.Frame('', 'Error', '', 'unknown source'));

  assertStackFrame('exception name only (chrome v22)',
      '    at Error (<anonymous>)',
      new webdriver.stacktrace.Frame('', 'Error', '', '<anonymous>'));

  assertStackFrame('context object + function name + url',
      '    at Object.assert (file:///.../asserts.js:29:10)',
      new webdriver.stacktrace.Frame('Object', 'assert', '',
          'file:///.../asserts.js:29:10'));

  assertStackFrame('context object + function name + file',
      '    at Object.assert (asserts.js:29:10)',
      new webdriver.stacktrace.Frame('Object', 'assert', '',
          'asserts.js:29:10'));

  assertStackFrame('context object + anonymous function + file',
      '    at Interface.<anonymous> (repl.js:182:12)',
      new webdriver.stacktrace.Frame('Interface', '<anonymous>', '',
          'repl.js:182:12'));

  assertStackFrame('url only',
      '    at http://www.example.com/jsunit.js:117:13',
      new webdriver.stacktrace.Frame('', '', '',
          'http://www.example.com/jsunit.js:117:13'));

  assertStackFrame('file only',
      '    at repl:1:57',
      new webdriver.stacktrace.Frame('', '', '', 'repl:1:57'));

  assertStackFrame('function alias',
      '    at [object Object].exec [as execute] (file:///foo)',
      new webdriver.stacktrace.Frame('[object Object]', 'exec',
          'execute', 'file:///foo'));

  assertStackFrame('constructor call',
      '    at new Class (file:///foo)',
      new webdriver.stacktrace.Frame('new ', 'Class', '', 'file:///foo'));

  assertStackFrame('object property constructor call',
      '    at new [object Object].foo (repl:1:2)',
      new webdriver.stacktrace.Frame('new [object Object]', 'foo', '',
          'repl:1:2'));

  assertStackFrame('namespaced constructor call',
      '    at new foo.bar.Class (foo:1:2)',
      new webdriver.stacktrace.Frame('new foo.bar', 'Class', '', 'foo:1:2'));

  assertStackFrame('anonymous constructor call',
      '    at new <anonymous> (file:///foo)',
      new webdriver.stacktrace.Frame('new ', '<anonymous>', '',
          'file:///foo'));

  assertStackFrame('native function call',
      '    at Array.forEach (native)',
      new webdriver.stacktrace.Frame('Array', 'forEach', '', 'native'));

  assertStackFrame('eval',
      '    at foo (eval at file://bar)',
      new webdriver.stacktrace.Frame('', 'foo', '', 'eval at file://bar'));

  assertStackFrame('nested anonymous eval',
      '    at eval (eval at <anonymous> (unknown source), <anonymous>:2:7)',
      new webdriver.stacktrace.Frame('', 'eval', '',
          'eval at <anonymous> (unknown source), <anonymous>:2:7'));

  assertStackFrame('Url containing parentheses',
      '    at Object.assert (http://bar:4000/bar.js?value=(a):150:3)',
      new webdriver.stacktrace.Frame('Object', 'assert', '',
          'http://bar:4000/bar.js?value=(a):150:3'));

  assertStackFrame('Frame with non-standard leading whitespace (issue 7994)',
      '  at module.exports.runCucumber (/local/dir/path)',
      new webdriver.stacktrace.Frame('module.exports', 'runCucumber', '',
          '/local/dir/path'));
}

function testParseClosureCanonicalStackFrame() {
  assertStackFrame('unknown frame', '> (unknown)',
      webdriver.stacktrace.ANONYMOUS_FRAME_);
  assertStackFrame('anonymous frame', '> anonymous',
      webdriver.stacktrace.ANONYMOUS_FRAME_);

  assertStackFrame('name only', '> foo',
      new webdriver.stacktrace.Frame('', 'foo', '', ''));

  assertStackFrame('name and path', '> foo at http://x:123',
      new webdriver.stacktrace.Frame('', 'foo', '', 'http://x:123'));

  assertStackFrame('anonymous function with path',
      '> anonymous at file:///x/y/z',
      new webdriver.stacktrace.Frame('', 'anonymous', '', 'file:///x/y/z'));

  assertStackFrame('anonymous function with v8 path',
      '> anonymous at /x/y/z:12:34',
      new webdriver.stacktrace.Frame('', 'anonymous', '', '/x/y/z:12:34'));

  assertStackFrame('context and name only',
      '> foo.bar', new webdriver.stacktrace.Frame('foo', 'bar', '', ''));

  assertStackFrame('name and alias',
      '> foo [as bar]', new webdriver.stacktrace.Frame('', 'foo', 'bar', ''));

  assertStackFrame('context, name, and alias',
      '> foo.bar [as baz]',
      new webdriver.stacktrace.Frame('foo', 'bar', 'baz', ''));

  assertStackFrame('path only', '> http://x:123',
      new webdriver.stacktrace.Frame('', '', '', 'http://x:123'));

  assertStackFrame('name and arguments',
      '> foo(arguments)', new webdriver.stacktrace.Frame('', 'foo', '', ''));

  assertStackFrame('full frame',
      '> foo.bar(123, "abc") [as baz] at http://x:123',
      new webdriver.stacktrace.Frame('foo', 'bar', 'baz', 'http://x:123'));

  assertStackFrame('name and url with sub-domain',
      '> foo at http://x.y.z:80/path:1:2',
      new webdriver.stacktrace.Frame('', 'foo', '',
          'http://x.y.z:80/path:1:2'));

  assertStackFrame('name and url with sub-domain',
      '> foo.bar.baz at http://x.y.z:80/path:1:2',
      new webdriver.stacktrace.Frame('foo.bar', 'baz', '',
          'http://x.y.z:80/path:1:2'));
}

// All test strings are parsed with the conventional and long
// frame algorithms.
function testParseStackFrameInFirefox() {
  var frameString = 'Error("Assertion failed")@:0';
  var frame = webdriver.stacktrace.parseStackFrame_(frameString);
  var expected = new webdriver.stacktrace.Frame('', 'Error', '', '');
  assertObjectEquals('function name + arguments', expected, frame);

  frame = webdriver.stacktrace.parseLongFirefoxFrame_(frameString);
  assertObjectEquals('function name + arguments', expected, frame);

  frameString = '()@file:///foo:42';
  frame = webdriver.stacktrace.parseStackFrame_(frameString);
  expected = new webdriver.stacktrace.Frame('', '', '', 'file:///foo:42');
  assertObjectEquals('anonymous function', expected, frame);

  frame = webdriver.stacktrace.parseLongFirefoxFrame_(frameString);
  assertObjectEquals('anonymous function', expected, frame);

  frameString = '@javascript:alert(0)';
  frame = webdriver.stacktrace.parseStackFrame_(frameString);
  expected = new webdriver.stacktrace.Frame('', '', '', 'javascript:alert(0)');
  assertObjectEquals('anonymous function', expected, frame);

  frame = webdriver.stacktrace.parseLongFirefoxFrame_(frameString);
  assertObjectEquals('anonymous function', expected, frame);
}

function testStringRepresentation() {
  var frame = new webdriver.stacktrace.Frame('window', 'foo', 'bar',
      'http://x?a=1&b=2:1');
  assertEquals('    at window.foo [as bar] (http://x?a=1&b=2:1)',
      frame.toString());

  frame = new webdriver.stacktrace.Frame('', 'Error', '', '');
  assertEquals('    at Error (<anonymous>)', frame.toString());

  assertEquals('    at <anonymous>',
      webdriver.stacktrace.ANONYMOUS_FRAME_.toString());

  frame = new webdriver.stacktrace.Frame('', '', '', 'http://x:123');
  assertEquals('    at http://x:123', frame.toString());

  frame = new webdriver.stacktrace.Frame('foo', 'bar', '', 'http://x:123');
  assertEquals('    at foo.bar (http://x:123)', frame.toString());

  frame = new webdriver.stacktrace.Frame('new ', 'Foo', '', 'http://x:123');
  assertEquals('    at new Foo (http://x:123)', frame.toString());

  frame = new webdriver.stacktrace.Frame('new foo', 'Bar', '', '');
  assertEquals('    at new foo.Bar (<anonymous>)', frame.toString());
}

// Create a stack trace string with one modest record and one long record,
// Verify that all frames are parsed. The length of the long arg is set
// to blow Firefox 3x's stack if put through a RegExp.
function testParsingLongStackTrace() {
  var longArg = goog.string.buildString(
      '(', goog.string.repeat('x', 1000000), ')');
  var stackTrace = goog.string.buildString(
      'shortFrame()@:0\n',
      'longFrame',
      longArg,
      '@http://google.com/somescript:0\n');
  var frames = webdriver.stacktrace.parse_(stackTrace);
  assertEquals('number of returned frames', 2, frames.length);
  var expected = new webdriver.stacktrace.Frame('', 'shortFrame', '', '');
  assertStackFrame('short frame', frames[0], expected);

  expected = new webdriver.stacktrace.Frame(
      '', 'longFrame', '', 'http://google.com/somescript:0');
  assertStackFrame('exception name only', frames[1], expected);
}

function testRemovesV8MessageHeaderBeforeParsingStack() {
  var stack =
        '    at Color.red (http://x:1234)\n' +
        '    at Foo.bar (http://y:5678)';

  var frames = webdriver.stacktrace.parse_(stack);
  assertEquals(2, frames.length);
  assertEquals('    at Color.red (http://x:1234)', frames[0].toString());
  assertEquals('    at Foo.bar (http://y:5678)', frames[1].toString());
}

function testCanParseClosureJsUnitExceptions() {
  stubs.set(goog.testing.stacktrace, 'get', function() {
    return '> Color.red at http://x:1234\n' +
           '> Foo.bar at http://y:5678';
  });

  var error = new goog.testing.JsUnitException('stub');
  stubs.reset();

  var frames = webdriver.stacktrace.parse_(error.stackTrace);
  assertEquals(2, frames.length);
  assertEquals('    at Color.red (http://x:1234)', frames[0].toString());
  assertEquals('    at Foo.bar (http://y:5678)', frames[1].toString());
}

function testFormattingAV8StyleError() {
  var errorStub = {
    name: 'Error',
    message: 'foo',
    toString: function() { return 'Error: foo'; },
    stack:
        'Error: foo\n' +
        '    at Color.red (http://x:1234)\n' +
        '    at Foo.bar (http://y:5678)'
  };

  var ret = webdriver.stacktrace.format(errorStub);
  assertEquals(errorStub, ret);
  assertEquals([
    'Error: foo',
    '    at Color.red (http://x:1234)',
    '    at Foo.bar (http://y:5678)'
  ].join('\n'), ret.stack);
}

function testFormattingAFirefoxStyleError() {
  var errorStub = {
    name: 'Error',
    message: 'boom',
    toString: function() { return 'Error: boom'; },
    stack:
        'foo@file:///foo/foo.js:1\n' +
        '@file:///bar/bar.js:1'
  };

  var ret = webdriver.stacktrace.format(errorStub);
  assertEquals(errorStub, ret);
  assertEquals([
    'Error: boom',
    '    at foo (file:///foo/foo.js:1)',
    '    at file:///bar/bar.js:1'
  ].join('\n'), ret.stack);
}

function testInsertsAnAnonymousFrameWhenUnableToParse() {
  var errorStub = {
    name: 'Error',
    message: 'boom',
    toString: function() { return 'Error: boom'; },
    stack:
        'foo@file:///foo/foo.js:1\n' +
        'this is unparsable garbage\n' +
        '@file:///bar/bar.js:1'
  };

  var ret = webdriver.stacktrace.format(errorStub);
  assertEquals(errorStub, ret);
  assertEquals([
    'Error: boom',
    '    at foo (file:///foo/foo.js:1)',
    '    at <anonymous>',
    '    at file:///bar/bar.js:1'
  ].join('\n'), ret.stack);
}

function testFormattingBotErrors() {
  var error = new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT, 'boom');
  var expectedStack = [
    'NoSuchElementError: boom',
    '    at Color.red (http://x:1234)',
    '    at Foo.bar (http://y:5678)'
  ].join('\n');
  error.stack = expectedStack;

  var ret = webdriver.stacktrace.format(error);
  assertEquals(ret, error);
  assertEquals(expectedStack, error.stack);
}

function testFormatsUsingNameAndMessageIfAvailable() {
  var ret = webdriver.stacktrace.format({
    name: 'TypeError',
    message: 'boom'
  });
  assertEquals('TypeError: boom\n', ret.stack);

  ret = webdriver.stacktrace.format({
    message: 'boom'
  });
  assertEquals('boom\n', ret.stack);

  ret = webdriver.stacktrace.format({
    toString: function() {
      return 'Hello world'
    }
  });
  assertEquals('Hello world\n', ret.stack);

  ret = webdriver.stacktrace.format({
    name: 'TypeError',
    message: 'boom',
    toString: function() {
      return 'Should not use this'
    }
  });
  assertEquals('TypeError: boom\n', ret.stack);
}

function testDoesNotFormatErrorIfOriginalStacktraceIsInAnUnexpectedFormat() {
  var error = Error('testing');
  var stack = error.stack = [
    'Error: testing',
    '..> at Color.red (http://x:1234)',
    '..> at Foo.bar (http://y:5678)'
  ].join('\n');

  var ret = webdriver.stacktrace.format(error);
  assertEquals(ret, error);
  assertEquals(stack, error.stack);
}

function testParseStackFrameInIE10() {
  assertStackFrame('name and path',
      '   at foo (http://bar:4000/bar.js:150:3)',
      new webdriver.stacktrace.Frame('', 'foo', '',
          'http://bar:4000/bar.js:150:3'));

  assertStackFrame('Anonymous function',
      '   at Anonymous function (http://bar:4000/bar.js:150:3)',
      new webdriver.stacktrace.Frame('', 'Anonymous function', '',
          'http://bar:4000/bar.js:150:3'));

  assertStackFrame('Global code',
      '   at Global code (http://bar:4000/bar.js:150:3)',
      new webdriver.stacktrace.Frame('', 'Global code', '',
          'http://bar:4000/bar.js:150:3'));

  assertStackFrame('eval code',
      '   at foo (eval code:150:3)',
      new webdriver.stacktrace.Frame('', 'foo', '', 'eval code:150:3'));

  assertStackFrame('nested eval',
      '   at eval code (eval code:150:3)',
      new webdriver.stacktrace.Frame('', 'eval code', '', 'eval code:150:3'));

  assertStackFrame('Url containing parentheses',
      '   at foo (http://bar:4000/bar.js?value=(a):150:3)',
      new webdriver.stacktrace.Frame('', 'foo', '',
          'http://bar:4000/bar.js?value=(a):150:3'));

  assertStackFrame('Url ending with parentheses',
      '   at foo (http://bar:4000/bar.js?a=))',
      new webdriver.stacktrace.Frame('', 'foo', '',
          'http://bar:4000/bar.js?a=)'));
}
