// 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.

'use strict';

var assert = require('assert'),
    sinon = require('sinon');

var Capabilities = require('../../lib/capabilities').Capabilities,
    Command = require('../../lib/command').Command,
    CommandName = require('../../lib/command').Name,
    error = require('../../lib/error'),
    http = require('../../lib/http'),
    Session = require('../../lib/session').Session,
    promise = require('../../lib/promise'),
    WebElement = require('../../lib/webdriver').WebElement;

describe('http', function() {
  describe('buildPath', function() {
    it('properly replaces path segments with command parameters', function() {
      var parameters = {'sessionId':'foo', 'url':'http://www.google.com'};
      var finalPath = http.buildPath('/session/:sessionId/url', parameters);
      assert.equal(finalPath, '/session/foo/url');
      assert.deepEqual(parameters,  {'url':'http://www.google.com'});
    });

    it('handles web element references', function() {
      var parameters = {'sessionId':'foo', 'id': WebElement.buildId('bar')};

      var finalPath = http.buildPath(
          '/session/:sessionId/element/:id/click', parameters);
      assert.equal(finalPath, '/session/foo/element/bar/click');
      assert.deepEqual(parameters, {});
    });

    it('throws if missing a parameter', function() {
      assert.throws(
        () => http.buildPath('/session/:sessionId', {}),
        function(err) {
          return err instanceof error.InvalidArgumentError
              && 'Missing required parameter: sessionId' === err.message;
        });

      assert.throws(
        () => http.buildPath(
            '/session/:sessionId/element/:id', {'sessionId': 'foo'}),
        function(err) {
          return err instanceof error.InvalidArgumentError
              && 'Missing required parameter: id' === err.message;
        });
    });

    it('does not match on segments that do not start with a colon', function() {
      assert.equal(
          http.buildPath('/session/foo:bar/baz', {}),
          '/session/foo:bar/baz');
    });
  });

  describe('Executor', function() {
    let executor;
    let client;
    let send;

    beforeEach(function setUp() {
      client = new http.Client;
      send = sinon.stub(client, 'send');
      executor = new http.Executor(client);
    });

    describe('command routing', function() {
      it('rejects unrecognized commands', function() {
        return executor.execute(new Command('fake-name'))
            .then(assert.fail, err => {
              if (err instanceof error.UnknownCommandError
                  && 'Unrecognized command: fake-name' === err.message) {
                return;
              }
              throw err;
            })
      });

      it('rejects promise if client fails to send request', function() {
        let error = new Error('boom');
        send.returns(Promise.reject(error));
        return assertFailsToSend(new Command(CommandName.NEW_SESSION))
            .then(function(e) {
               assert.strictEqual(error, e);
               assertSent(
                   'POST', '/session', {},
                   [['Accept', 'application/json; charset=utf-8']]);
            });
      });

      it('can execute commands with no URL parameters', function() {
        var resp = JSON.stringify({sessionId: 'abc123'});
        send.returns(Promise.resolve(new http.Response(200, {}, resp)));

        let command = new Command(CommandName.NEW_SESSION);
        return assertSendsSuccessfully(command).then(function(response) {
           assertSent(
               'POST', '/session', {},
               [['Accept', 'application/json; charset=utf-8']]);
        });
      });

      it('rejects commands missing URL parameters', function() {
        let command =
            new Command(CommandName.FIND_CHILD_ELEMENT).
                setParameter('sessionId', 's123').
                // Let this be missing: setParameter('id', {'ELEMENT': 'e456'}).
                setParameter('using', 'id').
                setParameter('value', 'foo');

        assert.throws(
            () => executor.execute(command),
            function(err) {
              return err instanceof error.InvalidArgumentError
                  && 'Missing required parameter: id' === err.message;
            });
        assert.ok(!send.called);
      });

      it('replaces URL parameters with command parameters', function() {
        var command = new Command(CommandName.GET).
            setParameter('sessionId', 's123').
            setParameter('url', 'http://www.google.com');

        send.returns(Promise.resolve(new http.Response(200, {}, '')));

        return assertSendsSuccessfully(command).then(function(response) {
           assertSent(
               'POST', '/session/s123/url', {'url': 'http://www.google.com'},
               [['Accept', 'application/json; charset=utf-8']]);
        });
      });

      describe('uses correct URL', function() {
        beforeEach(() => executor = new http.Executor(client));

        describe('in legacy mode', function() {
          test(CommandName.GET_WINDOW_SIZE, {sessionId:'s123'}, false,
               'GET', '/session/s123/window/current/size');

          test(CommandName.SET_WINDOW_SIZE,
               {sessionId:'s123', width: 1, height: 1}, false,
               'POST', '/session/s123/window/current/size',
               {width: 1, height: 1});

          test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, false,
               'POST', '/session/s123/window/current/maximize');

          // This is consistent b/w legacy and W3C, just making sure.
          test(CommandName.GET,
               {sessionId:'s123', url: 'http://www.example.com'}, false,
               'POST', '/session/s123/url', {url: 'http://www.example.com'});
        });

        describe('in W3C mode', function() {
          test(CommandName.GET_WINDOW_SIZE,
               {sessionId:'s123'}, true,
               'GET', '/session/s123/window/size');

          test(CommandName.SET_WINDOW_SIZE,
               {sessionId:'s123', width: 1, height: 1}, true,
               'POST', '/session/s123/window/size', {width: 1, height: 1});

          test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, true,
               'POST', '/session/s123/window/maximize');

          // This is consistent b/w legacy and W3C, just making sure.
          test(CommandName.GET,
               {sessionId:'s123', url: 'http://www.example.com'}, true,
               'POST', '/session/s123/url', {url: 'http://www.example.com'});
        });

        function test(command, parameters, w3c,
            expectedMethod, expectedUrl, opt_expectedParams) {
          it(`command=${command}`, function() {
            var resp = JSON.stringify({sessionId: 'abc123'});
            send.returns(Promise.resolve(new http.Response(200, {}, resp)));

            let cmd = new Command(command).setParameters(parameters);
            executor.w3c = w3c;
            return executor.execute(cmd).then(function() {
               assertSent(
                   expectedMethod, expectedUrl, opt_expectedParams || {},
                   [['Accept', 'application/json; charset=utf-8']]);
            });
          });
        }
      });
    });

    describe('response parsing', function() {
      it('extracts value from JSON response', function() {
        var responseObj = {
          'status': error.ErrorCode.SUCCESS,
          'value': 'http://www.google.com'
        };

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

        send.returns(Promise.resolve(
            new http.Response(200, {}, JSON.stringify(responseObj))));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, 'http://www.google.com');
        });
      });

      describe('extracts Session from NEW_SESSION response', function() {
        beforeEach(() => executor = new http.Executor(client));

        const command = new Command(CommandName.NEW_SESSION);

        describe('fails if server returns invalid response', function() {
          describe('(empty response)', function() {
            test(true);
            test(false);

            function test(w3c) {
              it('w3c === ' + w3c, function() {
                send.returns(Promise.resolve(new http.Response(200, {}, '')));
                executor.w3c = w3c;
                return executor.execute(command).then(
                    () => assert.fail('expected to fail'),
                    (e) => {
                      if (!e.message.startsWith('Unable to parse')) {
                        throw e;
                      }
                    });
              });
            }
          });

          describe('(no session ID)', function() {
            test(true);
            test(false);

            function test(w3c) {
              it('w3c === ' + w3c, function() {
                let resp = {value:{name: 'Bob'}};
                send.returns(Promise.resolve(
                    new http.Response(200, {}, JSON.stringify(resp))));
                executor.w3c = w3c;
                return executor.execute(command).then(
                    () => assert.fail('expected to fail'),
                    (e) => {
                      if (!e.message.startsWith('Unable to parse')) {
                        throw e;
                      }
                    });
              });
            }
          });
        });

        it('handles legacy response', function() {
          var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}};

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          assert.ok(!executor.w3c);
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');

            let caps = response.getCapabilities();
            assert.ok(caps instanceof Capabilities);
            assert.equal(caps.get('name'), 'Bob');

            assert.ok(!executor.w3c);
          });
        });

        it('auto-upgrades on W3C response', function() {
          let rawResponse = {
            value: {
              sessionId: 's123',
              value: {
                name: 'Bob'
              }
            }
          };

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          assert.ok(!executor.w3c);
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');

            let caps = response.getCapabilities();
            assert.ok(caps instanceof Capabilities);
            assert.equal(caps.get('name'), 'Bob');

            assert.ok(executor.w3c);
          });
        });

        it('if w3c, does not downgrade on legacy response', function() {
          var rawResponse = {sessionId: 's123', status: 0, value: null};

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          executor.w3c = true;
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');
            assert.equal(response.getCapabilities().size, 0);
            assert.ok(executor.w3c, 'should never downgrade');
          });
        });

        it('handles legacy new session failures', function() {
          let rawResponse = {
            status: error.ErrorCode.NO_SUCH_ELEMENT,
            value: {message: 'hi'}
          };

          send.returns(Promise.resolve(
              new http.Response(500, {}, JSON.stringify(rawResponse))));

          return executor.execute(command)
              .then(() => assert.fail('should have failed'),
                    e => {
                      assert.ok(e instanceof error.NoSuchElementError);
                      assert.equal(e.message, 'hi');
                    });
        });

        it('handles w3c new session failures', function() {
          let rawResponse =
              {value: {error: 'no such element', message: 'oops'}};

          send.returns(Promise.resolve(
              new http.Response(500, {}, JSON.stringify(rawResponse))));

          return executor.execute(command)
              .then(() => assert.fail('should have failed'),
                    e => {
                      assert.ok(e instanceof error.NoSuchElementError);
                      assert.equal(e.message, 'oops');
                    });
        });
      });

      describe('extracts Session from DESCRIBE_SESSION response', function() {
        let command;

        beforeEach(function() {
          executor = new http.Executor(client);
          command = new Command(CommandName.DESCRIBE_SESSION)
              .setParameter('sessionId', 'foo');
        });

        describe('fails if server returns invalid response', function() {
          describe('(empty response)', function() {
            test(true);
            test(false);

            function test(w3c) {
              it('w3c === ' + w3c, function() {
                send.returns(Promise.resolve(new http.Response(200, {}, '')));
                executor.w3c = w3c;
                return executor.execute(command).then(
                    () => assert.fail('expected to fail'),
                    (e) => {
                      if (!e.message.startsWith('Unable to parse')) {
                        throw e;
                      }
                    });
              });
            }
          });

          describe('(no session ID)', function() {
            test(true);
            test(false);

            function test(w3c) {
              it('w3c === ' + w3c, function() {
                let resp = {value:{name: 'Bob'}};
                send.returns(Promise.resolve(
                    new http.Response(200, {}, JSON.stringify(resp))));
                executor.w3c = w3c;
                return executor.execute(command).then(
                    () => assert.fail('expected to fail'),
                    (e) => {
                      if (!e.message.startsWith('Unable to parse')) {
                        throw e;
                      }
                    });
              });
            }
          });
        });

        it('handles legacy response', function() {
          var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}};

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          assert.ok(!executor.w3c);
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');

            let caps = response.getCapabilities();
            assert.ok(caps instanceof Capabilities);
            assert.equal(caps.get('name'), 'Bob');

            assert.ok(!executor.w3c);
          });
        });

        it('does not auto-upgrade on W3C response', function() {
          var rawResponse = {value: {sessionId: 's123', value: {name: 'Bob'}}};

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          assert.ok(!executor.w3c);
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');

            let caps = response.getCapabilities();
            assert.ok(caps instanceof Capabilities);
            assert.equal(caps.get('name'), 'Bob');

            assert.ok(!executor.w3c);
          });
        });

        it('if w3c, does not downgrade on legacy response', function() {
          var rawResponse = {sessionId: 's123', status: 0, value: null};

          send.returns(Promise.resolve(
              new http.Response(200, {}, JSON.stringify(rawResponse))));

          executor.w3c = true;
          return executor.execute(command).then(function(response) {
            assert.ok(response instanceof Session);
            assert.equal(response.getId(), 's123');
            assert.equal(response.getCapabilities().size, 0);
            assert.ok(executor.w3c, 'should never downgrade');
          });
        });
      });

      it('handles JSON null', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(new http.Response(200, {}, 'null')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, null);
        });
      });

      describe('falsy values', function() {
        test(0);
        test(false);
        test('');

        function test(value) {
          it(`value=${value}`, function() {
            var command = new Command(CommandName.GET_CURRENT_URL)
                .setParameter('sessionId', 's123');

            send.returns(Promise.resolve(
                new http.Response(200, {},
                    JSON.stringify({status: 0, value: value}))));

            return executor.execute(command).then(function(response) {
               assertSent('GET', '/session/s123/url', {},
                   [['Accept', 'application/json; charset=utf-8']]);
               assert.strictEqual(response, value);
            });
          });
        }
      });

      it('handles non-object JSON', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(new http.Response(200, {}, '123')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, 123);
        });
      });

      it('returns body text when 2xx but not JSON', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(
            new http.Response(200, {}, 'hello, world\r\ngoodbye, world!')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, 'hello, world\ngoodbye, world!');
        });
      });

      it('returns body text when 2xx but invalid JSON', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(
            new http.Response(200, {}, '[')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, '[');
        });
      });

      it('returns null if no body text and 2xx', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(new http.Response(200, {}, '')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, null);
        });
      });

      it('returns normalized body text when 2xx but not JSON', function() {
        var command = new Command(CommandName.GET_CURRENT_URL)
            .setParameter('sessionId', 's123');

        send.returns(Promise.resolve(new http.Response(200, {}, '\r\n\n\n\r\n')));

        return executor.execute(command).then(function(response) {
           assertSent('GET', '/session/s123/url', {},
               [['Accept', 'application/json; charset=utf-8']]);
           assert.strictEqual(response, '\n\n\n\n');
        });
      });

      it('throws UnsupportedOperationError for 404 and body not JSON',
          function() {
            var command = new Command(CommandName.GET_CURRENT_URL)
                .setParameter('sessionId', 's123');

            send.returns(Promise.resolve(
                new http.Response(404, {}, 'hello, world\r\ngoodbye, world!')));

            return executor.execute(command)
                .then(
                    () => assert.fail('should have failed'),
                    checkError(
                        error.UnsupportedOperationError,
                        'hello, world\ngoodbye, world!'));
          });

      it('throws WebDriverError for generic 4xx when body not JSON',
          function() {
            var command = new Command(CommandName.GET_CURRENT_URL)
                .setParameter('sessionId', 's123');

            send.returns(Promise.resolve(
                new http.Response(500, {}, 'hello, world\r\ngoodbye, world!')));

            return executor.execute(command)
                .then(
                    () => assert.fail('should have failed'),
                    checkError(
                        error.WebDriverError,
                        'hello, world\ngoodbye, world!'))
                .then(function() {
                   assertSent('GET', '/session/s123/url', {},
                       [['Accept', 'application/json; charset=utf-8']]);
                });
          });
    });

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

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

      send.returns(Promise.resolve(new http.Response(200, {}, '')));

      return assertSendsSuccessfully(command).then(function(response) {
         assertSent('GET', '/person/Bob', {},
             [['Accept', 'application/json; charset=utf-8']]);
      });
    });

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

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

      send.returns(Promise.resolve(new http.Response(200, {}, '')));

      return assertSendsSuccessfully(command).then(function(response) {
         assertSent('POST', '/custom/back', {'times': 3},
             [['Accept', 'application/json; charset=utf-8']]);
      });
    });

    it('accepts promised http clients', function() {
      executor = new http.Executor(Promise.resolve(client));

      var resp = JSON.stringify({sessionId: 'abc123'});
      send.returns(Promise.resolve(new http.Response(200, {}, resp)));

      let command = new Command(CommandName.NEW_SESSION);
      return executor.execute(command).then(response => {
         assertSent(
             'POST', '/session', {},
             [['Accept', 'application/json; charset=utf-8']]);
      });
    });

    function entries(map) {
      let entries = [];
      for (let e of map.entries()) {
        entries.push(e);
      }
      return entries;
    }

    function checkError(type, message) {
      return function(e) {
        if (e instanceof type) {
          assert.strictEqual(e.message, message);
        } else {
          throw e;
        }
      };
    }

    function assertSent(method, path, data, headers) {
      assert.ok(send.calledWith(sinon.match(function(value) {
        assert.equal(value.method, method);
        assert.equal(value.path, path);
        assert.deepEqual(value.data, data);
        assert.deepEqual(entries(value.headers), headers);
        return true;
      })));
    }

    function assertSendsSuccessfully(command) {
      return executor.execute(command).then(function(response) {
        return response;
      });
    }

    function assertFailsToSend(command, opt_onError) {
      return executor.execute(command).then(
          () => {throw Error('should have failed')},
          (e) => {return e});
    }
  });
});