import { expect } from 'chai'
import { providers, Wallet } from 'ethers'
import { useEffect } from 'react'
import { Config } from '../constants'
import { Mainnet, Mumbai } from '../model'
import { renderDAppHook, setupTestingConfig, sleep, TestingNetwork } from '../testing'
import { useEthers } from './useEthers'
import Ganache, { Server } from 'ganache'

describe('useEthers', () => {
  let network1: TestingNetwork
  let network2: TestingNetwork
  let config: Config
  const receiver = Wallet.createRandom().address

  before(async () => {
    ;({ config, network1, network2 } = await setupTestingConfig())
    await network1.wallets[0].sendTransaction({ to: receiver, value: 100 })
    await network2.wallets[1].sendTransaction({ to: receiver, value: 200 })
  })

  it('returns no wallets and readonly provider when not connected', async () => {
    const { result, waitForCurrent } = await renderDAppHook(() => useEthers(), { config })
    await waitForCurrent((val) => !val.isLoading)

    expect(result.error).to.be.undefined
    expect(result.current.error).to.be.undefined
    expect(result.current.activate).to.be.a('function')
    expect(result.current.deactivate).to.be.a('function')
    expect(result.current.activateBrowserWallet).to.be.a('function')
    expect(result.current.chainId).to.eq(Mainnet.chainId)
    expect(result.current.account).to.be.undefined
    expect(result.current.error).to.be.undefined
    expect(result.current.library).to.eq(network1.provider)
    expect(result.current.active).to.be.true
    expect(result.current.isLoading).to.be.false
  })

  it('throws error if trying to use unsupported network', async () => {
    const configWithUnsupportedNetworks: Config = {
      ...config,
      networks: [Mainnet],
    }
    const { result, waitForCurrent } = await renderDAppHook(
      () => {
        const { activate } = useEthers()
        useEffect(() => {
          void activate(network2.provider)
        }, [])

        return useEthers()
      },
      { config: configWithUnsupportedNetworks }
    )

    await waitForCurrent((val) => !!val.error)
    expect(result.current.error).not.to.be.undefined
    expect(result.current.error?.toString()).to.include(`Unsupported chain id: ${network2.chainId}`)
  })

  it('throws error if trying to use not configured network', async () => {
    const configWithNotConfiguredNetworks: Config = {
      ...config,
      readOnlyUrls: {
        [network1.chainId]: network1.provider,
      },
    }

    const { result, waitForCurrent } = await renderDAppHook(
      () => {
        const { activate } = useEthers()
        useEffect(() => {
          void activate(network2.provider)
        }, [])

        return useEthers()
      },
      { config: configWithNotConfiguredNetworks }
    )

    await waitForCurrent((val) => !!val.error)
    expect(result.current.error).not.to.be.undefined
    expect(result.current.error?.toString()).to.include(`Not configured chain id: ${network2.chainId}`)
  })

  it('returns correct provider after activation', async () => {
    const { result, waitForCurrent } = await renderDAppHook(
      () => {
        const { activate } = useEthers()
        useEffect(() => {
          void activate(network2.provider)
        }, [])

        return useEthers()
      },
      { config }
    )
    await waitForCurrent((val) => !val.isLoading && val.chainId === network2.provider.network.chainId)

    expect(result.error).to.be.undefined
    expect(result.current.error).to.be.undefined
    expect(result.current.activate).to.be.a('function')
    expect(result.current.deactivate).to.be.a('function')
    expect(result.current.activateBrowserWallet).to.be.a('function')
    expect(result.current.chainId).to.eq(network2.provider.network.chainId)
    expect(result.current.account).to.eq(network2.provider.getWallets()[0].address)
    expect(result.current.error).to.be.undefined
    expect(result.current.library).to.eq(network2.provider)
    expect(result.current.active).to.be.true
    expect(result.current.isLoading).to.be.false
  })

  it('return signer if library is type of JsonRpcProvider', async () => {
    const { result, waitForCurrent } = await renderDAppHook(
      () => {
        const { activate } = useEthers()
        useEffect(() => {
          void activate(network1.provider)
        }, [])

        return useEthers()
      },
      { config }
    )

    await waitForCurrent((val) => !val.isLoading)

    const provider = result.current.library
    const signer = provider && 'getSigner' in provider ? provider.getSigner() : undefined

    expect(result.current.error).to.be.undefined
    expect(result.current.library).to.be.instanceOf(providers.JsonRpcProvider)
    expect(signer).to.be.instanceOf(providers.JsonRpcSigner)
  })

  it('cannot get signer if library is type of FallbackProvider', async () => {
    const configWithFallbackProvider: Config = {
      ...config,
      readOnlyUrls: {
        [network1.chainId]: new providers.FallbackProvider([network1.provider]),
      },
    }
    const { result, waitForCurrent } = await renderDAppHook(useEthers, { config: configWithFallbackProvider })

    await waitForCurrent((val) => {
      return val.library instanceof providers.FallbackProvider
    })

    const provider = result.current.library
    const signer = provider && 'getSigner' in provider ? provider.getSigner() : undefined

    expect(result.current.error).to.be.undefined
    expect(result.current.library).to.be.instanceOf(providers.FallbackProvider)
    expect(signer).to.be.undefined
  })

  describe('Sets error if user rejects request', () => {
    before(() => {
      ;(window as any).ethereum = {
        request: async () => {
          // 100ms delay to simulate a real provider
          await sleep(100)
          throw {
            code: 4001,
            message: 'User rejected the request.',
          }
        },
      } as any // @metamask/detect-provider declares (window as any).ethereum as an incorrect type
    })

    after(() => {
      delete (window as any).ethereum
      window.localStorage.clear()
    })

    it('Sets error if user rejects request', async () => {
      const { result, waitForCurrent } = await renderDAppHook(
        () => {
          const { activateBrowserWallet, error } = useEthers()
          useEffect(() => {
            setTimeout(() => {
              // 100ms delay to simulate a user interaction
              activateBrowserWallet()
            }, 100)
          }, [])

          return error
        },
        { config }
      )

      await waitForCurrent((val) => !!val)
      expect(result.current).to.be.instanceOf(Error)
      expect(result.current?.message).to.eq('Could not activate connector: User rejected the request.')
    })
  })

  describe('Websocket provider', () => {
    let ganacheServer: Server<'ethereum'>
    let provider: providers.WebSocketProvider
    const wsPort = 18845
    const wsUrl = `ws://localhost:${wsPort}`

    before(async () => {
      ganacheServer = Ganache.server({ server: { ws: true }, logging: { quiet: true } })
      await ganacheServer.listen(wsPort)
      provider = new providers.WebSocketProvider(wsUrl)
    })

    after(async () => {
      await ganacheServer.close()
      // disrupting the connection forcefully so websocket server can be properly shutdown
      await provider.destroy()
    })

    it('works with a websocket provider', async () => {
      const { result, waitForCurrent } = await renderDAppHook(() => useEthers(), {
        config: {
          readOnlyChainId: Mumbai.chainId,
          readOnlyUrls: {
            [Mumbai.chainId]: provider,
          },
        },
      })
      await waitForCurrent((val) => !val.isLoading)
      expect(result.error).to.be.undefined
    })
  })
})
