// 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.ErrorCode');
goog.require('goog.Promise');
goog.require('goog.Uri');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('webdriver.Command');
goog.require('webdriver.http.Client');
goog.require('webdriver.http.Executor');
goog.require('webdriver.test.testutil');

// Alias for readability.
var callbackHelper = webdriver.test.testutil.callbackHelper;

var control = new goog.testing.MockControl();
var mockClient, executor, onCallback, onErrback;

function shouldRunTests() {
  return !goog.userAgent.IE || goog.userAgent.isVersionOrHigher(10);
}

function setUp() {
  mockClient = control.createStrictMock(webdriver.http.Client);

  executor = new webdriver.http.Executor(mockClient);
}

function tearDown() {
  control.$tearDown();
}

function assertSuccess() {
  onErrback.assertNotCalled('Did not expect errback');
  onCallback.assertCalled('Expected callback');
}

function assertFailure() {
  onCallback.assertNotCalled('Did not expect callback');
  onErrback.assertCalled('Expected errback');
}

function headersToString(headers) {
  var str = [];
  for (var key in headers) {
    str.push(key + ': ' + headers[key]);
  }
  return str.join('\n');
}

function expectRequest(method, path, data, headers) {
  var description = method + ' ' + path + '\n' + headersToString(headers) +
                    '\n' + JSON.stringify(data);

  return mockClient.send(new goog.testing.mockmatchers.ArgumentMatcher(
      function(request) {
        assertEquals('wrong method', method, request.method);
        assertEquals('wrong path', path + '', request.path);
        webdriver.test.testutil.assertObjectEquals(data, request.data);
        assertNull(
            'Wrong headers for request:\n' + description +
            '\n    Actual headers were:\n' + headersToString(request.headers),
            goog.testing.asserts.findDifferences(headers, request.headers));
        return true;
      }, description));
}

function response(status, headers, body) {
  return new webdriver.http.Response(status, headers, body);
}

function respondsWith(error, opt_response) {
  return function() {
    if (error) {
      return goog.Promise.reject(error);
    } else {
      return goog.Promise.resolve(opt_response);
    }
  };
}

///////////////////////////////////////////////////////////////////////////////
//
//  Tests
//
///////////////////////////////////////////////////////////////////////////////

function testBuildPath() {
  var parameters = {'sessionId':'foo', 'url':'http://www.google.com'};
  var finalPath = webdriver.http.Executor.buildPath_(
      '/session/:sessionId/url', parameters);
  assertEquals('/session/foo/url', finalPath);
  webdriver.test.testutil.assertObjectEquals({'url':'http://www.google.com'},
      parameters);
}

function testBuildPath_withWebElement() {
  var parameters = {'sessionId':'foo', 'id': {}};
  parameters['id']['ELEMENT'] = 'bar';

  var finalPath = webdriver.http.Executor.buildPath_(
      '/session/:sessionId/element/:id/click', parameters);
  assertEquals('/session/foo/element/bar/click', finalPath);
  webdriver.test.testutil.assertObjectEquals({}, parameters);
}

function testBuildPath_throwsIfMissingParameter() {
  assertThrows(goog.partial(webdriver.http.Executor.buildPath_,
      '/session/:sessionId', {}));

  assertThrows(goog.partial(webdriver.http.Executor.buildPath_,
      '/session/:sessionId/element/:id', {'sessionId': 'foo'}));
}

function testBuildPath_doesNotMatchOnSegmentsThatDoNotStartWithColon() {
  assertEquals('/session/foo:bar/baz',
      webdriver.http.Executor.buildPath_('/session/foo:bar/baz', {}));
}

function testExecute_rejectsUnrecognisedCommands() {
  assertThrows(goog.bind(executor.execute, executor,
      new webdriver.Command('fake-command-name')));
}

/**
 * @param {!webdriver.Command} command The command to send.
 * @param {!Function=} opt_onSuccess The function to check the response with.
 */
function assertSendsSuccessfully(command, opt_onSuccess) {
  return executor.execute(command)
      .then(function(response) {
        control.$verifyAll();
        if (opt_onSuccess) {
          opt_onSuccess(response);
        }
      });
}

/**
 * @param {!webdriver.Command} command The command to send.
 * @param {!Function=} opt_onError The function to check the error with.
 */
function assertFailsToSend(command, opt_onError) {
  return executor.execute(command)
      .then(fail, function(e) {
        control.$verifyAll();
        if (opt_onError) {
          opt_onError(e);
        }
      });
}

function testExecute_clientFailsToSendRequest() {
  var error = new Error('boom');
  expectRequest('POST', '/session', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).
  $does(respondsWith(error));
  control.$replayAll();

  return assertFailsToSend(
      new webdriver.Command(webdriver.CommandName.NEW_SESSION),
      function(e) {
        assertEquals(error, e);
      });
}

function testExecute_commandWithNoUrlParameters() {
  expectRequest('POST', '/session', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).
  $does(respondsWith(null, response(200, {}, '')));
  control.$replayAll();

  return assertSendsSuccessfully(
      new webdriver.Command(webdriver.CommandName.NEW_SESSION));
}

function testExecute_rejectsCommandsMissingUrlParameters() {
  var command =
      new webdriver.Command(webdriver.CommandName.FIND_CHILD_ELEMENT).
          setParameter('sessionId', 's123').
          // Let this be missing: setParameter('id', {'ELEMENT': 'e456'}).
          setParameter('using', 'id').
          setParameter('value', 'foo');

  control.$replayAll();
  assertThrows(goog.bind(executor.execute, executor, command));
  control.$verifyAll();
}

function testExecute_replacesUrlParametersWithCommandParameters() {
  var command =
      new webdriver.Command(webdriver.CommandName.GET).
          setParameter('sessionId', 's123').
          setParameter('url', 'http://www.google.com');

  expectRequest('POST', '/session/s123/url',
      {'url': 'http://www.google.com'},
      {'Accept': 'application/json; charset=utf-8'}).
      $does(respondsWith(null, response(200, {}, '')));
  control.$replayAll();

  return assertSendsSuccessfully(command);
}

function testExecute_returnsParsedJsonResponse() {
  var responseObj = {
    'status': bot.ErrorCode.SUCCESS,
    'value': 'http://www.google.com'
  };
  var command = new webdriver.Command(webdriver.CommandName.GET_CURRENT_URL).
      setParameter('sessionId', 's123');

  expectRequest('GET', '/session/s123/url', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).$does(respondsWith(null,
      response(200, {'Content-Type': 'application/json'},
          JSON.stringify(responseObj))));
  control.$replayAll();

  return assertSendsSuccessfully(command, function(response) {
    webdriver.test.testutil.assertObjectEquals(responseObj, response);
  });
}

function testExecute_returnsSuccessFor2xxWithBodyAsValueWhenNotJson() {
  var command = new webdriver.Command(webdriver.CommandName.GET_CURRENT_URL).
      setParameter('sessionId', 's123');

  expectRequest('GET', '/session/s123/url', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).$does(respondsWith(null,
      response(200, {}, 'hello, world\r\ngoodbye, world!')));
  control.$replayAll();

  return assertSendsSuccessfully(command, function(response) {
    webdriver.test.testutil.assertObjectEquals({
      'status': bot.ErrorCode.SUCCESS,
      'value': 'hello, world\ngoodbye, world!'
    }, response);
  });
}

function testExecute_returnsSuccessFor2xxInvalidJsonBody() {
  var invalidJson = '[';
  expectRequest('POST', '/session', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).
  $does(respondsWith(null, response(200, {
    'Content-Type': 'application/json'
  }, invalidJson)));
  control.$replayAll();

  return assertSendsSuccessfully(
      new webdriver.Command(webdriver.CommandName.NEW_SESSION),
      function(response) {
        webdriver.test.testutil.assertObjectEquals({
          'status': bot.ErrorCode.SUCCESS,
          'value': invalidJson
        }, response);
      });
}

function testExecute_returnsUnknownCommandFor404WithBodyAsValueWhenNotJson() {
  var command = new webdriver.Command(webdriver.CommandName.GET_CURRENT_URL).
      setParameter('sessionId', 's123');

  expectRequest('GET', '/session/s123/url', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).$does(respondsWith(null,
      response(404, {}, 'hello, world\r\ngoodbye, world!')));
  control.$replayAll();

  return assertSendsSuccessfully(command, function(response) {
    webdriver.test.testutil.assertObjectEquals({
      'status': bot.ErrorCode.UNKNOWN_COMMAND,
      'value': 'hello, world\ngoodbye, world!'
    }, response);
  });
}

function testExecute_returnsUnknownErrorForGenericErrorCodeWithBodyAsValueWhenNotJson() {
  var command = new webdriver.Command(webdriver.CommandName.GET_CURRENT_URL).
      setParameter('sessionId', 's123');

  expectRequest('GET', '/session/s123/url', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).$does(respondsWith(null,
      response(500, {}, 'hello, world\r\ngoodbye, world!')));
  control.$replayAll();

  return assertSendsSuccessfully(command, function(response) {
    webdriver.test.testutil.assertObjectEquals({
      'status': bot.ErrorCode.UNKNOWN_ERROR,
      'value': 'hello, world\ngoodbye, world!'
    }, response);
  });
}

function testExecute_attemptsToParseBodyWhenNoContentTypeSpecified() {
  var responseObj = {
    'status': bot.ErrorCode.SUCCESS,
    'value': 'http://www.google.com'
  };
  var command = new webdriver.Command(webdriver.CommandName.GET_CURRENT_URL).
      setParameter('sessionId', 's123');

  expectRequest('GET', '/session/s123/url', {}, {
    'Accept': 'application/json; charset=utf-8'
  }).$does(respondsWith(null,
      response(200, {}, JSON.stringify(responseObj))));
  control.$replayAll();

  return assertSendsSuccessfully(command, function(response) {
    webdriver.test.testutil.assertObjectEquals(responseObj, response);
  });
}

function testCanDefineNewCommands() {
  executor.defineCommand('greet', 'GET', '/person/:name');

  var command = new webdriver.Command('greet').
      setParameter('name', 'Bob');

  expectRequest('GET', '/person/Bob', {},
      {'Accept': 'application/json; charset=utf-8'}).
      $does(respondsWith(null, response(200, {}, '')));
  control.$replayAll();

  return assertSendsSuccessfully(command);
}

function testCanRedefineStandardCommands() {
  executor.defineCommand(webdriver.CommandName.GO_BACK,
      'POST', '/custom/back');

  var command = new webdriver.Command(webdriver.CommandName.GO_BACK).
      setParameter('times', 3);

  expectRequest('POST', '/custom/back',
      {'times': 3},
      {'Accept': 'application/json; charset=utf-8'}).
      $does(respondsWith(null, response(200, {}, '')));
  control.$replayAll();

  return assertSendsSuccessfully(command);
}

function FakeXmlHttpRequest(headers, status, responseText) {
  return {
    getAllResponseHeaders: function() { return headers; },
    status: status,
    responseText: responseText
  };
}

function testXmlHttpRequestToHttpResponse_parseHeaders_windows() {
  var response = webdriver.http.Response.fromXmlHttpRequest(
      FakeXmlHttpRequest([
        'a:b',
        'c: d',
        'e :f',
        'g : h'
      ].join('\r\n'), 200, ''));
  assertEquals(200, response.status);
  assertEquals('', response.body);

  webdriver.test.testutil.assertObjectEquals({
    'a': 'b',
    'c': 'd',
    'e': 'f',
    'g': 'h'
  }, response.headers);
}

function testXmlHttpRequestToHttpResponse_parseHeaders_unix() {
  var response = webdriver.http.Response.fromXmlHttpRequest(
      FakeXmlHttpRequest([
        'a:b',
        'c: d',
        'e :f',
        'g : h'
      ].join('\n'), 200, ''));
  assertEquals(200, response.status);
  assertEquals('', response.body);

  webdriver.test.testutil.assertObjectEquals({
    'a': 'b',
    'c': 'd',
    'e': 'f',
    'g': 'h'
  }, response.headers);
}

function testXmlHttpRequestToHttpResponse_noHeaders() {
  var response = webdriver.http.Response.fromXmlHttpRequest(
      FakeXmlHttpRequest('', 200, ''));
  assertEquals(200, response.status);
  assertEquals('', response.body);
  webdriver.test.testutil.assertObjectEquals({}, response.headers);
}

function testXmlHttpRequestToHttpResponse_stripsNullCharactersFromBody() {
  var response = webdriver.http.Response.fromXmlHttpRequest(
      FakeXmlHttpRequest('', 200, '\x00\0foo\x00\x00bar\x00\0'));
  assertEquals(200, response.status);
  assertEquals('foobar', response.body);
  webdriver.test.testutil.assertObjectEquals({}, response.headers);
}
