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

const assert = require('node:assert'),
  sinon = require('sinon')

const Capabilities = require('selenium-webdriver/lib/capabilities').Capabilities,
  Command = require('selenium-webdriver/lib/command').Command,
  CommandName = require('selenium-webdriver/lib/command').Name,
  error = require('selenium-webdriver/lib/error'),
  http = require('selenium-webdriver/lib/http'),
  Session = require('selenium-webdriver/lib/session').Session,
  WebElement = require('selenium-webdriver/lib/webdriver').WebElement

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

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

      const finalPath = http.buildPath('/session/:sessionId/element/:id/click', parameters)
      assert.strictEqual(finalPath, '/session/foo/element/bar/click')
      assert.deepStrictEqual(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.strictEqual(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 () {
        const 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', async 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')

        try {
          await executor.execute(command)
          return Promise.reject(Error('should have thrown'))
        } catch (err) {
          assert.strictEqual(err.constructor, error.InvalidArgumentError)
          assert.strictEqual(err.message, 'Missing required parameter: id')
        }
        assert.ok(!send.called)
      })

      it('replaces URL parameters with command parameters', function () {
        const 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 W3C mode', function () {
          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 () {
            const 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 () {
        const responseObj = {
          status: error.ErrorCode.SUCCESS,
          value: 'http://www.google.com',
        }

        const 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 () {
          const 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.strictEqual(response.getId(), 's123')

            let caps = response.getCapabilities()
            assert.ok(caps instanceof Capabilities)
            assert.strictEqual(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.strictEqual(response.getId(), 's123')

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

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

        it('if w3c, does not downgrade on legacy response', function () {
          const 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.strictEqual(response.getId(), 's123')
            assert.strictEqual(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.strictEqual(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.strictEqual(e.message, 'oops')
            },
          )
        })
      })

      it('handles JSON null', function () {
        const 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 () {
            const 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 () {
        const 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 () {
        const 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 () {
        const 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 () {
        const 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 () {
        const 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 () {
        const 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, 'getCurrentUrl: hello, world\ngoodbye, world!'),
          )
      })

      it('throws WebDriverError for generic 4xx when body not JSON', function () {
        const 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')

      const 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')

      const 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))

      const 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.strictEqual(value.method, method)
            assert.strictEqual(value.path, path)
            assert.deepStrictEqual(value.data, data)
            assert.deepStrictEqual(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
        },
      )
    }
  })
})
