import { ethers, waffle } from 'hardhat'
import { BigNumber, constants, Contract, ContractTransaction } from 'ethers'
import {
  IWETH9,
  MockTimeNonfungiblePositionManager,
  MockTimeSwapRouter,
  PairFlash,
  IUniswapV3Pool,
  TestERC20,
  TestERC20Metadata,
  IUniswapV3Factory,
  Quoter,
  SwapRouter,
} from '../typechain'
import completeFixture from './shared/completeFixture'
import { FeeAmount, MaxUint128, TICK_SPACINGS } from './shared/constants'
import { encodePriceSqrt } from './shared/encodePriceSqrt'
import snapshotGasCost from './shared/snapshotGasCost'

import { expect } from './shared/expect'
import { getMaxTick, getMinTick } from './shared/ticks'
import { computePoolAddress } from './shared/computePoolAddress'

describe('PairFlash test', () => {
  const provider = waffle.provider
  const wallets = waffle.provider.getWallets()
  const wallet = wallets[0]

  let flash: PairFlash
  let nft: MockTimeNonfungiblePositionManager
  let token0: TestERC20
  let token1: TestERC20
  let factory: IUniswapV3Factory
  let quoter: Quoter

  async function createPool(tokenAddressA: string, tokenAddressB: string, fee: FeeAmount, price: BigNumber) {
    if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
      [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]

    await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, fee, price)

    const liquidityParams = {
      token0: tokenAddressA,
      token1: tokenAddressB,
      fee: fee,
      tickLower: getMinTick(TICK_SPACINGS[fee]),
      tickUpper: getMaxTick(TICK_SPACINGS[fee]),
      recipient: wallet.address,
      amount0Desired: 1000000,
      amount1Desired: 1000000,
      amount0Min: 0,
      amount1Min: 0,
      deadline: 1,
    }

    return nft.mint(liquidityParams)
  }

  const flashFixture = async () => {
    const { router, tokens, factory, weth9, nft } = await completeFixture(wallets, provider)
    const token0 = tokens[0]
    const token1 = tokens[1]

    const flashContractFactory = await ethers.getContractFactory('PairFlash')
    const flash = (await flashContractFactory.deploy(router.address, factory.address, weth9.address)) as PairFlash

    const quoterFactory = await ethers.getContractFactory('Quoter')
    const quoter = (await quoterFactory.deploy(factory.address, weth9.address)) as Quoter

    return {
      token0,
      token1,
      flash,
      factory,
      weth9,
      nft,
      quoter,
      router,
    }
  }

  let loadFixture: ReturnType<typeof waffle.createFixtureLoader>

  before('create fixture loader', async () => {
    loadFixture = waffle.createFixtureLoader(wallets)
  })

  beforeEach('load fixture', async () => {
    ;({ factory, token0, token1, flash, nft, quoter } = await loadFixture(flashFixture))

    await token0.approve(nft.address, MaxUint128)
    await token1.approve(nft.address, MaxUint128)
    await createPool(token0.address, token1.address, FeeAmount.LOW, encodePriceSqrt(5, 10))
    await createPool(token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1))
    await createPool(token0.address, token1.address, FeeAmount.HIGH, encodePriceSqrt(20, 10))
  })

  describe('flash', () => {
    it('test correct transfer events', async () => {
      //choose amountIn to test
      const amount0In = 1000
      const amount1In = 1000

      const fee0 = Math.ceil((amount0In * FeeAmount.MEDIUM) / 1000000)
      const fee1 = Math.ceil((amount1In * FeeAmount.MEDIUM) / 1000000)

      const flashParams = {
        token0: token0.address,
        token1: token1.address,
        fee1: FeeAmount.MEDIUM,
        amount0: amount0In,
        amount1: amount1In,
        fee2: FeeAmount.LOW,
        fee3: FeeAmount.HIGH,
      }
      // pool1 is the borrow pool
      const pool1 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.MEDIUM)
      const pool2 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.LOW)
      const pool3 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.HIGH)

      const expectedAmountOut0 = await quoter.callStatic.quoteExactInputSingle(
        token1.address,
        token0.address,
        FeeAmount.LOW,
        amount1In,
        encodePriceSqrt(20, 10)
      )
      const expectedAmountOut1 = await quoter.callStatic.quoteExactInputSingle(
        token0.address,
        token1.address,
        FeeAmount.HIGH,
        amount0In,
        encodePriceSqrt(5, 10)
      )

      await expect(flash.initFlash(flashParams))
        .to.emit(token0, 'Transfer')
        .withArgs(pool1, flash.address, amount0In)
        .to.emit(token1, 'Transfer')
        .withArgs(pool1, flash.address, amount1In)
        .to.emit(token0, 'Transfer')
        .withArgs(pool2, flash.address, expectedAmountOut0)
        .to.emit(token1, 'Transfer')
        .withArgs(pool3, flash.address, expectedAmountOut1)
        .to.emit(token0, 'Transfer')
        .withArgs(flash.address, wallet.address, expectedAmountOut0.toNumber() - amount0In - fee0)
        .to.emit(token1, 'Transfer')
        .withArgs(flash.address, wallet.address, expectedAmountOut1.toNumber() - amount1In - fee1)
    })

    it('gas', async () => {
      const amount0In = 1000
      const amount1In = 1000

      const flashParams = {
        token0: token0.address,
        token1: token1.address,
        fee1: FeeAmount.MEDIUM,
        amount0: amount0In,
        amount1: amount1In,
        fee2: FeeAmount.LOW,
        fee3: FeeAmount.HIGH,
      }
      await snapshotGasCost(flash.initFlash(flashParams))
    })
  })
})
