import { Fixture } from 'ethereum-waffle'
import { constants, Contract, Wallet } from 'ethers'
import { ethers, waffle } from 'hardhat'
import {
  IUniswapV2Pair,
  IUniswapV3Factory,
  IWETH9,
  MockTimeNonfungiblePositionManager,
  TestERC20,
  V3Migrator,
} from '../typechain'
import completeFixture from './shared/completeFixture'
import { v2FactoryFixture } from './shared/externalFixtures'

import { abi as PAIR_V2_ABI } from '@uniswap/v2-core/build/UniswapV2Pair.json'
import { expect } from 'chai'
import { FeeAmount } from './shared/constants'
import { encodePriceSqrt } from './shared/encodePriceSqrt'
import snapshotGasCost from './shared/snapshotGasCost'
import { sortedTokens } from './shared/tokenSort'
import { getMaxTick, getMinTick } from './shared/ticks'

describe('V3Migrator', () => {
  let wallet: Wallet

  const migratorFixture: Fixture<{
    factoryV2: Contract
    factoryV3: IUniswapV3Factory
    token: TestERC20
    weth9: IWETH9
    nft: MockTimeNonfungiblePositionManager
    migrator: V3Migrator
  }> = async (wallets, provider) => {
    const { factory, tokens, nft, weth9 } = await completeFixture(wallets, provider)

    const { factory: factoryV2 } = await v2FactoryFixture(wallets, provider)

    const token = tokens[0]
    await token.approve(factoryV2.address, constants.MaxUint256)
    await weth9.deposit({ value: 10000 })
    await weth9.approve(nft.address, constants.MaxUint256)

    // deploy the migrator
    const migrator = (await (
      await ethers.getContractFactory('V3Migrator')
    ).deploy(factory.address, weth9.address, nft.address)) as V3Migrator

    return {
      factoryV2,
      factoryV3: factory,
      token,
      weth9,
      nft,
      migrator,
    }
  }

  let factoryV2: Contract
  let factoryV3: IUniswapV3Factory
  let token: TestERC20
  let weth9: IWETH9
  let nft: MockTimeNonfungiblePositionManager
  let migrator: V3Migrator
  let pair: IUniswapV2Pair

  let loadFixture: ReturnType<typeof waffle.createFixtureLoader>

  const expectedLiquidity = 10000 - 1000

  before('create fixture loader', async () => {
    const wallets = await (ethers as any).getSigners()
    wallet = wallets[0]

    loadFixture = waffle.createFixtureLoader(wallets)
  })

  beforeEach('load fixture', async () => {
    ;({ factoryV2, factoryV3, token, weth9, nft, migrator } = await loadFixture(migratorFixture))
  })

  beforeEach('add V2 liquidity', async () => {
    await factoryV2.createPair(token.address, weth9.address)

    const pairAddress = await factoryV2.getPair(token.address, weth9.address)

    pair = new ethers.Contract(pairAddress, PAIR_V2_ABI, wallet) as IUniswapV2Pair

    await token.transfer(pair.address, 10000)
    await weth9.transfer(pair.address, 10000)

    await pair.mint(wallet.address)

    expect(await pair.balanceOf(wallet.address)).to.be.eq(expectedLiquidity)
  })

  afterEach('ensure allowances are cleared', async () => {
    const allowanceToken = await token.allowance(migrator.address, nft.address)
    const allowanceWETH9 = await weth9.allowance(migrator.address, nft.address)
    expect(allowanceToken).to.be.eq(0)
    expect(allowanceWETH9).to.be.eq(0)
  })

  afterEach('ensure balances are cleared', async () => {
    const balanceToken = await token.balanceOf(migrator.address)
    const balanceWETH9 = await weth9.balanceOf(migrator.address)
    expect(balanceToken).to.be.eq(0)
    expect(balanceWETH9).to.be.eq(0)
  })

  afterEach('ensure eth balance is cleared', async () => {
    const balanceETH = await ethers.provider.getBalance(migrator.address)
    expect(balanceETH).to.be.eq(0)
  })

  describe('#migrate', () => {
    let tokenLower: boolean
    beforeEach(() => {
      tokenLower = token.address.toLowerCase() < weth9.address.toLowerCase()
    })

    it('fails if v3 pool is not initialized', async () => {
      await pair.approve(migrator.address, expectedLiquidity)
      await expect(
        migrator.migrate({
          pair: pair.address,
          liquidityToMigrate: expectedLiquidity,
          percentageToMigrate: 100,
          token0: tokenLower ? token.address : weth9.address,
          token1: tokenLower ? weth9.address : token.address,
          fee: FeeAmount.MEDIUM,
          tickLower: -1,
          tickUpper: 1,
          amount0Min: 9000,
          amount1Min: 9000,
          recipient: wallet.address,
          deadline: 1,
          refundAsETH: false,
        })
      ).to.be.reverted
    })

    it('works once v3 pool is initialized', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(1, 1)
      )

      await pair.approve(migrator.address, expectedLiquidity)
      await migrator.migrate({
        pair: pair.address,
        liquidityToMigrate: expectedLiquidity,
        percentageToMigrate: 100,
        token0: tokenLower ? token.address : weth9.address,
        token1: tokenLower ? weth9.address : token.address,
        fee: FeeAmount.MEDIUM,
        tickLower: getMinTick(FeeAmount.MEDIUM),
        tickUpper: getMaxTick(FeeAmount.MEDIUM),
        amount0Min: 9000,
        amount1Min: 9000,
        recipient: wallet.address,
        deadline: 1,
        refundAsETH: false,
      })

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(9000)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      expect(await token.balanceOf(poolAddress)).to.be.eq(9000)
      expect(await weth9.balanceOf(poolAddress)).to.be.eq(9000)
    })

    it('works for partial', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(1, 1)
      )

      const tokenBalanceBefore = await token.balanceOf(wallet.address)
      const weth9BalanceBefore = await weth9.balanceOf(wallet.address)

      await pair.approve(migrator.address, expectedLiquidity)
      await migrator.migrate({
        pair: pair.address,
        liquidityToMigrate: expectedLiquidity,
        percentageToMigrate: 50,
        token0: tokenLower ? token.address : weth9.address,
        token1: tokenLower ? weth9.address : token.address,
        fee: FeeAmount.MEDIUM,
        tickLower: getMinTick(FeeAmount.MEDIUM),
        tickUpper: getMaxTick(FeeAmount.MEDIUM),
        amount0Min: 4500,
        amount1Min: 4500,
        recipient: wallet.address,
        deadline: 1,
        refundAsETH: false,
      })

      const tokenBalanceAfter = await token.balanceOf(wallet.address)
      const weth9BalanceAfter = await weth9.balanceOf(wallet.address)

      expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500)
      expect(weth9BalanceAfter.sub(weth9BalanceBefore)).to.be.eq(4500)

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(4500)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      expect(await token.balanceOf(poolAddress)).to.be.eq(4500)
      expect(await weth9.balanceOf(poolAddress)).to.be.eq(4500)
    })

    it('double the price', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(2, 1)
      )

      const tokenBalanceBefore = await token.balanceOf(wallet.address)
      const weth9BalanceBefore = await weth9.balanceOf(wallet.address)

      await pair.approve(migrator.address, expectedLiquidity)
      await migrator.migrate({
        pair: pair.address,
        liquidityToMigrate: expectedLiquidity,
        percentageToMigrate: 100,
        token0: tokenLower ? token.address : weth9.address,
        token1: tokenLower ? weth9.address : token.address,
        fee: FeeAmount.MEDIUM,
        tickLower: getMinTick(FeeAmount.MEDIUM),
        tickUpper: getMaxTick(FeeAmount.MEDIUM),
        amount0Min: 4500,
        amount1Min: 8999,
        recipient: wallet.address,
        deadline: 1,
        refundAsETH: false,
      })

      const tokenBalanceAfter = await token.balanceOf(wallet.address)
      const weth9BalanceAfter = await weth9.balanceOf(wallet.address)

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(6363)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      if (token.address.toLowerCase() < weth9.address.toLowerCase()) {
        expect(await token.balanceOf(poolAddress)).to.be.eq(4500)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(8999)
        expect(weth9BalanceAfter.sub(weth9BalanceBefore)).to.be.eq(1)
      } else {
        expect(await token.balanceOf(poolAddress)).to.be.eq(8999)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(4500)
        expect(weth9BalanceAfter.sub(weth9BalanceBefore)).to.be.eq(4500)
      }
    })

    it('half the price', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(1, 2)
      )

      const tokenBalanceBefore = await token.balanceOf(wallet.address)
      const weth9BalanceBefore = await weth9.balanceOf(wallet.address)

      await pair.approve(migrator.address, expectedLiquidity)
      await migrator.migrate({
        pair: pair.address,
        liquidityToMigrate: expectedLiquidity,
        percentageToMigrate: 100,
        token0: tokenLower ? token.address : weth9.address,
        token1: tokenLower ? weth9.address : token.address,
        fee: FeeAmount.MEDIUM,
        tickLower: getMinTick(FeeAmount.MEDIUM),
        tickUpper: getMaxTick(FeeAmount.MEDIUM),
        amount0Min: 8999,
        amount1Min: 4500,
        recipient: wallet.address,
        deadline: 1,
        refundAsETH: false,
      })

      const tokenBalanceAfter = await token.balanceOf(wallet.address)
      const weth9BalanceAfter = await weth9.balanceOf(wallet.address)

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(6363)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      if (token.address.toLowerCase() < weth9.address.toLowerCase()) {
        expect(await token.balanceOf(poolAddress)).to.be.eq(8999)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(4500)
        expect(weth9BalanceAfter.sub(weth9BalanceBefore)).to.be.eq(4500)
      } else {
        expect(await token.balanceOf(poolAddress)).to.be.eq(4500)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(8999)
        expect(weth9BalanceAfter.sub(weth9BalanceBefore)).to.be.eq(1)
      }
    })

    it('double the price - as ETH', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(2, 1)
      )

      const tokenBalanceBefore = await token.balanceOf(wallet.address)

      await pair.approve(migrator.address, expectedLiquidity)
      await expect(
        migrator.migrate({
          pair: pair.address,
          liquidityToMigrate: expectedLiquidity,
          percentageToMigrate: 100,
          token0: tokenLower ? token.address : weth9.address,
          token1: tokenLower ? weth9.address : token.address,
          fee: FeeAmount.MEDIUM,
          tickLower: getMinTick(FeeAmount.MEDIUM),
          tickUpper: getMaxTick(FeeAmount.MEDIUM),
          amount0Min: 4500,
          amount1Min: 8999,
          recipient: wallet.address,
          deadline: 1,
          refundAsETH: true,
        })
      )
        .to.emit(weth9, 'Withdrawal')
        .withArgs(migrator.address, tokenLower ? 1 : 4500)

      const tokenBalanceAfter = await token.balanceOf(wallet.address)

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(6363)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      if (tokenLower) {
        expect(await token.balanceOf(poolAddress)).to.be.eq(4500)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(8999)
      } else {
        expect(await token.balanceOf(poolAddress)).to.be.eq(8999)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(4500)
      }
    })

    it('half the price - as ETH', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(1, 2)
      )

      const tokenBalanceBefore = await token.balanceOf(wallet.address)

      await pair.approve(migrator.address, expectedLiquidity)
      await expect(
        migrator.migrate({
          pair: pair.address,
          liquidityToMigrate: expectedLiquidity,
          percentageToMigrate: 100,
          token0: tokenLower ? token.address : weth9.address,
          token1: tokenLower ? weth9.address : token.address,
          fee: FeeAmount.MEDIUM,
          tickLower: getMinTick(FeeAmount.MEDIUM),
          tickUpper: getMaxTick(FeeAmount.MEDIUM),
          amount0Min: 8999,
          amount1Min: 4500,
          recipient: wallet.address,
          deadline: 1,
          refundAsETH: true,
        })
      )
        .to.emit(weth9, 'Withdrawal')
        .withArgs(migrator.address, tokenLower ? 4500 : 1)

      const tokenBalanceAfter = await token.balanceOf(wallet.address)

      const position = await nft.positions(1)
      expect(position.liquidity).to.be.eq(6363)

      const poolAddress = await factoryV3.getPool(token.address, weth9.address, FeeAmount.MEDIUM)
      if (tokenLower) {
        expect(await token.balanceOf(poolAddress)).to.be.eq(8999)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(4500)
      } else {
        expect(await token.balanceOf(poolAddress)).to.be.eq(4500)
        expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500)
        expect(await weth9.balanceOf(poolAddress)).to.be.eq(8999)
      }
    })

    it('gas', async () => {
      const [token0, token1] = sortedTokens(weth9, token)
      await migrator.createAndInitializePoolIfNecessary(
        token0.address,
        token1.address,
        FeeAmount.MEDIUM,
        encodePriceSqrt(1, 1)
      )

      await pair.approve(migrator.address, expectedLiquidity)
      await snapshotGasCost(
        migrator.migrate({
          pair: pair.address,
          liquidityToMigrate: expectedLiquidity,
          percentageToMigrate: 100,
          token0: tokenLower ? token.address : weth9.address,
          token1: tokenLower ? weth9.address : token.address,
          fee: FeeAmount.MEDIUM,
          tickLower: getMinTick(FeeAmount.MEDIUM),
          tickUpper: getMaxTick(FeeAmount.MEDIUM),
          amount0Min: 9000,
          amount1Min: 9000,
          recipient: wallet.address,
          deadline: 1,
          refundAsETH: false,
        })
      )
    })
  })
})
