import { Fixture } from 'ethereum-waffle'
import { constants, Wallet, Contract, BigNumber } from 'ethers'
import { ethers, waffle } from 'hardhat'
import { MixedRouteQuoterV1, TestERC20 } from '../typechain'
import completeFixture from './shared/completeFixture'
import { FeeAmount, CLASSIC_FEE_PLACEHOLDER } from './shared/constants'
import { encodePriceSqrt } from './shared/encodePriceSqrt'
import { expandTo18Decimals } from './shared/expandTo18Decimals'
import { expect } from './shared/expect'
import { encodePath } from './shared/path'
import {
  createPair,
  createPool,
  createPoolWithMultiplePositions,
  createPoolWithZeroTickInitialized,
} from './shared/quoter'
import snapshotGasCost from './shared/snapshotGasCost'

import { abi as PAIR_V2_ABI } from '@airdao/astra-contracts/artifacts/contracts/core/AstraPair.sol/AstraPair.json'

const V3_MAX_FEE = 999999 // = 1_000_000 - 1 since must be < 1_000_000

describe('MixedRouteQuoterV1', function () {
  this.timeout(40000)
  let wallet: Wallet
  let trader: Wallet

  const swapRouterFixture: Fixture<{
    nft: Contract
    factoryClassic: Contract
    tokens: [TestERC20, TestERC20, TestERC20]
    quoter: MixedRouteQuoterV1
  }> = async (wallets, provider) => {
    const { samb, factory, factoryClassic, router, tokens, nft } = await completeFixture(wallets, provider)

    // approve & fund wallets
    for (const token of tokens) {
      await token.approve(router.address, constants.MaxUint256)
      await token.approve(nft.address, constants.MaxUint256)
      await token.connect(trader).approve(router.address, constants.MaxUint256)
      await token.transfer(trader.address, expandTo18Decimals(1_000_000))
    }

    const quoterFactory = await ethers.getContractFactory('MixedRouteQuoterV1')
    quoter = (await quoterFactory.deploy(factory.address, factoryClassic.address, samb.address)) as MixedRouteQuoterV1

    return {
      tokens,
      nft,
      factoryClassic,
      quoter,
    }
  }

  let nft: Contract
  let factoryClassic: Contract
  let tokens: [TestERC20, TestERC20, TestERC20]
  let quoter: MixedRouteQuoterV1

  let pair01Address, pair02Address, pair12Address: string

  let loadFixture: ReturnType<typeof waffle.createFixtureLoader>

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

  // helper for getting weth and token balances
  beforeEach('load fixture', async () => {
    ;({ tokens, nft, factoryClassic, quoter } = await loadFixture(swapRouterFixture))
  })

  const addLiquidityV2 = async (
    pairAddress: string,
    token0: TestERC20,
    token1: TestERC20,
    amount0: string,
    amount1: string
  ) => {
    const pair = new Contract(pairAddress, PAIR_V2_ABI, wallet)
    expect(await pair.callStatic.token0()).to.equal(token0.address)
    expect(await pair.callStatic.token1()).to.equal(token1.address)
    // seed the pairs with liquidity

    const [reserve0Before, reserve1Before]: [BigNumber, BigNumber] = await pair.callStatic.getReserves()

    const token0BalanceBefore = await token0.balanceOf(pairAddress)
    const token1BalanceBefore = await token1.balanceOf(pairAddress)

    await token0.transfer(pairAddress, ethers.utils.parseEther(amount0))
    await token1.transfer(pairAddress, ethers.utils.parseEther(amount1))

    expect(await token0.balanceOf(pairAddress)).to.equal(token0BalanceBefore.add(ethers.utils.parseEther(amount0)))
    expect(await token1.balanceOf(pairAddress)).to.equal(token1BalanceBefore.add(ethers.utils.parseEther(amount1)))

    await pair.mint(wallet.address) // update the reserves

    const [reserve0, reserve1] = await pair.callStatic.getReserves()
    expect(reserve0).to.equal(reserve0Before.add(ethers.utils.parseEther(amount0)))
    expect(reserve1).to.equal(reserve1Before.add(ethers.utils.parseEther(amount1)))
  }

  describe('quotes', () => {
    beforeEach(async () => {
      await createPool(nft, wallet, tokens[0].address, tokens[1].address)
      await createPool(nft, wallet, tokens[1].address, tokens[2].address)
      await createPoolWithMultiplePositions(nft, wallet, tokens[0].address, tokens[2].address)
      /// @dev Create V2 Pairs
      pair01Address = await createPair(factoryClassic, tokens[0].address, tokens[1].address)
      pair12Address = await createPair(factoryClassic, tokens[1].address, tokens[2].address)
      pair02Address = await createPair(factoryClassic, tokens[0].address, tokens[2].address)

      await addLiquidityV2(pair01Address, tokens[0], tokens[1], '1000000', '1000000')
      await addLiquidityV2(pair12Address, tokens[1], tokens[2], '1000000', '1000000')
      await addLiquidityV2(pair02Address, tokens[0], tokens[2], '1000000', '1000000')
    })

    /// @dev Test running the old suite on the new function but with protocolFlags only being V3[]
    describe('#quoteExactInput V3 only', () => {
      it('0 -> 2 cross 2 tick', async () => {
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
          10000
        )

        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
        expect(CLInitializedTicksCrossedList[0]).to.eq(2)
        expect(amountOut).to.eq(9871)
        await snapshotGasCost(CLSwapGasEstimate)
      })

      it('0 -> 2 cross 2 tick where after is initialized', async () => {
        // The swap amount is set such that the active tick after the swap is -120.
        // -120 is an initialized tick for this pool. We check that we don't count it.
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
          6200
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('78757224507315167622282810783')
        expect(CLInitializedTicksCrossedList.length).to.eq(1)
        expect(CLInitializedTicksCrossedList[0]).to.eq(1)
        expect(amountOut).to.eq(6143)
      })

      it('0 -> 2 cross 1 tick', async () => {
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
          4000
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(1)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('78926452400586371254602774705')
        expect(amountOut).to.eq(3971)
      })

      it('0 -> 2 cross 0 tick, starting tick not initialized', async () => {
        // Tick before 0, tick after -1.
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
          10
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(0)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('79227483487511329217250071027')
        expect(amountOut).to.eq(8)
      })

      it('0 -> 2 cross 0 tick, starting tick initialized', async () => {
        // Tick before 0, tick after -1. Tick 0 initialized.
        await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)

        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
          10
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(1)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('79227817515327498931091950511')
        expect(amountOut).to.eq(8)
      })

      it('2 -> 0 cross 2', async () => {
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
          10000
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(2)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('80001962924147897865541384515')
        expect(CLInitializedTicksCrossedList.length).to.eq(1)
        expect(amountOut).to.eq(9871)
      })

      it('2 -> 0 cross 2 where tick after is initialized', async () => {
        // The swap amount is set such that the active tick after the swap is 120.
        // 120 is an initialized tick for this pool. We check we don't count it.
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
          6250
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(2)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('79705728824507063507279123685')
        expect(CLInitializedTicksCrossedList.length).to.eq(1)
        expect(amountOut).to.eq(6190)
      })

      it('2 -> 0 cross 0 tick, starting tick initialized', async () => {
        // Tick 0 initialized. Tick after = 1
        await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)

        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
          200
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(0)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('79235729830182478001034429156')
        expect(CLInitializedTicksCrossedList.length).to.eq(1)
        expect(amountOut).to.eq(198)
      })

      it('2 -> 0 cross 0 tick, starting tick not initialized', async () => {
        // Tick 0 initialized. Tick after = 1
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
          103
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLInitializedTicksCrossedList[0]).to.eq(0)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('79235858216754624215638319723')
        expect(CLInitializedTicksCrossedList.length).to.eq(1)
        expect(amountOut).to.eq(101)
      })

      it('2 -> 1', async () => {
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM]),
          10000
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLSqrtPriceX96AfterList.length).to.eq(1)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('80018067294531553039351583520')
        expect(CLInitializedTicksCrossedList[0]).to.eq(0)
        expect(amountOut).to.eq(9871)
      })

      it('0 -> 2 -> 1', async () => {
        const {
          amountOut,
          CLSqrtPriceX96AfterList,
          CLInitializedTicksCrossedList,
          CLSwapGasEstimate,
        } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM, FeeAmount.MEDIUM]),
          10000
        )

        await snapshotGasCost(CLSwapGasEstimate)
        expect(CLSqrtPriceX96AfterList.length).to.eq(2)
        expect(CLSqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
        expect(CLSqrtPriceX96AfterList[1]).to.eq('80007846861567212939802016351')
        expect(CLInitializedTicksCrossedList[0]).to.eq(2)
        expect(CLInitializedTicksCrossedList[1]).to.eq(0)
        expect(amountOut).to.eq(9745)
      })
    })

    /// @dev Test running the old suite on the new function but with protocolFlags only being V2[]
    describe('#quoteExactInput V2 only', () => {
      it('0 -> 2', async () => {
        const { amountOut, CLSwapGasEstimate } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath([tokens[0].address, tokens[2].address], [CLASSIC_FEE_PLACEHOLDER]),
          10000
        )

        expect(amountOut).to.eq(9969)
      })

      it('0 -> 1 -> 2', async () => {
        const { amountOut, CLSwapGasEstimate } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
          encodePath(
            [tokens[0].address, tokens[1].address, tokens[2].address],
            [CLASSIC_FEE_PLACEHOLDER, CLASSIC_FEE_PLACEHOLDER]
          ),
          10000
        )

        expect(amountOut).to.eq(9939)
      })
    })

    /// @dev Test copied over from QuoterV2.spec.ts
    describe('#quoteExactInputSingle V3', () => {
      it('0 -> 2', async () => {
        const {
          amountOut: quote,
          sqrtPriceX96After,
          initializedTicksCrossed,
          gasEstimate,
        } = await quoter.callStatic.quoteExactInputSingleCL({
          tokenIn: tokens[0].address,
          tokenOut: tokens[2].address,
          fee: FeeAmount.MEDIUM,
          amountIn: 10000,
          // -2%
          sqrtPriceLimitX96: encodePriceSqrt(100, 102),
        })

        await snapshotGasCost(gasEstimate)
        expect(initializedTicksCrossed).to.eq(2)
        expect(quote).to.eq(9871)
        expect(sqrtPriceX96After).to.eq('78461846509168490764501028180')
      })

      it('2 -> 0', async () => {
        const {
          amountOut: quote,
          sqrtPriceX96After,
          initializedTicksCrossed,
          gasEstimate,
        } = await quoter.callStatic.quoteExactInputSingleCL({
          tokenIn: tokens[2].address,
          tokenOut: tokens[0].address,
          fee: FeeAmount.MEDIUM,
          amountIn: 10000,
          // +2%
          sqrtPriceLimitX96: encodePriceSqrt(102, 100),
        })

        await snapshotGasCost(gasEstimate)
        expect(initializedTicksCrossed).to.eq(2)
        expect(quote).to.eq(9871)
        expect(sqrtPriceX96After).to.eq('80001962924147897865541384515')
      })
    })

    /// @dev Test the new function for fetching a single V2 pair quote on chain (exactIn)
    describe('#quoteExactInputSingleClassic', () => {
      it('0 -> 2', async () => {
        const amountIn = 10000
        const tokenIn = tokens[0].address
        const tokenOut = tokens[2].address
        const quote = await quoter.callStatic.quoteExactInputSingleClassic({ tokenIn, tokenOut, amountIn })

        expect(quote).to.eq(9969)
      })

      it('2 -> 0', async () => {
        const amountIn = 10000
        const tokenIn = tokens[2].address
        const tokenOut = tokens[0].address
        const quote = await quoter.callStatic.quoteExactInputSingleClassic({ tokenIn, tokenOut, amountIn })

        expect(quote).to.eq(9969)
      })

      describe('+ with imbalanced pairs', () => {
        before(async () => {
          await addLiquidityV2(pair12Address, tokens[1], tokens[2], '1000000', '1000')
        })

        it('1 -> 2', async () => {
          const amountIn = 2_000_000
          const tokenIn = tokens[1].address
          const tokenOut = tokens[2].address
          const quote = await quoter.callStatic.quoteExactInputSingleClassic({ tokenIn, tokenOut, amountIn })

          expect(quote).to.eq(1993999)
        })
      })
    })

    describe('testing bit masking for protocol selection', () => {
      it('when given the max v3 fee, should still route v3 and revert because pool DNE', async () => {
        /// @define 999999 is the max fee that can be set on a V3 pool per the factory
        ///       in this environment this pool does not exist, and thus the call should revert
        ///     - however, if the bitmask fails to catch this the call will succeed and route to V2
        ///     - thus, we expect it to be reverted.
        await expect(
          quoter.callStatic['quoteExactInput(bytes,uint256)'](
            encodePath([tokens[0].address, tokens[1].address], [V3_MAX_FEE]),
            10000
          )
        ).to.be.reverted
      })
    })
  })
})
