import { expect } from 'chai'
import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'

describe('ipc module', () => {
  describe('invoke', () => {
    let w = (null as unknown as BrowserWindow)

    before(async () => {
      w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
      await w.loadURL('about:blank')
    })
    after(async () => {
      w.destroy()
    })

    async function rendererInvoke (...args: any[]) {
      const { ipcRenderer } = require('electron')
      try {
        const result = await ipcRenderer.invoke('test', ...args)
        ipcRenderer.send('result', { result })
      } catch (e) {
        ipcRenderer.send('result', { error: e.message })
      }
    }

    it('receives a response from a synchronous handler', async () => {
      ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => {
        expect(arg).to.equal(123)
        return 3
      })
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg).to.deep.equal({ result: 3 })
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`)
      await done
    })

    it('receives a response from an asynchronous handler', async () => {
      ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => {
        expect(arg).to.equal(123)
        await new Promise(resolve => setImmediate(resolve))
        return 3
      })
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg).to.deep.equal({ result: 3 })
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`)
      await done
    })

    it('receives an error from a synchronous handler', async () => {
      ipcMain.handleOnce('test', () => {
        throw new Error('some error')
      })
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg.error).to.match(/some error/)
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
      await done
    })

    it('receives an error from an asynchronous handler', async () => {
      ipcMain.handleOnce('test', async () => {
        await new Promise(resolve => setImmediate(resolve))
        throw new Error('some error')
      })
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg.error).to.match(/some error/)
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
      await done
    })

    it('throws an error if no handler is registered', async () => {
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg.error).to.match(/No handler registered/)
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
      await done
    })

    it('throws an error when invoking a handler that was removed', async () => {
      ipcMain.handle('test', () => {})
      ipcMain.removeHandler('test')
      const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
        expect(arg.error).to.match(/No handler registered/)
        resolve()
      }))
      await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
      await done
    })

    it('forbids multiple handlers', async () => {
      ipcMain.handle('test', () => {})
      try {
        expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/)
      } finally {
        ipcMain.removeHandler('test')
      }
    })
  })

  describe('ordering', () => {
    let w = (null as unknown as BrowserWindow)

    before(async () => {
      w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
      await w.loadURL('about:blank')
    })
    after(async () => {
      w.destroy()
    })

    it('between send and sendSync is consistent', async () => {
      const received: number[] = []
      ipcMain.on('test-async', (e, i) => { received.push(i) })
      ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null })
      const done = new Promise(resolve => ipcMain.once('done', () => { resolve() }))
      function rendererStressTest () {
        const { ipcRenderer } = require('electron')
        for (let i = 0; i < 1000; i++) {
          switch ((Math.random() * 2) | 0) {
            case 0:
              ipcRenderer.send('test-async', i)
              break
            case 1:
              ipcRenderer.sendSync('test-sync', i)
              break
          }
        }
        ipcRenderer.send('done')
      }
      try {
        w.webContents.executeJavaScript(`(${rendererStressTest})()`)
        await done
      } finally {
        ipcMain.removeAllListeners('test-async')
        ipcMain.removeAllListeners('test-sync')
      }
      expect(received).to.have.lengthOf(1000)
      expect(received).to.deep.equal([...received].sort((a, b) => a - b))
    })

    it('between send, sendSync, and invoke is consistent', async () => {
      const received: number[] = []
      ipcMain.handle('test-invoke', (e, i) => { received.push(i) })
      ipcMain.on('test-async', (e, i) => { received.push(i) })
      ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null })
      const done = new Promise(resolve => ipcMain.once('done', () => { resolve() }))
      function rendererStressTest () {
        const { ipcRenderer } = require('electron')
        for (let i = 0; i < 1000; i++) {
          switch ((Math.random() * 3) | 0) {
            case 0:
              ipcRenderer.send('test-async', i)
              break
            case 1:
              ipcRenderer.sendSync('test-sync', i)
              break
            case 2:
              ipcRenderer.invoke('test-invoke', i)
              break
          }
        }
        ipcRenderer.send('done')
      }
      try {
        w.webContents.executeJavaScript(`(${rendererStressTest})()`)
        await done
      } finally {
        ipcMain.removeHandler('test-invoke')
        ipcMain.removeAllListeners('test-async')
        ipcMain.removeAllListeners('test-sync')
      }
      expect(received).to.have.lengthOf(1000)
      expect(received).to.deep.equal([...received].sort((a, b) => a - b))
    })
  })
})
