import { expect } from 'chai'
import { constants } from 'ethers'
import hre, { ethers } from 'hardhat'
import { TokenValidator, TestERC20, IAstraPair__factory } from '../typechain'

describe('TokenValidator', function () {
  let tokenValidator: TokenValidator
  let testToken: TestERC20

  this.timeout(100000)

  enum Status {
    UNKN = 0,
    FOT = 1,
    STF = 2,
  }

  // SAMB and USDC
  const BASE_TOKENS = ['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48']
  // Arbitrary amount to flash loan.
  const AMOUNT_TO_BORROW = 1000

  const FOT_TOKENS = [
    '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
    '0x8B3192f5eEBD8579568A2Ed41E6FEB402f93f73F', // SAITAMA
    '0xA2b4C0Af19cC16a6CfAcCe81F192B024d625817D', // KISHU
  ]

  const BROKEN_TOKENS = [
    '0xd233d1f6fd11640081abb8db125f722b5dc729dc', // USD
  ]

  const NON_FOT_TOKENS = [
    '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
    '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', // UNI
    '0xc00e94Cb662C3520282E6f5717214004A7f26888', // COMP
    '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // SAMB
  ]

  before(async function () {
    // Easiest to test FOT using real world data, so these tests require a hardhat fork.
    if (!process.env.ARCHIVE_RPC_URL) {
      this.skip()
    }

    await hre.network.provider.request({
      method: 'hardhat_reset',
      params: [
        {
          forking: {
            jsonRpcUrl: process.env.ARCHIVE_RPC_URL,
            blockNumber: 14024832,
          },
        },
      ],
    })

    const factory = await ethers.getContractFactory('TokenValidator')
    tokenValidator = (await factory.deploy(
      '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', // V2 Factory
      '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' // V3 NFT position manager
    )) as TokenValidator

    // Deploy a new token for testing.
    const tokenFactory = await ethers.getContractFactory('TestERC20')
    testToken = (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20
  })

  after(async () => {
    // Disable mainnet forking to avoid effecting other tests.
    await hre.network.provider.request({
      method: 'hardhat_reset',
      params: [],
    })
  })

  it('succeeds for tokens that cant be transferred', async () => {
    for (const token of BROKEN_TOKENS) {
      const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW)
      expect(isFot).to.equal(Status.STF)
    }
  })

  it('succeeds to detect fot tokens', async () => {
    for (const token of FOT_TOKENS) {
      const isFot = await tokenValidator.callStatic.validate(token, [BASE_TOKENS[0]!], AMOUNT_TO_BORROW)
      expect(isFot).to.equal(Status.FOT)
    }
  })

  it('succeeds to detect fot token when token doesnt have pair with first base token', async () => {
    const isFot = await tokenValidator.callStatic.validate(
      FOT_TOKENS[0],
      [testToken.address, ...BASE_TOKENS],
      AMOUNT_TO_BORROW
    )
    expect(isFot).to.equal(Status.FOT)
  })

  it('succeeds to return unknown when flash loaning full reserves', async () => {
    const pairAddress = '0xab293dce330b92aa52bc2a7cd3816edaa75f890b' // WTF/ETH pair
    const pair = IAstraPair__factory.connect(pairAddress, ethers.provider)
    const { reserve0: wtfReserve } = await pair.callStatic.getReserves()

    const isFot1 = await tokenValidator.callStatic.validate(
      '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
      ['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // SAMB
      wtfReserve.sub(1).toString()
    )
    expect(isFot1).to.equal(Status.FOT)

    const isFot2 = await tokenValidator.callStatic.validate(
      '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
      ['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // SAMB
      wtfReserve.toString()
    )
    expect(isFot2).to.equal(Status.UNKN)
  })

  it('succeeds to batch detect fot tokens', async () => {
    const isFots = await tokenValidator.callStatic.batchValidate(FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW)
    expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true
  })

  it('succeeds to batch detect fot tokens when dont have pair with first base token', async () => {
    const isFots = await tokenValidator.callStatic.batchValidate(
      FOT_TOKENS,
      [testToken.address, ...BASE_TOKENS],
      AMOUNT_TO_BORROW
    )
    expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true
  })

  it('succeeds to detect non fot tokens', async () => {
    for (const token of NON_FOT_TOKENS) {
      const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW)
      expect(isFot).to.equal(Status.UNKN)
    }
  })

  it('succeeds to batch detect non fot tokens', async () => {
    const isFots = await tokenValidator.callStatic.batchValidate(NON_FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW)
    expect(isFots.every((isFot: Status) => isFot == Status.UNKN)).to.be.true
  })

  it('succeeds to batch detect mix of fot tokens and non fot tokens', async () => {
    const isFots = await tokenValidator.callStatic.batchValidate(
      [NON_FOT_TOKENS[0], FOT_TOKENS[0], BROKEN_TOKENS[0]],
      BASE_TOKENS,
      1000
    )
    expect(isFots).to.deep.equal([Status.UNKN, Status.FOT, Status.STF])
  })

  it('succeeds to return false if token doesnt have a pool with any of the base tokens', async () => {
    await tokenValidator.callStatic.validate(testToken.address, BASE_TOKENS, AMOUNT_TO_BORROW)
  })
})
