/* eslint-env mocha */
/* globals chai assert FileReaderSync assert WHATWGFetch */
var IEorEdge = /Edge\//.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent)
var Chrome = /Chrome\//.test(navigator.userAgent) && !IEorEdge
var Safari = /Safari\//.test(navigator.userAgent) && !IEorEdge && !Chrome

var support = {
  url: (function(url) {
    try {
      return new URL(url).toString() === url
    } catch (e) {
      return false
    }
  })('http://example.com/'),
  blob:
    'FileReader' in self &&
    'Blob' in self &&
    (function() {
      try {
        new Blob()
        return true
      } catch (e) {
        return false
      }
    })(),
  formData: 'FormData' in self,
  arrayBuffer: 'ArrayBuffer' in self,
  aborting: 'signal' in new Request(''),
  permanentRedirect: !/Trident/.test(navigator.userAgent)
}

function readBlobAsText(blob) {
  if ('FileReader' in self) {
    return new Promise(function(resolve, reject) {
      var reader = new FileReader()
      reader.onload = function() {
        resolve(reader.result)
      }
      reader.onerror = function() {
        reject(reader.error)
      }
      reader.readAsText(blob)
    })
  } else if ('FileReaderSync' in self) {
    return new FileReaderSync().readAsText(blob)
  } else {
    throw new ReferenceError('FileReader is not defined')
  }
}

function readBlobAsBytes(blob) {
  if ('FileReader' in self) {
    return new Promise(function(resolve, reject) {
      var reader = new FileReader()
      reader.onload = function() {
        var view = new Uint8Array(reader.result)
        resolve(Array.prototype.slice.call(view))
      }
      reader.onerror = function() {
        reject(reader.error)
      }
      reader.readAsArrayBuffer(blob)
    })
  } else if ('FileReaderSync' in self) {
    return new FileReaderSync().readAsArrayBuffer(blob)
  } else {
    throw new ReferenceError('FileReader is not defined')
  }
}

function arrayBufferFromText(text) {
  var buf = new ArrayBuffer(text.length)
  var view = new Uint8Array(buf)

  for (var i = 0; i < text.length; i++) {
    view[i] = text.charCodeAt(i)
  }
  return buf
}

function readArrayBufferAsText(buf) {
  var view = new Uint8Array(buf)
  var chars = new Array(view.length)

  for (var i = 0; i < view.length; i++) {
    chars[i] = String.fromCharCode(view[i])
  }
  return chars.join('')
}

var preservedGlobals = {}
var keepGlobals = ['fetch', 'Headers', 'Request', 'Response']
var exercise = ['polyfill']

// If native fetch implementation exists, replace it with the polyfilled
// version at first. The native implementation will be restored before the
// additional `native` pass of the test suite.
if (!self.fetch.polyfill) {
  keepGlobals.forEach(function(name) {
    preservedGlobals[name] = self[name]
    self[name] = WHATWGFetch[name]
  })
  exercise.push('native')
}

var slice = Array.prototype.slice

function featureDependent(testOrSuite, condition) {
  (condition ? testOrSuite : testOrSuite.skip).apply(this, slice.call(arguments, 2))
}

exercise.forEach(function(exerciseMode) {
  suite(exerciseMode, function() {
    if (exerciseMode === 'native') {
      suiteSetup(function() {
        keepGlobals.forEach(function(name) {
          self[name] = preservedGlobals[name]
        })
      })
    }

    var nativeChrome = Chrome && exerciseMode === 'native'
    var nativeSafari = Safari && exerciseMode === 'native'
    var nativeEdge = /Edge\//.test(navigator.userAgent) && exerciseMode === 'native'
    var firefox = navigator.userAgent.match(/Firefox\/(\d+)/)
    var brokenFF = firefox && firefox[1] <= 56 && exerciseMode === 'native'
    var emptyDefaultStatusText =
      exerciseMode !== 'native' || (exerciseMode === 'native' && (Chrome || (firefox && firefox[1] >= 67)))
    var polyfillFirefox = firefox && exerciseMode === 'polyfill'
    var omitSafari =
      Safari && exerciseMode === 'native' && navigator.userAgent.match(/Version\/(\d+\.\d+)/)[1] <= '11.1'

    // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
    function testBodyExtract(factory) {
      suite('body extract', function() {
        var expected = 'Hello World!'
        var inputs = [['type USVString', expected]]
        if (support.blob) {
          inputs.push(['type Blob', new Blob([expected])])
        }
        if (support.arrayBuffer) {
          inputs = inputs.concat([
            ['type ArrayBuffer', arrayBufferFromText(expected)],
            ['type TypedArray', new Uint8Array(arrayBufferFromText(expected))],
            ['type DataView', new DataView(arrayBufferFromText(expected))]
          ])
        }

        inputs.forEach(function(input) {
          var typeLabel = input[0],
            body = input[1]

          suite(typeLabel, function() {
            featureDependent(test, support.blob, 'consume as blob', function() {
              var r = factory(body)
              return r
                .blob()
                .then(readBlobAsText)
                .then(function(text) {
                  assert.equal(text, expected)
                })
            })

            test('consume as text', function() {
              var r = factory(body)
              return r.text().then(function(text) {
                assert.equal(text, expected)
              })
            })

            featureDependent(test, support.arrayBuffer, 'consume as array buffer', function() {
              var r = factory(body)
              return r
                .arrayBuffer()
                .then(readArrayBufferAsText)
                .then(function(text) {
                  assert.equal(text, expected)
                })
            })
          })
        })
      })
    }

    // https://fetch.spec.whatwg.org/#headers-class
    suite('Headers', function() {
      test('constructor copies headers', function() {
        var original = new Headers()
        original.append('Accept', 'application/json')
        original.append('Accept', 'text/plain')
        original.append('Content-Type', 'text/html')

        var headers = new Headers(original)
        assert.equal(headers.get('Accept'), 'application/json, text/plain')
        assert.equal(headers.get('Content-type'), 'text/html')
      })
      test('constructor works with arrays', function() {
        var array = [
          ['Content-Type', 'text/xml'],
          ['Breaking-Bad', '<3']
        ]
        var headers = new Headers(array)

        assert.equal(headers.get('Content-Type'), 'text/xml')
        assert.equal(headers.get('Breaking-Bad'), '<3')
        assert.throws(function() {
          new Headers([
            ['Content-Type'],
          ])
        }, TypeError)
        assert.throws(function() {
          new Headers([
            ['Content-Type', 'a', 'b'],
          ])
        }, TypeError)
      })
      test('headers are case insensitive', function() {
        var headers = new Headers({Accept: 'application/json'})
        assert.equal(headers.get('ACCEPT'), 'application/json')
        assert.equal(headers.get('Accept'), 'application/json')
        assert.equal(headers.get('accept'), 'application/json')
      })
      test('appends to existing', function() {
        var headers = new Headers({Accept: 'application/json'})
        assert.isFalse(headers.has('Content-Type'))
        headers.append('Content-Type', 'application/json')
        assert.isTrue(headers.has('Content-Type'))
        assert.equal(headers.get('Content-Type'), 'application/json')
      })
      test('appends values to existing header name', function() {
        var headers = new Headers({Accept: 'application/json'})
        headers.append('Accept', 'text/plain')
        assert.equal(headers.get('Accept'), 'application/json, text/plain')
      })
      test('sets header name and value', function() {
        var headers = new Headers()
        headers.set('Content-Type', 'application/json')
        assert.equal(headers.get('Content-Type'), 'application/json')
      })
      test('returns null on no header found', function() {
        var headers = new Headers()
        assert.isNull(headers.get('Content-Type'))
      })
      test('has headers that are set', function() {
        var headers = new Headers()
        headers.set('Content-Type', 'application/json')
        assert.isTrue(headers.has('Content-Type'))
      })
      test('deletes headers', function() {
        var headers = new Headers()
        headers.set('Content-Type', 'application/json')
        assert.isTrue(headers.has('Content-Type'))
        headers.delete('Content-Type')
        assert.isFalse(headers.has('Content-Type'))
        assert.isNull(headers.get('Content-Type'))
      })
      test('converts field name to string on set and get', function() {
        var headers = new Headers()
        headers.set(1, 'application/json')
        assert.isTrue(headers.has('1'))
        assert.equal(headers.get(1), 'application/json')
      })
      test('converts field value to string on set and get', function() {
        var headers = new Headers()
        headers.set('Content-Type', 1)
        headers.set('X-CSRF-Token', undefined)
        assert.equal(headers.get('Content-Type'), '1')
        assert.equal(headers.get('X-CSRF-Token'), 'undefined')
      })
      test('throws TypeError on invalid character in field name', function() {
        assert.throws(function() {
          new Headers({'[Accept]': 'application/json'})
        }, TypeError)
        assert.throws(function() {
          new Headers({'Accept:': 'application/json'})
        }, TypeError)
        assert.throws(function() {
          var headers = new Headers()
          headers.set({field: 'value'}, 'application/json')
        }, TypeError)
        assert.throws(function() {
          new Headers({'': 'application/json'})
        }, TypeError)
      })
      featureDependent(test, !brokenFF, 'is iterable with forEach', function() {
        var headers = new Headers()
        headers.append('Accept', 'application/json')
        headers.append('Accept', 'text/plain')
        headers.append('Content-Type', 'text/html')

        var results = []
        headers.forEach(function(value, key, object) {
          results.push({value: value, key: key, object: object})
        })

        assert.equal(results.length, 2)
        assert.deepEqual({key: 'accept', value: 'application/json, text/plain', object: headers}, results[0])
        assert.deepEqual({key: 'content-type', value: 'text/html', object: headers}, results[1])
      })
      test('forEach accepts second thisArg argument', function() {
        var headers = new Headers({Accept: 'application/json'})
        var thisArg = 42
        headers.forEach(function() {
          assert.equal(this, thisArg)
        }, thisArg)
      })
      featureDependent(test, !brokenFF, 'is iterable with keys', function() {
        var headers = new Headers()
        headers.append('Accept', 'application/json')
        headers.append('Accept', 'text/plain')
        headers.append('Content-Type', 'text/html')

        var iterator = headers.keys()
        assert.deepEqual({done: false, value: 'accept'}, iterator.next())
        assert.deepEqual({done: false, value: 'content-type'}, iterator.next())
        assert.deepEqual({done: true, value: undefined}, iterator.next())
      })
      featureDependent(test, !brokenFF, 'is iterable with values', function() {
        var headers = new Headers()
        headers.append('Accept', 'application/json')
        headers.append('Accept', 'text/plain')
        headers.append('Content-Type', 'text/html')

        var iterator = headers.values()
        assert.deepEqual({done: false, value: 'application/json, text/plain'}, iterator.next())
        assert.deepEqual({done: false, value: 'text/html'}, iterator.next())
        assert.deepEqual({done: true, value: undefined}, iterator.next())
      })
      featureDependent(test, !brokenFF, 'is iterable with entries', function() {
        var headers = new Headers()
        headers.append('Accept', 'application/json')
        headers.append('Accept', 'text/plain')
        headers.append('Content-Type', 'text/html')

        var iterator = headers.entries()
        assert.deepEqual({done: false, value: ['accept', 'application/json, text/plain']}, iterator.next())
        assert.deepEqual({done: false, value: ['content-type', 'text/html']}, iterator.next())
        assert.deepEqual({done: true, value: undefined}, iterator.next())
      })
    })

    // https://fetch.spec.whatwg.org/#request-class
    suite('Request', function() {
      test('called as normal function', function() {
        assert.throws(function() {
          Request('https://fetch.spec.whatwg.org/')
        })
      })
      test('construct with string url', function() {
        var request = new Request('https://fetch.spec.whatwg.org/')
        assert.equal(request.url, 'https://fetch.spec.whatwg.org/')
      })

      featureDependent(test, support.url, 'construct with URL instance', function() {
        var url = new URL('https://fetch.spec.whatwg.org/')
        url.pathname = 'cors'
        var request = new Request(url)
        assert.equal(request.url, 'https://fetch.spec.whatwg.org/cors')
      })

      test('construct with non-Request object', function() {
        var url = {
          toString: function() {
            return 'https://fetch.spec.whatwg.org/'
          }
        }
        var request = new Request(url)
        assert.equal(request.url, 'https://fetch.spec.whatwg.org/')
      })

      test('construct with Request', function() {
        var request1 = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out',
          headers: {
            accept: 'application/json',
            'Content-Type': 'text/plain'
          }
        })
        var request2 = new Request(request1)

        return request2.text().then(function(body2) {
          assert.equal(body2, 'I work out')
          assert.equal(request2.method, 'POST')
          assert.equal(request2.url, 'https://fetch.spec.whatwg.org/')
          assert.equal(request2.headers.get('accept'), 'application/json')
          assert.equal(request2.headers.get('content-type'), 'text/plain')

          return request1.text().then(
            function() {
              assert(false, 'original request body should have been consumed')
            },
            function(error) {
              assert(error instanceof TypeError, 'expected TypeError for already read body')
            }
          )
        })
      })

      test('construct with Request and override headers', function() {
        var request1 = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out',
          headers: {
            accept: 'application/json',
            'X-Request-ID': '123'
          }
        })
        var request2 = new Request(request1, {
          headers: {'x-test': '42'}
        })

        assert.equal(request2.headers.get('accept'), undefined)
        assert.equal(request2.headers.get('x-request-id'), undefined)
        assert.equal(request2.headers.get('x-test'), '42')
      })

      test('construct with Request and override body', function() {
        var request1 = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out',
          headers: {
            'Content-Type': 'text/plain'
          }
        })
        var request2 = new Request(request1, {
          body: '{"wiggles": 5}',
          headers: {'Content-Type': 'application/json'}
        })

        return request2.json().then(function(data) {
          assert.equal(data.wiggles, 5)
          assert.equal(request2.headers.get('content-type'), 'application/json')
        })
      })

      featureDependent(test, !nativeChrome, 'construct with used Request body', function() {
        var request1 = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out'
        })

        return request1.text().then(function() {
          assert.throws(function() {
            new Request(request1)
          }, TypeError)
        })
      })

      test('GET should not have implicit Content-Type', function() {
        var req = new Request('https://fetch.spec.whatwg.org/')
        assert.equal(req.headers.get('content-type'), undefined)
      })

      test('POST with blank body should not have implicit Content-Type', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post'
        })
        assert.equal(req.headers.get('content-type'), undefined)
      })

      test('construct with string body sets Content-Type header', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out'
        })

        assert.equal(req.headers.get('content-type'), 'text/plain;charset=UTF-8')
      })

      featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: new Blob(['test'], {type: 'image/png'})
        })

        assert.equal(req.headers.get('content-type'), 'image/png')
      })

      test('construct with body and explicit header uses header', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          headers: {'Content-Type': 'image/png'},
          body: 'I work out'
        })

        assert.equal(req.headers.get('content-type'), 'image/png')
      })

      featureDependent(test, support.blob, 'construct with Blob body and explicit Content-Type header', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          headers: {'Content-Type': 'image/png'},
          body: new Blob(['test'], {type: 'text/plain'})
        })

        assert.equal(req.headers.get('content-type'), 'image/png')
      })

      featureDependent(test, !IEorEdge, 'construct with URLSearchParams body sets Content-Type header', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: new URLSearchParams('a=1&b=2')
        })

        assert.equal(req.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8')
      })

      featureDependent(
        test,
        !IEorEdge,
        'construct with URLSearchParams body and explicit Content-Type header',
        function() {
          var req = new Request('https://fetch.spec.whatwg.org/', {
            method: 'post',
            headers: {'Content-Type': 'image/png'},
            body: new URLSearchParams('a=1&b=2')
          })

          assert.equal(req.headers.get('content-type'), 'image/png')
        }
      )

      test('construct with unsupported body type', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: {}
        })

        assert.equal(req.headers.get('content-type'), 'text/plain;charset=UTF-8')
        return req.text().then(function(bodyText) {
          assert.equal(bodyText, '[object Object]')
        })
      })

      test('construct with null body', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post'
        })

        assert.isNull(req.headers.get('content-type'))
        return req.text().then(function(bodyText) {
          assert.equal(bodyText, '')
        })
      })

      test('clone GET request', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          headers: {'content-type': 'text/plain'}
        })
        var clone = req.clone()

        assert.equal(clone.url, req.url)
        assert.equal(clone.method, 'GET')
        assert.equal(clone.headers.get('content-type'), 'text/plain')
        assert.notEqual(clone.headers, req.headers)
        assert.isFalse(req.bodyUsed)
      })

      test('clone POST request', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          headers: {'content-type': 'text/plain'},
          body: 'I work out'
        })
        var clone = req.clone()

        assert.equal(clone.method, 'POST')
        assert.equal(clone.headers.get('content-type'), 'text/plain')
        assert.notEqual(clone.headers, req.headers)
        assert.equal(req.bodyUsed, false)

        return Promise.all([clone.text(), req.clone().text()]).then(function(bodies) {
          assert.deepEqual(bodies, ['I work out', 'I work out'])
        })
      })

      featureDependent(test, !nativeChrome, 'clone with used Request body', function() {
        var req = new Request('https://fetch.spec.whatwg.org/', {
          method: 'post',
          body: 'I work out'
        })

        return req.text().then(function() {
          assert.throws(function() {
            req.clone()
          }, TypeError)
        })
      })

      testBodyExtract(function(body) {
        return new Request('', {method: 'POST', body: body})
      })

      featureDependent(test, !omitSafari, 'credentials defaults to same-origin', function() {
        var request = new Request('')
        assert.equal(request.credentials, 'same-origin')
      })

      test('credentials is overridable', function() {
        var request = new Request('', {credentials: 'omit'})
        assert.equal(request.credentials, 'omit')
      })
    })

    // https://fetch.spec.whatwg.org/#response-class
    suite('Response', function() {
      featureDependent(test, emptyDefaultStatusText, 'default status is 200', function() {
        var res = new Response()
        assert.equal(res.status, 200)
        assert.equal(res.statusText, '')
        assert.isTrue(res.ok)
      })

      featureDependent(
        test,
        emptyDefaultStatusText,
        'default status is 200 when an explicit undefined status code is passed',
        function() {
          var res = new Response('', {status: undefined})
          assert.equal(res.status, 200)
          assert.equal(res.statusText, '')
          assert.isTrue(res.ok)
        }
      )

      testBodyExtract(function(body) {
        return new Response(body)
      })

      test('called as normal function', function() {
        assert.throws(function() {
          Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}})
        })
      })
      test('status outside inclusive range 200-599 ', function() {
        assert.throws(function() {
          new Response('', {status: 199})
        })
        for (var i = 0; i < 200; i++) {
          assert.throws(function() {
            new Response('', {status: i})
          })
        }
        for (i = 999; i > 599; i--) {
          assert.throws(function() {
            new Response('', {status: i})
          })
        }
        // A fetch with the url of a `file://` scheme may have a status 0 or
        // similar in some operating systems. In the event that a status is found outside 
        // the standard range of 200-599, and the url start with `file://`
        // the status should return 200
        assert.doesNotThrow(function() {
          new Request('', {status: 0, request: {url: 'file://path/to/local/file'}})
        })
      })

      test('creates Headers object from raw headers', function() {
        var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}})
        assert.equal(r.headers instanceof Headers, true)
        return r.json().then(function(json) {
          assert.equal(json.foo, 'bar')
          return json
        })
      })

      test('always creates a new Headers instance', function() {
        var headers = new Headers({'x-hello': 'world'})
        var res = new Response('', {headers: headers})

        assert.equal(res.headers.get('x-hello'), 'world')
        assert.notEqual(res.headers, headers)
      })

      test('clone text response', function() {
        var res = new Response('{"foo":"bar"}', {
          headers: {'content-type': 'application/json'}
        })
        var clone = res.clone()

        assert.notEqual(clone.headers, res.headers, 'headers were cloned')
        assert.equal(clone.headers.get('content-type'), 'application/json')

        return Promise.all([clone.json(), res.json()]).then(function(jsons) {
          assert.deepEqual(jsons[0], jsons[1], 'json of cloned object is the same as original')
        })
      })

      featureDependent(test, support.blob, 'clone blob response', function() {
        var req = new Request(new Blob(['test']))
        req.clone()
        assert.equal(req.bodyUsed, false)
      })

      test('error creates error Response', function() {
        var r = Response.error()
        assert(r instanceof Response)
        assert.equal(r.ok, false)
        assert.equal(r.status, 0)
        assert.equal(r.statusText, '')
        assert.equal(r.type, 'error')
      })

      test('redirect creates redirect Response', function() {
        var r = Response.redirect('https://fetch.spec.whatwg.org/', 301)
        assert(r instanceof Response)
        assert.equal(r.status, 301)
        assert.equal(r.headers.get('Location'), 'https://fetch.spec.whatwg.org/')
      })

      test('construct with string body sets Content-Type header', function() {
        var r = new Response('I work out')
        assert.equal(r.headers.get('content-type'), 'text/plain;charset=UTF-8')
      })

      featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() {
        var r = new Response(new Blob(['test'], {type: 'text/plain'}))
        assert.equal(r.headers.get('content-type'), 'text/plain')
      })

      test('construct with body and explicit header uses header', function() {
        var r = new Response('I work out', {
          headers: {
            'Content-Type': 'text/plain'
          }
        })

        assert.equal(r.headers.get('content-type'), 'text/plain')
      })

      test('construct with undefined statusText', function() {
        var r = new Response('', {statusText: undefined})
        assert.equal(r.statusText, '')
      })

      test('construct with null statusText', function() {
        var r = new Response('', {statusText: null})
        assert.equal(r.statusText, 'null')
      })

      test('init object as first argument', function() {
        var r = new Response({
          status: 201,
          headers: {
            'Content-Type': 'text/html'
          }
        })

        assert.equal(r.status, 200)
        assert.equal(r.headers.get('content-type'), 'text/plain;charset=UTF-8')
        return r.text().then(function(bodyText) {
          assert.equal(bodyText, '[object Object]')
        })
      })

      test('null as first argument', function() {
        var r = new Response(null)

        assert.isNull(r.headers.get('content-type'))
        return r.text().then(function(bodyText) {
          assert.equal(bodyText, '')
        })
      })
    })

    // https://fetch.spec.whatwg.org/#body-mixin
    suite('Body mixin', function() {
      featureDependent(suite, support.blob, 'arrayBuffer', function() {
        test('resolves arrayBuffer promise', function() {
          return fetch('/hello')
            .then(function(response) {
              return response.arrayBuffer()
            })
            .then(function(buf) {
              assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
              assert.equal(buf.byteLength, 2)
            })
        })

        test('arrayBuffer handles binary data', function() {
          return fetch('/binary')
            .then(function(response) {
              return response.arrayBuffer()
            })
            .then(function(buf) {
              assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
              assert.equal(buf.byteLength, 256, 'buf.byteLength is correct')
              var view = new Uint8Array(buf)
              for (var i = 0; i < 256; i++) {
                assert.equal(view[i], i)
              }
            })
        })

        test('arrayBuffer handles utf-8 data', function() {
          return fetch('/hello/utf8')
            .then(function(response) {
              return response.arrayBuffer()
            })
            .then(function(buf) {
              assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
              assert.equal(buf.byteLength, 5, 'buf.byteLength is correct')
              var octets = Array.prototype.slice.call(new Uint8Array(buf))
              assert.deepEqual(octets, [104, 101, 108, 108, 111])
            })
        })

        test('arrayBuffer handles utf-16le data', function() {
          return fetch('/hello/utf16le')
            .then(function(response) {
              return response.arrayBuffer()
            })
            .then(function(buf) {
              assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
              assert.equal(buf.byteLength, 10, 'buf.byteLength is correct')
              var octets = Array.prototype.slice.call(new Uint8Array(buf))
              assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0])
            })
        })

        test('rejects arrayBuffer promise after body is consumed', function() {
          return fetch('/hello')
            .then(function(response) {
              assert.equal(response.bodyUsed, false)
              response.blob()
              assert.equal(response.bodyUsed, true)
              return response.arrayBuffer()
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Promise rejected after body consumed')
            })
        })
      })

      featureDependent(suite, support.blob, 'blob', function() {
        test('resolves blob promise', function() {
          return fetch('/hello')
            .then(function(response) {
              return response.blob()
            })
            .then(function(blob) {
              assert(blob instanceof Blob, 'blob is a Blob instance')
              assert.equal(blob.size, 2)
            })
        })

        test('blob handles binary data', function() {
          return fetch('/binary')
            .then(function(response) {
              return response.blob()
            })
            .then(function(blob) {
              assert(blob instanceof Blob, 'blob is a Blob instance')
              assert.equal(blob.size, 256, 'blob.size is correct')
            })
        })

        test('blob handles utf-8 data', function() {
          return fetch('/hello/utf8')
            .then(function(response) {
              return response.blob()
            })
            .then(readBlobAsBytes)
            .then(function(octets) {
              assert.equal(octets.length, 5, 'blob.size is correct')
              assert.deepEqual(octets, [104, 101, 108, 108, 111])
            })
        })

        test('blob handles utf-16le data', function() {
          return fetch('/hello/utf16le')
            .then(function(response) {
              return response.blob()
            })
            .then(readBlobAsBytes)
            .then(function(octets) {
              assert.equal(octets.length, 10, 'blob.size is correct')
              assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0])
            })
        })

        test('rejects blob promise after body is consumed', function() {
          return fetch('/hello')
            .then(function(response) {
              assert(response.blob, 'Body does not implement blob')
              assert.equal(response.bodyUsed, false)
              response.text()
              assert.equal(response.bodyUsed, true)
              return response.blob()
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Promise rejected after body consumed')
            })
        })
      })

      featureDependent(suite, support.formData, 'formData', function() {
        test('post sets content-type header', function() {
          return fetch('/request', {
            method: 'post',
            body: new FormData()
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(json) {
              assert.equal(json.method, 'POST')
              assert(/^multipart\/form-data;/.test(json.headers['content-type']))
            })
        })

        featureDependent(test, !nativeChrome && !nativeEdge, 'formData rejects after body was consumed', function() {
          return fetch('/json')
            .then(function(response) {
              assert(response.formData, 'Body does not implement formData')
              response.formData()
              return response.formData()
            })
            .catch(function(error) {
              if (error instanceof chai.AssertionError) {
                throw error
              } else {
                assert(error instanceof TypeError, 'Promise rejected after body consumed')
              }
            })
        })

        featureDependent(
          test,
          !nativeChrome && !nativeSafari && !nativeEdge,
          'parses form encoded response',
          function() {
            return fetch('/form')
              .then(function(response) {
                return response.formData()
              })
              .then(function(form) {
                assert(form instanceof FormData, 'Parsed a FormData object')
              })
          }
        )
      })

      suite('json', function() {
        test('parses json response', function() {
          return fetch('/json')
            .then(function(response) {
              return response.json()
            })
            .then(function(json) {
              assert.equal(json.name, 'Hubot')
              assert.equal(json.login, 'hubot')
            })
        })

        test('rejects json promise after body is consumed', function() {
          return fetch('/json')
            .then(function(response) {
              assert(response.json, 'Body does not implement json')
              assert.equal(response.bodyUsed, false)
              response.text()
              assert.equal(response.bodyUsed, true)
              return response.json()
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Promise rejected after body consumed')
            })
        })

        featureDependent(test, !polyfillFirefox, 'handles json parse error', function() {
          return fetch('/json-error')
            .then(function(response) {
              return response.json()
            })
            .catch(function(error) {
              if (!IEorEdge) assert(error instanceof Error, 'JSON exception is an Error instance')
              assert(error.message, 'JSON exception has an error message')
            })
        })
      })

      suite('text', function() {
        test('handles 204 No Content response', function() {
          return fetch('/empty')
            .then(function(response) {
              assert.equal(response.status, 204)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, '')
            })
        })

        test('resolves text promise', function() {
          return fetch('/hello')
            .then(function(response) {
              return response.text()
            })
            .then(function(text) {
              assert.equal(text, 'hi')
            })
        })

        test('rejects text promise after body is consumed', function() {
          return fetch('/hello')
            .then(function(response) {
              assert(response.text, 'Body does not implement text')
              assert.equal(response.bodyUsed, false)
              response.text()
              assert.equal(response.bodyUsed, true)
              return response.text()
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Promise rejected after body consumed')
            })
        })

        test('does not set bodyUsed to true if no body supplied', function() {
          var response = new Response();
          assert(response.text, 'Body does not implement text')
          assert.equal(response.bodyUsed, false)
          response.text()
          assert.equal(response.bodyUsed, false)
          return response.text()
        })
      })
    })

    suite('fetch method', function() {
      suite('promise resolution', function() {
        test('resolves promise on 500 error', function() {
          return fetch('/boom')
            .then(function(response) {
              assert.equal(response.status, 500)
              assert.equal(response.ok, false)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'boom')
            })
        })

        test.skip('rejects promise for network error', function() {
          return fetch('/error')
            .then(function(response) {
              assert(false, 'HTTP status ' + response.status + ' was treated as success')
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Rejected with Error')
            })
        })

        test('rejects when Request constructor throws', function() {
          return fetch('/request', {method: 'GET', body: 'invalid'})
            .then(function() {
              assert(false, 'Invalid Request init was accepted')
            })
            .catch(function(error) {
              assert(error instanceof TypeError, 'Rejected with Error')
            })
        })
      })

      suite('request', function() {
        test('sends headers', function() {
          return fetch('/request', {
            headers: {
              Accept: 'application/json',
              'X-Test': '42'
            }
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(json) {
              assert.equal(json.headers['accept'], 'application/json')
              assert.equal(json.headers['x-test'], '42')
            })
        })

        test('with Request as argument', function() {
          var request = new Request('/request', {
            headers: {
              Accept: 'application/json',
              'X-Test': '42'
            }
          })

          return fetch(request)
            .then(function(response) {
              return response.json()
            })
            .then(function(json) {
              assert.equal(json.headers['accept'], 'application/json')
              assert.equal(json.headers['x-test'], '42')
            })
        })

        test('reusing same Request multiple times', function() {
          var request = new Request('/request', {
            headers: {
              Accept: 'application/json',
              'X-Test': '42'
            }
          })

          var responses = []

          return fetch(request)
            .then(function(response) {
              responses.push(response)
              return fetch(request)
            })
            .then(function(response) {
              responses.push(response)
              return fetch(request)
            })
            .then(function(response) {
              responses.push(response)
              return Promise.all(
                responses.map(function(r) {
                  return r.json()
                })
              )
            })
            .then(function(jsons) {
              jsons.forEach(function(json) {
                assert.equal(json.headers['accept'], 'application/json')
                assert.equal(json.headers['x-test'], '42')
              })
            })
        })

        featureDependent(suite, support.arrayBuffer, 'ArrayBuffer', function() {
          test('ArrayBuffer body', function() {
            return fetch('/request', {
              method: 'post',
              body: arrayBufferFromText('name=Hubot')
            })
              .then(function(response) {
                return response.json()
              })
              .then(function(request) {
                assert.equal(request.method, 'POST')
                assert.equal(request.data, 'name=Hubot')
              })
          })

          test('DataView body', function() {
            return fetch('/request', {
              method: 'post',
              body: new DataView(arrayBufferFromText('name=Hubot'))
            })
              .then(function(response) {
                return response.json()
              })
              .then(function(request) {
                assert.equal(request.method, 'POST')
                assert.equal(request.data, 'name=Hubot')
              })
          })

          test('TypedArray body', function() {
            return fetch('/request', {
              method: 'post',
              body: new Uint8Array(arrayBufferFromText('name=Hubot'))
            })
              .then(function(response) {
                return response.json()
              })
              .then(function(request) {
                assert.equal(request.method, 'POST')
                assert.equal(request.data, 'name=Hubot')
              })
          })
        })

        featureDependent(test, !IEorEdge, 'sends URLSearchParams body', function() {
          return fetch('/request', {
            method: 'post',
            body: new URLSearchParams('a=1&b=2')
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'POST')
              assert.equal(request.data, 'a=1&b=2')
            })
        })
      })

      featureDependent(suite, exerciseMode !== 'native' || support.aborting, 'aborting', function() {
        test('Request init creates an AbortSignal without option', function() {
          var request = new Request('/request')
          assert.ok(request.signal);
          assert.equal(request.signal.aborted, false);
        })

        test('Request init passes AbortSignal from option', function () {
          var controller = new AbortController()
          var request = new Request('/request', {signal: controller.signal})
          assert.ok(request.signal);
          assert.deepEqual(controller.signal, request.signal);
        })

        test('initially aborted signal', function () {
          var controller = new AbortController()
          controller.abort()

          return fetch('/request', {
            signal: controller.signal
          }).then(
            function() {
              assert.ok(false)
            },
            function(error) {
              if (!IEorEdge) assert.instanceOf(error, WHATWGFetch.DOMException)
              assert.equal(error.name, 'AbortError')
            }
          )
        })

        test('initially aborted signal within Request', function() {
          var controller = new AbortController()
          controller.abort()

          var request = new Request('/request', {signal: controller.signal})

          return fetch(request).then(
            function() {
              assert.ok(false)
            },
            function(error) {
              assert.equal(error.name, 'AbortError')
            }
          )
        })

        test('mid-request', function() {
          var controller = new AbortController()

          setTimeout(function() {
            controller.abort()
          }, 30)

          return fetch('/slow?_=' + new Date().getTime(), {
            signal: controller.signal
          }).then(
            function() {
              assert.ok(false)
            },
            function(error) {
              assert.equal(error.name, 'AbortError')
            }
          )
        })

        test('mid-request within Request', function() {
          var controller = new AbortController()
          var request = new Request('/slow?_=' + new Date().getTime(), {signal: controller.signal})

          setTimeout(function() {
            controller.abort()
          }, 30)

          return fetch(request).then(
            function() {
              assert.ok(false)
            },
            function(error) {
              assert.equal(error.name, 'AbortError')
            }
          )
        })

        test('abort multiple with same signal', function() {
          var controller = new AbortController()

          setTimeout(function() {
            controller.abort()
          }, 30)

          return Promise.all([
            fetch('/slow?_=' + new Date().getTime(), {
              signal: controller.signal
            }).then(
              function() {
                assert.ok(false)
              },
              function(error) {
                assert.equal(error.name, 'AbortError')
              }
            ),
            fetch('/slow?_=' + new Date().getTime(), {
              signal: controller.signal
            }).then(
              function() {
                assert.ok(false)
              },
              function(error) {
                assert.equal(error.name, 'AbortError')
              }
            )
          ])
        })
      })

      suite('response', function() {
        test('populates body', function() {
          return fetch('/hello')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })

        test('parses headers', function() {
          return fetch('/headers?' + new Date().getTime()).then(function(response) {
            assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT')
            assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')
          })
        })
      })

      // https://fetch.spec.whatwg.org/#methods
      suite('HTTP methods', function() {
        test('supports HTTP GET', function() {
          return fetch('/request', {
            method: 'get'
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'GET')
              assert.equal(request.data, '')
            })
        })

        test('GET with body throws TypeError', function() {
          assert.throw(function() {
            new Request('', {
              method: 'get',
              body: 'invalid'
            })
          }, TypeError)
        })

        test('HEAD with body throws TypeError', function() {
          assert.throw(function() {
            new Request('', {
              method: 'head',
              body: 'invalid'
            })
          }, TypeError)
        })

        test('supports HTTP POST', function() {
          return fetch('/request', {
            method: 'post',
            body: 'name=Hubot'
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'POST')
              assert.equal(request.data, 'name=Hubot')
            })
        })

        test('supports HTTP PUT', function() {
          return fetch('/request', {
            method: 'put',
            body: 'name=Hubot'
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'PUT')
              assert.equal(request.data, 'name=Hubot')
            })
        })

        test('supports HTTP PATCH', function() {
          return fetch('/request', {
            method: 'PATCH',
            body: 'name=Hubot'
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'PATCH')
              assert.equal(request.data, 'name=Hubot')
            })
        })

        test('supports HTTP DELETE', function() {
          return fetch('/request', {
            method: 'delete'
          })
            .then(function(response) {
              return response.json()
            })
            .then(function(request) {
              assert.equal(request.method, 'DELETE')
              assert.equal(request.data, '')
            })
        })
      })

      // https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
      suite('Atomic HTTP redirect handling', function() {
        test('handles 301 redirect response', function() {
          return fetch('/redirect/301')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              assert.match(response.url, /\/hello/)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })

        test('handles 302 redirect response', function() {
          return fetch('/redirect/302')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              assert.match(response.url, /\/hello/)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })

        test('handles 303 redirect response', function() {
          return fetch('/redirect/303')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              assert.match(response.url, /\/hello/)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })

        test('handles 307 redirect response', function() {
          return fetch('/redirect/307')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              assert.match(response.url, /\/hello/)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })

        featureDependent(test, support.permanentRedirect, 'handles 308 redirect response', function() {
          return fetch('/redirect/308')
            .then(function(response) {
              assert.equal(response.status, 200)
              assert.equal(response.ok, true)
              assert.match(response.url, /\/hello/)
              return response.text()
            })
            .then(function(body) {
              assert.equal(body, 'hi')
            })
        })
      })

      // https://fetch.spec.whatwg.org/#concept-request-credentials-mode
      suite('credentials mode', function() {
        setup(function() {
          return fetch('/cookie?name=foo&value=reset', {credentials: 'same-origin'})
        })

        featureDependent(suite, exerciseMode === 'native', 'omit', function() {
          test('does not accept cookies with omit credentials', function() {
            return fetch('/cookie?name=foo&value=bar', {credentials: 'omit'})
              .then(function() {
                return fetch('/cookie?name=foo', {credentials: 'same-origin'})
              })
              .then(function(response) {
                return response.text()
              })
              .then(function(data) {
                assert.equal(data, 'reset')
              })
          })

          test('does not send cookies with omit credentials', function() {
            return fetch('/cookie?name=foo&value=bar')
              .then(function() {
                return fetch('/cookie?name=foo', {credentials: 'omit'})
              })
              .then(function(response) {
                return response.text()
              })
              .then(function(data) {
                assert.equal(data, '')
              })
          })
        })

        suite('same-origin', function() {
          test('send cookies with same-origin credentials', function() {
            return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'})
              .then(function() {
                return fetch('/cookie?name=foo', {credentials: 'same-origin'})
              })
              .then(function(response) {
                return response.text()
              })
              .then(function(data) {
                assert.equal(data, 'bar')
              })
          })
        })

        suite('include', function() {
          test('send cookies with include credentials', function() {
            return fetch('/cookie?name=foo&value=bar', {credentials: 'include'})
              .then(function() {
                return fetch('/cookie?name=foo', {credentials: 'include'})
              })
              .then(function(response) {
                return response.text()
              })
              .then(function(data) {
                assert.equal(data, 'bar')
              })
          })
        })
      })
    })
  })
})
