import { Config, useContractFunction } from '../../src'
import { expect } from 'chai'
import { BigNumber, Contract, ethers, Wallet } from 'ethers'
import { deployMockToken, setupTestingConfig, TestingNetwork } from '../../src/testing'
import { renderDAppHook } from '../testing/renderDAppHook'

const CONTRACT_FUNCTION_COST = 52441 // mock transfer transaction cost

describe('useContractFunction', () => {
  let token: Contract
  let config: Config
  let network1: TestingNetwork
  let wallet1: Wallet
  let wallet2: Wallet
  let spender: Wallet

  beforeEach(async () => {
    ;({ config, network1 } = await setupTestingConfig())
    token = await deployMockToken(network1.deployer)
    spender = network1.wallets[1]
    wallet2 = network1.wallets[1]
    wallet1 = ethers.Wallet.fromMnemonic(
      'radar blur cabbage chef fix engine embark joy scheme fiction master release'
    ).connect(network1.provider)
    await network1.wallets[1].sendTransaction({ to: wallet1.address, value: 100000 })
    await token.transfer(wallet1.address, 1000)
  })

  it('success', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'approve'),
      {
        config,
      }
    )
    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    expect(await token.allowance(network1.deployer.address, spender.address)).to.eq(200)
  })

  it('events', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'approve'),
      {
        config,
      }
    )
    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current?.events?.length).to.eq(1)

    const event = result.current?.events?.[0]

    expect(event?.name).to.eq('Approval')
    expect(event?.args['owner']).to.eq(network1.deployer.address)
    expect(event?.args['spender']).to.eq(spender.address)
    expect(event?.args['value']).to.eq(BigNumber.from(200))
  })

  it('exception (bad arguments)', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'approve'),
      {
        config,
      }
    )

    await waitForNextUpdate()
    await result.current.send()
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Exception')
    if (result.current.state.status === 'Exception') {
      expect(result.current.state.errorMessage).to.eq('missing argument: passed to contract')
    }
  })

  it('exception (when transaction reverts)', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      // { gasLimitBufferPercentage: -10 } - to cause out of gas error
      () => useContractFunction(token, 'transfer', { gasLimitBufferPercentage: -10 }),
      {
        config,
      }
    )

    await waitForNextUpdate()
    await result.current.send(spender.address, 10)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Exception')
    expect(result.current.state.errorMessage).to.eq('transaction failed')
  })

  it('should not throw error when contract is Falsy', async () => {
    const { result, waitForNextUpdate } = await renderDAppHook(() => useContractFunction(null, 'approve'), {
      config,
    })
    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
  })

  it('transfer amount with limit', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'transfer'),
      {
        config: {
          ...config,
          gasLimitBufferPercentage: 100,
        },
      }
    )

    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    expect(result.current.state.transaction?.gasLimit.toNumber()).to.equal(2 * CONTRACT_FUNCTION_COST)
  })

  it('transfer amount with limit in args', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'transfer', { gasLimitBufferPercentage: 100 }),
      {
        config,
      }
    )

    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    expect(result.current.state.transaction?.gasLimit.toNumber()).to.equal(2 * CONTRACT_FUNCTION_COST)
  })

  it('success with correct receipt', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'approve'),
      {
        config,
      }
    )
    await waitForNextUpdate()
    await result.current.send(spender.address, 200)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    expect(await token.allowance(network1.deployer.address, spender.address)).to.eq(200)

    expect(result.current.state.receipt).to.not.be.undefined
    expect(result.current.state.receipt?.to).to.eq(token.address)
    expect(result.current.state.receipt?.from).to.eq(network1.deployer.address)
    expect(result.current.state.receipt?.gasUsed).to.be.gt(0)
    expect(result.current.state.receipt?.status).to.eq(1)
    expect(result.current.state.receipt?.blockHash).to.match(/^0x/)
    expect(result.current.state.receipt?.transactionHash).to.match(/^0x/)
    expect(result.current.state.receipt?.gasUsed).to.be.gt(0)
  })

  it('transfer amount with just private key', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'transfer', { chainId: 1, privateKey: wallet1.privateKey }),
      {
        config,
      }
    )
    await waitForNextUpdate()

    const startingBalance = await token.balanceOf(wallet2.address)
    await result.current.send(wallet2.address, 100)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    const finalBalance = await token.balanceOf(wallet2.address)
    expect(finalBalance).to.equal(startingBalance.add(100))
  })

  it('transfer amount with just mnemonic phrase', async () => {
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () => useContractFunction(token, 'transfer', { chainId: 1, mnemonicPhrase: wallet1.mnemonic.phrase }),
      {
        config,
      }
    )
    await waitForNextUpdate()

    const startingBalance = await token.balanceOf(wallet2.address)
    await result.current.send(wallet2.address, 100)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    const finalBalance = await token.balanceOf(wallet2.address)
    expect(finalBalance).to.equal(startingBalance.add(100))
  })

  it('transfer amount with just encrypted json', async () => {
    const json = await wallet1.encrypt('test')
    const { result, waitForCurrent, waitForNextUpdate } = await renderDAppHook(
      () =>
        useContractFunction(token, 'transfer', {
          chainId: 1,
          json,
          password: 'test',
        }),
      {
        config,
      }
    )
    await waitForNextUpdate()

    const startingBalance = await token.balanceOf(wallet2.address)
    await result.current.send(wallet2.address, 100)
    await waitForCurrent((val) => val.state !== undefined)

    expect(result.current.state.status).to.eq('Success')
    const finalBalance = await token.balanceOf(wallet2.address)
    expect(finalBalance).to.equal(startingBalance.add(100))
  })
})
