import { ethers, waffle } from 'hardhat'
import { Wallet } from 'ethers'
import { MockTimeUniswapV3Pool } from '../typechain/MockTimeUniswapV3Pool'
import { expect } from './shared/expect'

import { poolFixture } from './shared/fixtures'
import snapshotGasCost from './shared/snapshotGasCost'

import {
  expandTo18Decimals,
  FeeAmount,
  getMinTick,
  encodePriceSqrt,
  TICK_SPACINGS,
  createPoolFunctions,
  SwapFunction,
  MintFunction,
  getMaxTick,
  MaxUint128,
  SwapToPriceFunction,
  MAX_SQRT_RATIO,
  MIN_SQRT_RATIO,
} from './shared/utilities'

const createFixtureLoader = waffle.createFixtureLoader

describe('UniswapV3Pool gas tests', () => {
  let wallet: Wallet, other: Wallet

  let loadFixture: ReturnType<typeof createFixtureLoader>

  before('create fixture loader', async () => {
    ;[wallet, other] = await (ethers as any).getSigners()
    loadFixture = createFixtureLoader([wallet, other])
  })

  for (const feeProtocol of [0, 6]) {
    describe(feeProtocol > 0 ? 'fee is on' : 'fee is off', () => {
      const startingPrice = encodePriceSqrt(100001, 100000)
      const startingTick = 0
      const feeAmount = FeeAmount.MEDIUM
      const tickSpacing = TICK_SPACINGS[feeAmount]
      const minTick = getMinTick(tickSpacing)
      const maxTick = getMaxTick(tickSpacing)

      const gasTestFixture = async ([wallet]: Wallet[]) => {
        const fix = await poolFixture([wallet], waffle.provider)

        const pool = await fix.createPool(feeAmount, tickSpacing)

        const { swapExact0For1, swapToHigherPrice, mint, swapToLowerPrice } = await createPoolFunctions({
          swapTarget: fix.swapTargetCallee,
          token0: fix.token0,
          token1: fix.token1,
          pool,
        })

        await pool.initialize(encodePriceSqrt(1, 1))
        await pool.setFeeProtocol(feeProtocol, feeProtocol)
        await pool.increaseObservationCardinalityNext(4)
        await pool.advanceTime(1)
        await mint(wallet.address, minTick, maxTick, expandTo18Decimals(2))

        await swapExact0For1(expandTo18Decimals(1), wallet.address)
        await pool.advanceTime(1)
        await swapToHigherPrice(startingPrice, wallet.address)
        await pool.advanceTime(1)
        expect((await pool.slot0()).tick).to.eq(startingTick)
        expect((await pool.slot0()).sqrtPriceX96).to.eq(startingPrice)

        return { pool, swapExact0For1, mint, swapToHigherPrice, swapToLowerPrice }
      }

      let swapExact0For1: SwapFunction
      let swapToHigherPrice: SwapToPriceFunction
      let swapToLowerPrice: SwapToPriceFunction
      let pool: MockTimeUniswapV3Pool
      let mint: MintFunction

      beforeEach('load the fixture', async () => {
        ;({ swapExact0For1, pool, mint, swapToHigherPrice, swapToLowerPrice } = await loadFixture(gasTestFixture))
      })

      describe('#swapExact0For1', () => {
        it('first swap in block with no tick movement', async () => {
          await snapshotGasCost(swapExact0For1(2000, wallet.address))
          expect((await pool.slot0()).sqrtPriceX96).to.not.eq(startingPrice)
          expect((await pool.slot0()).tick).to.eq(startingTick)
        })

        it('first swap in block moves tick, no initialized crossings', async () => {
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1).div(10000), wallet.address))
          expect((await pool.slot0()).tick).to.eq(startingTick - 1)
        })

        it('second swap in block with no tick movement', async () => {
          await swapExact0For1(expandTo18Decimals(1).div(10000), wallet.address)
          expect((await pool.slot0()).tick).to.eq(startingTick - 1)
          await snapshotGasCost(swapExact0For1(2000, wallet.address))
          expect((await pool.slot0()).tick).to.eq(startingTick - 1)
        })

        it('second swap in block moves tick, no initialized crossings', async () => {
          await swapExact0For1(1000, wallet.address)
          expect((await pool.slot0()).tick).to.eq(startingTick)
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1).div(10000), wallet.address))
          expect((await pool.slot0()).tick).to.eq(startingTick - 1)
        })

        it('first swap in block, large swap, no initialized crossings', async () => {
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(10), wallet.address))
          expect((await pool.slot0()).tick).to.eq(-35787)
        })

        it('first swap in block, large swap crossing several initialized ticks', async () => {
          await mint(wallet.address, startingTick - 3 * tickSpacing, startingTick - tickSpacing, expandTo18Decimals(1))
          await mint(
            wallet.address,
            startingTick - 4 * tickSpacing,
            startingTick - 2 * tickSpacing,
            expandTo18Decimals(1)
          )
          expect((await pool.slot0()).tick).to.eq(startingTick)
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(startingTick - 4 * tickSpacing) // we crossed the last tick
        })

        it('first swap in block, large swap crossing a single initialized tick', async () => {
          await mint(wallet.address, minTick, startingTick - 2 * tickSpacing, expandTo18Decimals(1))
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(startingTick - 2 * tickSpacing) // we crossed the last tick
        })

        it('second swap in block, large swap crossing several initialized ticks', async () => {
          await mint(wallet.address, startingTick - 3 * tickSpacing, startingTick - tickSpacing, expandTo18Decimals(1))
          await mint(
            wallet.address,
            startingTick - 4 * tickSpacing,
            startingTick - 2 * tickSpacing,
            expandTo18Decimals(1)
          )
          await swapExact0For1(expandTo18Decimals(1).div(10000), wallet.address)
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(startingTick - 4 * tickSpacing)
        })

        it('second swap in block, large swap crossing a single initialized tick', async () => {
          await mint(wallet.address, minTick, startingTick - 2 * tickSpacing, expandTo18Decimals(1))
          await swapExact0For1(expandTo18Decimals(1).div(10000), wallet.address)
          expect((await pool.slot0()).tick).to.be.gt(startingTick - 2 * tickSpacing) // we didn't cross the initialized tick
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(startingTick - 2 * tickSpacing) // we crossed the last tick
        })

        it('large swap crossing several initialized ticks after some time passes', async () => {
          await mint(wallet.address, startingTick - 3 * tickSpacing, startingTick - tickSpacing, expandTo18Decimals(1))
          await mint(
            wallet.address,
            startingTick - 4 * tickSpacing,
            startingTick - 2 * tickSpacing,
            expandTo18Decimals(1)
          )
          await swapExact0For1(2, wallet.address)
          await pool.advanceTime(1)
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(startingTick - 4 * tickSpacing)
        })

        it('large swap crossing several initialized ticks second time after some time passes', async () => {
          await mint(wallet.address, startingTick - 3 * tickSpacing, startingTick - tickSpacing, expandTo18Decimals(1))
          await mint(
            wallet.address,
            startingTick - 4 * tickSpacing,
            startingTick - 2 * tickSpacing,
            expandTo18Decimals(1)
          )
          await swapExact0For1(expandTo18Decimals(1), wallet.address)
          await swapToHigherPrice(startingPrice, wallet.address)
          await pool.advanceTime(1)
          await snapshotGasCost(swapExact0For1(expandTo18Decimals(1), wallet.address))
          expect((await pool.slot0()).tick).to.be.lt(tickSpacing * -4)
        })
      })

      describe('#mint', () => {
        for (const { description, tickLower, tickUpper } of [
          {
            description: 'around current price',
            tickLower: startingTick - tickSpacing,
            tickUpper: startingTick + tickSpacing,
          },
          {
            description: 'below current price',
            tickLower: startingTick - 2 * tickSpacing,
            tickUpper: startingTick - tickSpacing,
          },
          {
            description: 'above current price',
            tickLower: startingTick + tickSpacing,
            tickUpper: startingTick + 2 * tickSpacing,
          },
        ]) {
          describe(description, () => {
            it('new position mint first in range', async () => {
              await snapshotGasCost(mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1)))
            })
            it('add to position existing', async () => {
              await mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1))
              await snapshotGasCost(mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1)))
            })
            it('second position in same range', async () => {
              await mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1))
              await snapshotGasCost(mint(other.address, tickLower, tickUpper, expandTo18Decimals(1)))
            })
            it('add to position after some time passes', async () => {
              await mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1))
              await pool.advanceTime(1)
              await snapshotGasCost(mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1)))
            })
          })
        }
      })

      describe('#burn', () => {
        for (const { description, tickLower, tickUpper } of [
          {
            description: 'around current price',
            tickLower: startingTick - tickSpacing,
            tickUpper: startingTick + tickSpacing,
          },
          {
            description: 'below current price',
            tickLower: startingTick - 2 * tickSpacing,
            tickUpper: startingTick - tickSpacing,
          },
          {
            description: 'above current price',
            tickLower: startingTick + tickSpacing,
            tickUpper: startingTick + 2 * tickSpacing,
          },
        ]) {
          describe(description, () => {
            const liquidityAmount = expandTo18Decimals(1)
            beforeEach('mint a position', async () => {
              await mint(wallet.address, tickLower, tickUpper, liquidityAmount)
            })

            it('burn when only position using ticks', async () => {
              await snapshotGasCost(pool.burn(tickLower, tickUpper, expandTo18Decimals(1)))
            })
            it('partial position burn', async () => {
              await snapshotGasCost(pool.burn(tickLower, tickUpper, expandTo18Decimals(1).div(2)))
            })
            it('entire position burn but other positions are using the ticks', async () => {
              await mint(other.address, tickLower, tickUpper, expandTo18Decimals(1))
              await snapshotGasCost(pool.burn(tickLower, tickUpper, expandTo18Decimals(1)))
            })
            it('burn entire position after some time passes', async () => {
              await pool.advanceTime(1)
              await snapshotGasCost(pool.burn(tickLower, tickUpper, expandTo18Decimals(1)))
            })
          })
        }
      })

      describe('#poke', () => {
        const tickLower = startingTick - tickSpacing
        const tickUpper = startingTick + tickSpacing

        it('best case', async () => {
          await mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1))
          await swapExact0For1(expandTo18Decimals(1).div(100), wallet.address)
          await pool.burn(tickLower, tickUpper, 0)
          await swapExact0For1(expandTo18Decimals(1).div(100), wallet.address)
          await snapshotGasCost(pool.burn(tickLower, tickUpper, 0))
        })
      })

      describe('#collect', () => {
        const tickLower = startingTick - tickSpacing
        const tickUpper = startingTick + tickSpacing

        it('close to worst case', async () => {
          await mint(wallet.address, tickLower, tickUpper, expandTo18Decimals(1))
          await swapExact0For1(expandTo18Decimals(1).div(100), wallet.address)
          await pool.burn(tickLower, tickUpper, 0) // poke to accumulate fees
          await snapshotGasCost(pool.collect(wallet.address, tickLower, tickUpper, MaxUint128, MaxUint128))
        })
      })

      describe('#increaseObservationCardinalityNext', () => {
        it('grow by 1 slot', async () => {
          await snapshotGasCost(pool.increaseObservationCardinalityNext(5))
        })
        it('no op', async () => {
          await snapshotGasCost(pool.increaseObservationCardinalityNext(3))
        })
      })

      describe('#snapshotCumulativesInside', () => {
        it('tick inside', async () => {
          await snapshotGasCost(pool.estimateGas.snapshotCumulativesInside(minTick, maxTick))
        })
        it('tick above', async () => {
          await swapToHigherPrice(MAX_SQRT_RATIO.sub(1), wallet.address)
          await snapshotGasCost(pool.estimateGas.snapshotCumulativesInside(minTick, maxTick))
        })
        it('tick below', async () => {
          await swapToLowerPrice(MIN_SQRT_RATIO.add(1), wallet.address)
          await snapshotGasCost(pool.estimateGas.snapshotCumulativesInside(minTick, maxTick))
        })
      })
    })
  }
})
