import _ from 'lodash'
import { solidity } from "ethereum-waffle"
import chai from "chai"
import { BigNumber, Signer, Wallet, constants, utils } from "ethers"
import { ethers, deployments } from "hardhat"
import {
  TestERC721,
  Conduit,
  BNPL,
  TestERC20,
  ERC4907,
} from '../build/typechain'
import { increaseTimestamp } from './testUtils'
import { orderType } from '../eip-712-types/order'
import { OrderComponents } from '../utils/types'
import { calculateOrderHash } from '../utils/encoding'

chai.use(solidity)
const { expect } = chai
const { get } = deployments

describe("BoostPool", () => {
  let deployer: Wallet
  let alice: Wallet
  let bob: Wallet
  let cat: Wallet
  let attacker: Wallet
  let deployerAddress: string
  let aliceAddress: string
  let bobAddress: string
  let catAddress: string
  let conduit: Conduit
  let bnpl: BNPL
  let erc721: TestERC721
  let erc20: TestERC20
  let erc4907: ERC4907
  let chainId: string
  let conduitKey: string
  const ERC721_TokenId = 100
  const ERC4907_TokenId = 0

  const setupTest = deployments.createFixture(
    async ({ deployments, ethers, getChainId }) => {
      await deployments.fixture()

      const signers = await ethers.getSigners();
      [ deployer, alice, bob, cat, attacker ] = signers

      deployerAddress = await deployer.getAddress()
      aliceAddress = await alice.getAddress()
      bobAddress = await bob.getAddress()
      catAddress = await cat.getAddress()

      conduit = (await ethers.getContractAt(
        "Conduit",
        (await get("ConduitControllerCreateConduit")).address
      )) as Conduit

      bnpl = (await ethers.getContractAt(
        "BNPL",
        (await get("BNPL")).address
      )) as BNPL

      erc721 = (await ethers.getContractAt(
        "TestERC721",
        (await get("TestERC721")).address
      )) as TestERC721

      erc20 = (await ethers.getContractAt(
        "TestERC20",
        (await get("TestERC20")).address
      )) as TestERC20

      erc4907 = (await ethers.getContractAt(
        "ERC4907",
        (await get("ERC4907")).address
      )) as ERC4907

      chainId = await getChainId()

      await erc721.mint(aliceAddress, ERC721_TokenId)
      await erc721.connect(alice).setApprovalForAll(conduit.address, true)

      await erc20.mint(aliceAddress, utils.parseEther('10000'))
      await erc20.mint(deployerAddress, utils.parseEther('10000'))

      await erc20.approve(conduit.address, constants.MaxUint256)

      conduitKey = `${deployerAddress}000000000000000000000000`
    }
  )

  const getAndVerifyOrderHash = async (orderComponents: OrderComponents) => {
    const orderHash = await bnpl.getOrderHash(orderComponents)
    const derivedOrderHash = calculateOrderHash(orderComponents)
    expect(orderHash).to.equal(derivedOrderHash)
    return orderHash
  }

  const signOrder = async (
    orderComponents: OrderComponents,
    signer: Wallet
  ) => {
    const domainData = {
      name: "BNPL",
      version: "1.0",
      chainId,
      verifyingContract: bnpl.address,
    }

    const signature = await signer._signTypedData(
      domainData,
      orderType,
      orderComponents
    );

    const orderHash = await getAndVerifyOrderHash(orderComponents);

    const { domainSeparator } = await bnpl.information();
    const digest = utils.keccak256(
      `0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}`
    );
    const recoveredAddress = utils.recoverAddress(digest, signature);

    expect(recoveredAddress).to.equal(signer.address);

    return signature;
  }

  const getETHOrderComponents = () => {
    const total = utils.parseEther('1.0')
    const royalty = total.mul(5).div(100)
    const fee = total.mul(1).div(100)
    const withdrawFee = total.mul(5).div(1000)
    return {
      offerer: aliceAddress,
      token: erc721.address,
      identifier: BigNumber.from(ERC721_TokenId),
      currency: constants.AddressZero,
      artist: catAddress,
      platform: bobAddress,
      startTime: 0,
      endTime: constants.MaxUint256,
      duration: 3600,
      periods: 3,
      amount: total,
      ratio: '7000',
      royalty: royalty,
      fee: fee,
      withdrawFee: withdrawFee,
      salt: constants.HashZero,
      conduitKey,
      counter: BigNumber.from(0)
    }
  }

  const getERC20OrderComponents = () => {
    const total = utils.parseEther('1.0')
    const royalty = total.mul(5).div(100)
    const fee = total.mul(1).div(100)
    const withdrawFee = total.mul(5).div(1000)
    return {
      offerer: aliceAddress,
      token: erc721.address,
      identifier: BigNumber.from(ERC721_TokenId),
      currency: erc20.address,
      artist: catAddress,
      platform: bobAddress,
      startTime: 0,
      endTime: constants.MaxUint256,
      duration: 3600,
      periods: 3,
      amount: total,
      ratio: '7000',
      royalty: royalty,
      fee: fee,
      withdrawFee: withdrawFee,
      salt: constants.HashZero,
      conduitKey,
      counter: BigNumber.from(0)
    }
  }

  beforeEach(async () => {
    await setupTest()
  })

  describe("fulfillOrder", () => {
    it('signature', async () => {
      const orderComponents = getETHOrderComponents()
      await signOrder(orderComponents, alice)
    })

    it('eth fulfillOrder', async () => {
      const orderComponents = getETHOrderComponents()
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await alice.getBalance()
      const bobBalanceBefore = await bob.getBalance()
      const catBalanceBefore = await cat.getBalance()

      const orderHash = calculateOrderHash(orderComponents)

      let status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(false)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(false)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(constants.AddressZero)
      expect(status.startedAt).eq(0)
      expect(status.shadowId).eq(0)
      expect(status.paidTimes).eq(0)

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey, { value: utils.parseEther('1.0') })

      const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
      const toOfferer = orderComponents.amount
        .div(orderComponents.periods)
        .mul(orderComponents.ratio)
        .div(10000)
        .sub(orderComponents.withdrawFee)
        .sub(orderComponents.royalty.div(3))

      expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(catBalanceBefore)
      expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))

      status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(false)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(1)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
    })
  })

  describe("repayOrder", () => {
    it('repay', async () => {
      const orderComponents = getETHOrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await alice.getBalance()
      const bobBalanceBefore = await bob.getBalance()
      const catBalanceBefore = await cat.getBalance()

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey, { value: utils.parseEther('1.0') })

      const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
      const toOfferer = orderComponents.amount
        .div(orderComponents.periods)
        .mul(orderComponents.ratio)
        .div(10000)
        .sub(orderComponents.withdrawFee)
        .sub(orderComponents.royalty.div(3))

      expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(catBalanceBefore)
      expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))

      await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        1,
        { value: utils.parseEther('1.0') }
      )

      const toPlatform2 = orderComponents.withdrawFee

      expect(await bob.getBalance()).eq(toPlatform.add(toPlatform2).add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(catBalanceBefore)
      expect(await alice.getBalance()).eq(toOfferer.mul(2).add(aliceBalanceBefore))

      let status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(false)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(2)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(bnpl.address)

      await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        1,
        { value: utils.parseEther('1.0') }
      )

      expect(await bob.getBalance()).eq(toPlatform.add(toPlatform2.mul(2)).add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(orderComponents.royalty.add(catBalanceBefore))
      expect(await alice.getBalance()).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(3)).sub(orderComponents.royalty).add(aliceBalanceBefore))

      status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(3)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(deployerAddress)
    })

    it('repay ERC20', async () => {
      const orderComponents = getERC20OrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
      const bobBalanceBefore = await erc20.balanceOf(bobAddress)
      const catBalanceBefore = await erc20.balanceOf(catAddress)

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey)

      const toPlatform = orderComponents.fee.add(orderComponents.withdrawFee)
      const toOfferer = orderComponents.amount
        .div(orderComponents.periods)
        .mul(orderComponents.ratio)
        .div(10000)
        .sub(orderComponents.withdrawFee)
        .sub(orderComponents.royalty.div(3))

      expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(bobBalanceBefore))
      expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
      expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.add(aliceBalanceBefore))

      await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        1
      )

      const toPlatform2 = orderComponents.withdrawFee

      expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(toPlatform2).add(bobBalanceBefore))
      expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
      expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.mul(2).add(aliceBalanceBefore))

      let status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(false)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(2)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(deployerAddress)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(bnpl.address)

      const tx = await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        1
      )
      const rx = await tx.wait()
      console.log(rx.gasUsed.toString())
      

      expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(toPlatform2.mul(2)).add(bobBalanceBefore))
      expect(await erc20.balanceOf(catAddress)).eq(orderComponents.royalty.add(catBalanceBefore))
      expect(await erc20.balanceOf(aliceAddress)).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(3)).sub(orderComponents.royalty).add(aliceBalanceBefore))

      status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(3)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(deployerAddress)
    })

    it('repay all', async () => {
      const orderComponents = getETHOrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await alice.getBalance()
      const bobBalanceBefore = await bob.getBalance()
      const catBalanceBefore = await cat.getBalance()

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey, { value: utils.parseEther('1.0') })

      await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        2,
        { value: utils.parseEther('1.0') }
      )

      expect(await bob.getBalance()).eq(orderComponents.withdrawFee.mul(2).add(orderComponents.fee).add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(orderComponents.royalty.add(catBalanceBefore))
      expect(await alice.getBalance()).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(2)).sub(orderComponents.royalty).add(aliceBalanceBefore))

      let status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(3)

      expect(await erc721.ownerOf(100)).eq(deployerAddress)
      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
    })

    it('repay all ERC20', async () => {
      const orderComponents = getERC20OrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
      const bobBalanceBefore = await erc20.balanceOf(bobAddress)
      const catBalanceBefore = await erc20.balanceOf(catAddress)

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey)

      await bnpl.repayOrder(
        orderComponents,
        conduitKey,
        2
      )

      expect(await erc20.balanceOf(bobAddress)).eq(orderComponents.withdrawFee.mul(2).add(orderComponents.fee).add(bobBalanceBefore))
      expect(await erc20.balanceOf(catAddress)).eq(orderComponents.royalty.add(catBalanceBefore))
      expect(await erc20.balanceOf(aliceAddress)).eq(orderComponents.amount.sub(orderComponents.withdrawFee.mul(2)).sub(orderComponents.royalty).add(aliceBalanceBefore))

      let status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.isBroken).eq(false)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(3)

      expect(await erc721.ownerOf(100)).eq(deployerAddress)
      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
    })
  })

  describe("breakOrder", () => {
    it('break', async () => {
      const orderComponents = getETHOrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await alice.getBalance()
      const bobBalanceBefore = await bob.getBalance()
      const catBalanceBefore = await cat.getBalance()

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey, { value: utils.parseEther('1.0') })

      await increaseTimestamp(3601)

      await bnpl.breakOrder(orderComponents)

      const toOfferer = orderComponents.amount
        .div(orderComponents.periods)
        .mul(orderComponents.ratio)
        .div(10000)
        .sub(orderComponents.withdrawFee)

      const toPlatform = orderComponents.withdrawFee
        .add(orderComponents.fee)
        .add(orderComponents.amount.div(orderComponents.periods)
          .sub(orderComponents.amount
            .div(orderComponents.periods)
            .mul(orderComponents.ratio)
            .div(10000)
          )
        )

      expect(await bob.getBalance()).eq(toPlatform.add(bobBalanceBefore))
      expect(await cat.getBalance()).eq(catBalanceBefore)
      expect(await alice.getBalance()).eq(toOfferer.add(aliceBalanceBefore))

      const status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.isBroken).eq(true)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(1)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(aliceAddress)
    })

    it('break ERC20', async () => {
      const orderComponents = getERC20OrderComponents()
      const orderHash = calculateOrderHash(orderComponents)
      const signature = await signOrder(orderComponents, alice)

      const aliceBalanceBefore = await erc20.balanceOf(aliceAddress)
      const bobBalanceBefore = await erc20.balanceOf(bobAddress)
      const catBalanceBefore = await erc20.balanceOf(catAddress)

      await bnpl.fulfillOrder({
        parameters: orderComponents,
        signature
      }, conduitKey)

      await increaseTimestamp(3601)

      await bnpl.breakOrder(orderComponents)

      const toOfferer = orderComponents.amount
        .div(orderComponents.periods)
        .mul(orderComponents.ratio)
        .div(10000)
        .sub(orderComponents.withdrawFee)

      const toPlatform = orderComponents.withdrawFee
        .add(orderComponents.fee)
        .add(orderComponents.amount.div(orderComponents.periods)
          .sub(orderComponents.amount
            .div(orderComponents.periods)
            .mul(orderComponents.ratio)
            .div(10000)
          )
        )

      expect(await erc20.balanceOf(bobAddress)).eq(toPlatform.add(bobBalanceBefore))
      expect(await erc20.balanceOf(catAddress)).eq(catBalanceBefore)
      expect(await erc20.balanceOf(aliceAddress)).eq(toOfferer.add(aliceBalanceBefore))

      const status = await bnpl.getOrderStatus(orderHash)
      expect(status.isValidated).eq(true)
      expect(status.isCancelled).eq(false)
      expect(status.isFinalized).eq(true)
      expect(status.isBroken).eq(true)
      expect(status.fulfiller).eq(deployerAddress)
      expect(status.startedAt).gt(0)
      expect(status.shadowId).eq(ERC4907_TokenId)
      expect(status.paidTimes).eq(1)

      expect(await erc4907.userOf(ERC4907_TokenId)).eq(constants.AddressZero)
      expect(await erc721.ownerOf(ERC721_TokenId)).eq(aliceAddress)
    })
  })

  describe("cancelOrder", () => {
    
  })
})