const assert = require('assert')
const http = require('http')
const path = require('path')
const qs = require('querystring')
const {closeWindow} = require('./window-helpers')
const remote = require('electron').remote
const {BrowserWindow, ipcMain, protocol, session, webContents} = remote

describe('protocol module', function () {
  var protocolName = 'sp'
  var text = 'valar morghulis'
  var postData = {
    name: 'post test',
    type: 'string'
  }

  afterEach(function (done) {
    protocol.unregisterProtocol(protocolName, function () {
      protocol.uninterceptProtocol('http', function () {
        done()
      })
    })
  })

  describe('protocol.register(Any)Protocol', function () {
    var emptyHandler = function (request, callback) {
      callback()
    }

    it('throws error when scheme is already registered', function (done) {
      protocol.registerStringProtocol(protocolName, emptyHandler, function (error) {
        assert.equal(error, null)
        protocol.registerBufferProtocol(protocolName, emptyHandler, function (error) {
          assert.notEqual(error, null)
          done()
        })
      })
    })

    it('does not crash when handler is called twice', function (done) {
      var doubleHandler = function (request, callback) {
        try {
          callback(text)
          callback()
        } catch (error) {
          // Ignore error
        }
      }
      protocol.registerStringProtocol(protocolName, doubleHandler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sends error when callback is called with nothing', function (done) {
      protocol.registerBufferProtocol(protocolName, emptyHandler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            return done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            return done()
          }
        })
      })
    })

    it('does not crash when callback is called in next tick', function (done) {
      var handler = function (request, callback) {
        setImmediate(function () {
          callback(text)
        })
      }
      protocol.registerStringProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })
  })

  describe('protocol.unregisterProtocol', function () {
    it('returns error when scheme does not exist', function (done) {
      protocol.unregisterProtocol('not-exist', function (error) {
        assert.notEqual(error, null)
        done()
      })
    })
  })

  describe('protocol.registerStringProtocol', function () {
    it('sends string as response', function (done) {
      var handler = function (request, callback) {
        callback(text)
      }
      protocol.registerStringProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sets Access-Control-Allow-Origin', function (done) {
      var handler = function (request, callback) {
        callback(text)
      }
      protocol.registerStringProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data, status, request) {
            assert.equal(data, text)
            assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*')
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sends object as response', function (done) {
      var handler = function (request, callback) {
        callback({
          data: text,
          mimeType: 'text/html'
        })
      }
      protocol.registerStringProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('fails when sending object other than string', function (done) {
      var handler = function (request, callback) {
        callback(new Date())
      }
      protocol.registerBufferProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })
  })

  describe('protocol.registerBufferProtocol', function () {
    var buffer = new Buffer(text)

    it('sends Buffer as response', function (done) {
      var handler = function (request, callback) {
        callback(buffer)
      }
      protocol.registerBufferProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sets Access-Control-Allow-Origin', function (done) {
      var handler = function (request, callback) {
        callback(buffer)
      }

      protocol.registerBufferProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data, status, request) {
            assert.equal(data, text)
            assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*')
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sends object as response', function (done) {
      var handler = function (request, callback) {
        callback({
          data: buffer,
          mimeType: 'text/html'
        })
      }
      protocol.registerBufferProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('fails when sending string', function (done) {
      var handler = function (request, callback) {
        callback(text)
      }
      protocol.registerBufferProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })
  })

  describe('protocol.registerFileProtocol', function () {
    var filePath = path.join(__dirname, 'fixtures', 'asar', 'a.asar', 'file1')
    var fileContent = require('fs').readFileSync(filePath)
    var normalPath = path.join(__dirname, 'fixtures', 'pages', 'a.html')
    var normalContent = require('fs').readFileSync(normalPath)

    it('sends file path as response', function (done) {
      var handler = function (request, callback) {
        callback(filePath)
      }
      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, String(fileContent))
            return done()
          },
          error: function (xhr, errorType, error) {
            return done(error)
          }
        })
      })
    })

    it('sets Access-Control-Allow-Origin', function (done) {
      var handler = function (request, callback) {
        callback(filePath)
      }
      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data, status, request) {
            assert.equal(data, String(fileContent))
            assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*')
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sends object as response', function (done) {
      var handler = function (request, callback) {
        callback({
          path: filePath
        })
      }
      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, String(fileContent))
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('can send normal file', function (done) {
      var handler = function (request, callback) {
        callback(normalPath)
      }

      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, String(normalContent))
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('fails when sending unexist-file', function (done) {
      var fakeFilePath = path.join(__dirname, 'fixtures', 'asar', 'a.asar', 'not-exist')
      var handler = function (request, callback) {
        callback(fakeFilePath)
      }
      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })

    it('fails when sending unsupported content', function (done) {
      var handler = function (request, callback) {
        callback(new Date())
      }
      protocol.registerFileProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })
  })

  describe('protocol.registerHttpProtocol', function () {
    it('sends url as response', function (done) {
      var server = http.createServer(function (req, res) {
        assert.notEqual(req.headers.accept, '')
        res.end(text)
        server.close()
      })
      server.listen(0, '127.0.0.1', function () {
        var port = server.address().port
        var url = 'http://127.0.0.1:' + port
        var handler = function (request, callback) {
          callback({
            url: url
          })
        }
        protocol.registerHttpProtocol(protocolName, handler, function (error) {
          if (error) {
            return done(error)
          }
          $.ajax({
            url: protocolName + '://fake-host',
            cache: false,
            success: function (data) {
              assert.equal(data, text)
              done()
            },
            error: function (xhr, errorType, error) {
              done(error)
            }
          })
        })
      })
    })

    it('fails when sending invalid url', function (done) {
      var handler = function (request, callback) {
        callback({
          url: 'url'
        })
      }
      protocol.registerHttpProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })

    it('fails when sending unsupported content', function (done) {
      var handler = function (request, callback) {
        callback(new Date())
      }
      protocol.registerHttpProtocol(protocolName, handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: protocolName + '://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })

    it('works when target URL redirects', function (done) {
      var contents = null
      var server = http.createServer(function (req, res) {
        if (req.url === '/serverRedirect') {
          res.statusCode = 301
          res.setHeader('Location', 'http://' + req.rawHeaders[1])
          res.end()
        } else {
          res.end(text)
        }
      })
      server.listen(0, '127.0.0.1', function () {
        var port = server.address().port
        var url = `${protocolName}://fake-host`
        var redirectURL = `http://127.0.0.1:${port}/serverRedirect`
        var handler = function (request, callback) {
          callback({
            url: redirectURL
          })
        }
        protocol.registerHttpProtocol(protocolName, handler, function (error) {
          if (error) {
            return done(error)
          }
          contents = webContents.create({})
          contents.on('did-finish-load', function () {
            assert.equal(contents.getURL(), url)
            server.close()
            contents.destroy()
            done()
          })
          contents.loadURL(url)
        })
      })
    })
  })

  describe('protocol.isProtocolHandled', function () {
    it('returns true for about:', function (done) {
      protocol.isProtocolHandled('about', function (result) {
        assert.equal(result, true)
        done()
      })
    })

    it('returns true for file:', function (done) {
      protocol.isProtocolHandled('file', function (result) {
        assert.equal(result, true)
        done()
      })
    })

    it('returns true for http:', function (done) {
      protocol.isProtocolHandled('http', function (result) {
        assert.equal(result, true)
        done()
      })
    })

    it('returns true for https:', function (done) {
      protocol.isProtocolHandled('https', function (result) {
        assert.equal(result, true)
        done()
      })
    })

    it('returns false when scheme is not registered', function (done) {
      protocol.isProtocolHandled('no-exist', function (result) {
        assert.equal(result, false)
        done()
      })
    })

    it('returns true for custom protocol', function (done) {
      var emptyHandler = function (request, callback) {
        callback()
      }
      protocol.registerStringProtocol(protocolName, emptyHandler, function (error) {
        assert.equal(error, null)
        protocol.isProtocolHandled(protocolName, function (result) {
          assert.equal(result, true)
          done()
        })
      })
    })

    it('returns true for intercepted protocol', function (done) {
      var emptyHandler = function (request, callback) {
        callback()
      }
      protocol.interceptStringProtocol('http', emptyHandler, function (error) {
        assert.equal(error, null)
        protocol.isProtocolHandled('http', function (result) {
          assert.equal(result, true)
          done()
        })
      })
    })
  })

  describe('protocol.intercept(Any)Protocol', function () {
    var emptyHandler = function (request, callback) {
      callback()
    }

    it('throws error when scheme is already intercepted', function (done) {
      protocol.interceptStringProtocol('http', emptyHandler, function (error) {
        assert.equal(error, null)
        protocol.interceptBufferProtocol('http', emptyHandler, function (error) {
          assert.notEqual(error, null)
          done()
        })
      })
    })

    it('does not crash when handler is called twice', function (done) {
      var doubleHandler = function (request, callback) {
        try {
          callback(text)
          callback()
        } catch (error) {
          // Ignore error
        }
      }
      protocol.interceptStringProtocol('http', doubleHandler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('sends error when callback is called with nothing', function (done) {
      if (process.env.TRAVIS === 'true') {
        return done()
      }
      protocol.interceptBufferProtocol('http', emptyHandler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          success: function () {
            done('request succeeded but it should not')
          },
          error: function (xhr, errorType) {
            assert.equal(errorType, 'error')
            done()
          }
        })
      })
    })
  })

  describe('protocol.interceptStringProtocol', function () {
    it('can intercept http protocol', function (done) {
      var handler = function (request, callback) {
        callback(text)
      }
      protocol.interceptStringProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('can set content-type', function (done) {
      var handler = function (request, callback) {
        callback({
          mimeType: 'application/json',
          data: '{"value": 1}'
        })
      }
      protocol.interceptStringProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(typeof data, 'object')
            assert.equal(data.value, 1)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('can receive post data', function (done) {
      var handler = function (request, callback) {
        var uploadData = request.uploadData[0].bytes.toString()
        callback({
          data: uploadData
        })
      }
      protocol.interceptStringProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          type: 'POST',
          data: postData,
          success: function (data) {
            assert.deepEqual(qs.parse(data), postData)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })
  })

  describe('protocol.interceptBufferProtocol', function () {
    it('can intercept http protocol', function (done) {
      var handler = function (request, callback) {
        callback(new Buffer(text))
      }
      protocol.interceptBufferProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          success: function (data) {
            assert.equal(data, text)
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })

    it('can receive post data', function (done) {
      var handler = function (request, callback) {
        var uploadData = request.uploadData[0].bytes
        callback(uploadData)
      }
      protocol.interceptBufferProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        $.ajax({
          url: 'http://fake-host',
          cache: false,
          type: 'POST',
          data: postData,
          success: function (data) {
            assert.equal(data, $.param(postData))
            done()
          },
          error: function (xhr, errorType, error) {
            done(error)
          }
        })
      })
    })
  })

  describe('protocol.interceptHttpProtocol', function () {
    it('can send POST request', function (done) {
      var server = http.createServer(function (req, res) {
        var body = ''
        req.on('data', function (chunk) {
          body += chunk
        })
        req.on('end', function () {
          res.end(body)
        })
        server.close()
      })
      server.listen(0, '127.0.0.1', function () {
        var port = server.address().port
        var url = 'http://127.0.0.1:' + port
        var handler = function (request, callback) {
          var data = {
            url: url,
            method: 'POST',
            uploadData: {
              contentType: 'application/x-www-form-urlencoded',
              data: request.uploadData[0].bytes.toString()
            },
            session: null
          }
          callback(data)
        }
        protocol.interceptHttpProtocol('http', handler, function (error) {
          if (error) {
            return done(error)
          }
          $.ajax({
            url: 'http://fake-host',
            cache: false,
            type: 'POST',
            data: postData,
            success: function (data) {
              assert.deepEqual(qs.parse(data), postData)
              done()
            },
            error: function (xhr, errorType, error) {
              done(error)
            }
          })
        })
      })
    })

    it('can use custom session', function (done) {
      const customSession = session.fromPartition('custom-ses', {
        cache: false
      })
      customSession.webRequest.onBeforeRequest(function (details, callback) {
        assert.equal(details.url, 'http://fake-host/')
        callback({cancel: true})
      })
      const handler = function (request, callback) {
        callback({
          url: request.url,
          session: customSession
        })
      }
      protocol.interceptHttpProtocol('http', handler, function (error) {
        if (error) {
          return done(error)
        }
        fetch('http://fake-host').then(function () {
          done('request succeeded but it should not')
        }).catch(function () {
          customSession.webRequest.onBeforeRequest(null)
          done()
        })
      })
    })
  })

  describe('protocol.uninterceptProtocol', function () {
    it('returns error when scheme does not exist', function (done) {
      protocol.uninterceptProtocol('not-exist', function (error) {
        assert.notEqual(error, null)
        done()
      })
    })

    it('returns error when scheme is not intercepted', function (done) {
      protocol.uninterceptProtocol('http', function (error) {
        assert.notEqual(error, null)
        done()
      })
    })
  })

  describe('protocol.registerStandardSchemes', function () {
    const standardScheme = remote.getGlobal('standardScheme')
    const origin = standardScheme + '://fake-host'
    const imageURL = origin + '/test.png'
    const filePath = path.join(__dirname, 'fixtures', 'pages', 'b.html')
    const fileContent = '<img src="/test.png" />'
    var w = null
    var success = null

    beforeEach(function () {
      w = new BrowserWindow({show: false})
      success = false
    })

    afterEach(function (done) {
      protocol.unregisterProtocol(standardScheme, function () {
        closeWindow(w).then(function () {
          w = null
          done()
        })
      })
    })

    it('resolves relative resources', function (done) {
      var handler = function (request, callback) {
        if (request.url === imageURL) {
          success = true
          callback()
        } else {
          callback(filePath)
        }
      }
      protocol.registerFileProtocol(standardScheme, handler, function (error) {
        if (error) {
          return done(error)
        }
        w.webContents.on('did-finish-load', function () {
          assert(success)
          done()
        })
        w.loadURL(origin)
      })
    })

    it('resolves absolute resources', function (done) {
      var handler = function (request, callback) {
        if (request.url === imageURL) {
          success = true
          callback()
        } else {
          callback({
            data: fileContent,
            mimeType: 'text/html'
          })
        }
      }
      protocol.registerStringProtocol(standardScheme, handler, function (error) {
        if (error) {
          return done(error)
        }
        w.webContents.on('did-finish-load', function () {
          assert(success)
          done()
        })
        w.loadURL(origin)
      })
    })

    it('can have fetch working in it', function (done) {
      const content = '<html><script>fetch("http://github.com")</script></html>'
      const handler = function (request, callback) {
        callback({data: content, mimeType: 'text/html'})
      }
      protocol.registerStringProtocol(standardScheme, handler, function (error) {
        if (error) return done(error)
        w.webContents.on('crashed', function () {
          done('WebContents crashed')
        })
        w.webContents.on('did-finish-load', function () {
          done()
        })
        w.loadURL(origin)
      })
    })

    it('can access files through the FileSystem API', function (done) {
      let filePath = path.join(__dirname, 'fixtures', 'pages', 'filesystem.html')
      const handler = function (request, callback) {
        callback({path: filePath})
      }
      protocol.registerFileProtocol(standardScheme, handler, function (error) {
        if (error) return done(error)
        w.loadURL(origin)
      })
      ipcMain.once('file-system-error', (event, err) => done(err))
      ipcMain.once('file-system-write-end', () => done())
    })

    it('registers secure, when {secure: true}', function (done) {
      // the CacheStorage API will only work if secure == true
      let filePath = path.join(__dirname, 'fixtures', 'pages', 'cache-storage.html')
      const handler = function (request, callback) {
        callback({path: filePath})
      }
      ipcMain.once('success', () => done())
      ipcMain.once('failure', (event, err) => done(err))
      protocol.registerFileProtocol(standardScheme, handler, function (error) {
        if (error) return done(error)
        w.loadURL(origin)
      })
    })
  })
})
