import {
  AccountUpdate,
  Bool,
  Cache,
  Field,
  MerkleMap,
  Mina,
  Poseidon,
  PrivateKey,
  Provable,
  PublicKey,
  Signature,
  UInt32,
  UInt64,
  UInt8,
  VerificationKey
} from "o1js"
import { beforeAll, beforeEach, describe, expect, it } from "vitest"

import { FungibleToken, FungibleTokenAdmin, minTimeUnlockFarmReward, Pool, PoolFactory, PoolTokenHolder } from "../dist"
import {
  claimerNumber,
  Farm,
  FarmMerkleWitness,
  FarmReward,
  FarmRewardTokenHolder,
  FarmTokenHolder,
  MultisigInfo,
  SignatureInfo,
  SignatureRight,
  UpdateSignerData
} from "../dist"

import { generateRewardMerkle } from "./Farm.Token.test"
import { FarmUpgradeTest } from "./FarmUpgradeTest"

const proofsEnabled = false

describe("Farming pool mina", () => {
  let deployerAccount: Mina.TestPublicKey,
    deployerKey: PrivateKey,
    senderAccount: Mina.TestPublicKey,
    senderKey: PrivateKey,
    bobAccount: Mina.TestPublicKey,
    bobKey: PrivateKey,
    merkle: MerkleMap,
    aliceAccount: Mina.TestPublicKey,
    aliceKey: PrivateKey,
    dylanAccount: Mina.TestPublicKey,
    bobPublic: PublicKey,
    alicePublic: PublicKey,
    dylanPublic: PublicKey,
    senderPublic: PublicKey,
    deployerPublic: PublicKey,
    allRight: SignatureRight,
    deployRight: SignatureRight,
    zkAppAddress: PublicKey,
    zkAppPrivateKey: PrivateKey,
    zkApp: PoolFactory,
    zkPoolMinaAddress: PublicKey,
    zkPoolMinaPrivateKey: PrivateKey,
    zkPoolMina: Pool,
    zkTokenAdminAddress: PublicKey,
    zkTokenAdminPrivateKey: PrivateKey,
    zkTokenAdmin: FungibleTokenAdmin,
    zkTokenAddress0: PublicKey,
    zkTokenPrivateKey0: PrivateKey,
    zkToken0: FungibleToken,
    zkTokenAddress1: PublicKey,
    zkTokenPrivateKey1: PrivateKey,
    zkToken1: FungibleToken,
    zkTokenAddress2: PublicKey,
    zkTokenPrivateKey2: PrivateKey,
    zkToken2: FungibleToken,
    zkFarmTokenAddress: PublicKey,
    zkFarmTokenPrivateKey: PrivateKey,
    zkFarmToken: Farm,
    zkFarmTokenHolder: FarmTokenHolder,
    local: any,
    compileKey: VerificationKey,
    tokenHolder: PoolTokenHolder

  beforeAll(async () => {
    const analyze = await Pool.analyzeMethods()
    getGates(analyze)

    const cache = Cache.FileSystem("./cache")
    const compileResult = await FarmUpgradeTest.compile({ cache })
    compileKey = compileResult.verificationKey

    if (proofsEnabled) {
      console.time("compile pool")
      await FungibleTokenAdmin.compile()
      await FungibleToken.compile()
      await PoolFactory.compile()
      await Pool.compile()
      await PoolTokenHolder.compile()
      await Farm.compile()
      console.timeEnd("compile pool")
    }

    function getGates(analyze: any) {
      for (const key in analyze) {
        if (Object.prototype.hasOwnProperty.call(analyze, key)) {
          const element = analyze[key]
          console.log(key, element?.gates.length)
        }
      }
    }
  })

  beforeEach(async () => {
    // no transaction limits on zeko
    local = await Mina.LocalBlockchain({ proofsEnabled })
    Mina.setActiveInstance(local)
    ;[deployerAccount, senderAccount, bobAccount, aliceAccount, dylanAccount] = local.testAccounts
    deployerKey = deployerAccount.key
    senderKey = senderAccount.key
    bobKey = bobAccount.key
    aliceKey = aliceAccount.key

    senderPublic = senderKey.toPublicKey()
    bobPublic = bobKey.toPublicKey()
    alicePublic = aliceKey.toPublicKey()
    dylanPublic = dylanAccount.key.toPublicKey()
    deployerPublic = deployerKey.toPublicKey()

    allRight = new SignatureRight(Bool(true), Bool(true), Bool(true), Bool(true), Bool(true), Bool(true))
    deployRight = SignatureRight.canDeployPool()

    zkAppPrivateKey = PrivateKey.random()
    zkAppAddress = zkAppPrivateKey.toPublicKey()
    zkApp = new PoolFactory(zkAppAddress)

    zkPoolMinaPrivateKey = PrivateKey.random()
    zkPoolMinaAddress = zkPoolMinaPrivateKey.toPublicKey()
    zkPoolMina = new Pool(zkPoolMinaAddress)

    zkFarmTokenPrivateKey = PrivateKey.random()
    zkFarmTokenAddress = zkFarmTokenPrivateKey.toPublicKey()
    zkFarmToken = new Farm(zkFarmTokenAddress)
    zkFarmTokenHolder = new FarmTokenHolder(zkFarmTokenAddress, zkPoolMina.deriveTokenId())

    zkTokenAdminPrivateKey = PrivateKey.random()
    zkTokenAdminAddress = zkTokenAdminPrivateKey.toPublicKey()
    zkTokenAdmin = new FungibleTokenAdmin(zkTokenAdminAddress)

    const keyTokenX = PrivateKey.random()
    const keyTokenY = PrivateKey.random()

    // order token to create pool
    const xIsLower = keyTokenX.toPublicKey().x.lessThan(keyTokenY.toPublicKey().x)

    zkTokenPrivateKey0 = xIsLower.toBoolean() ? keyTokenX : keyTokenY
    zkTokenAddress0 = zkTokenPrivateKey0.toPublicKey()
    zkToken0 = new FungibleToken(zkTokenAddress0)

    zkTokenPrivateKey1 = xIsLower.toBoolean() ? keyTokenY : keyTokenX
    zkTokenAddress1 = zkTokenPrivateKey1.toPublicKey()
    zkToken1 = new FungibleToken(zkTokenAddress1)

    zkTokenPrivateKey2 = PrivateKey.random()
    zkTokenAddress2 = zkTokenPrivateKey2.toPublicKey()
    zkToken2 = new FungibleToken(zkTokenAddress2)

    tokenHolder = new PoolTokenHolder(zkPoolMinaAddress, zkToken0.deriveTokenId())

    merkle = new MerkleMap()
    merkle.set(Poseidon.hash(bobPublic.toFields()), allRight.hash())
    merkle.set(Poseidon.hash(alicePublic.toFields()), allRight.hash())
    merkle.set(Poseidon.hash(senderPublic.toFields()), allRight.hash())
    merkle.set(Poseidon.hash(deployerPublic.toFields()), deployRight.hash())

    const root = merkle.getRoot()

    const today = new Date()
    today.setDate(today.getDate() + 1)
    const tomorrow = today.getTime()
    const time = getSlotFromTimestamp(tomorrow)
    const info = new UpdateSignerData({ oldRoot: Field.empty(), newRoot: root, deadlineSlot: UInt32.from(time) })

    const signBob = Signature.create(bobKey, info.toFields())
    const signAlice = Signature.create(aliceKey, info.toFields())
    const signDylan = Signature.create(senderAccount.key, info.toFields())

    const multi = new MultisigInfo({
      approvedUpgrader: root,
      messageHash: info.hash(),
      deadlineSlot: UInt32.from(time)
    })
    const infoBob = new SignatureInfo({
      user: bobPublic,
      witness: merkle.getWitness(Poseidon.hash(bobPublic.toFields())),
      signature: signBob,
      right: allRight
    })
    const infoAlice = new SignatureInfo({
      user: alicePublic,
      witness: merkle.getWitness(Poseidon.hash(alicePublic.toFields())),
      signature: signAlice,
      right: allRight
    })
    const infoDylan = new SignatureInfo({
      user: senderPublic,
      witness: merkle.getWitness(Poseidon.hash(senderPublic.toFields())),
      signature: signDylan,
      right: allRight
    })
    const array = [infoBob, infoAlice, infoDylan]

    const txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 4)
      await zkApp.deploy({
        symbol: "FAC",
        src: "https://luminadex.com/",
        protocol: aliceAccount,
        delegator: dylanAccount,
        approvedSigner: root,
        signatures: array,
        multisigInfo: multi
      })
      await zkTokenAdmin.deploy({
        adminPublicKey: deployerAccount
      })
      await zkToken0.deploy({
        symbol: "LTA",
        src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
        allowUpdates: false
      })
      await zkToken0.initialize(
        zkTokenAdminAddress,
        UInt8.from(9),
        Bool(false)
      )
    })
    await txn.prove()
    // this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
    await txn.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey0]).send()

    // deploy token 2
    const txn2 = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await zkToken1.deploy({
        symbol: "LTB",
        src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
        allowUpdates: false
      })
      await zkToken1.initialize(
        zkTokenAdminAddress,
        UInt8.from(9),
        Bool(false)
      )
    })
    await txn2.prove()
    // this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
    await txn2.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey1]).send()

    // deploy token 3
    const txn4 = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await zkToken2.deploy({
        symbol: "REW",
        src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
        allowUpdates: false
      })
      await zkToken2.initialize(
        zkTokenAdminAddress,
        UInt8.from(9),
        Bool(false)
      )
    })
    await txn4.prove()
    // this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
    await txn4.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey2]).send()

    const witness = merkle.getWitness(Poseidon.hash(bobPublic.toFields()))
    const signature2 = Signature.create(bobKey, zkPoolMinaAddress.toFields())
    const txn6 = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 4)
      await zkApp.createPool(zkPoolMinaAddress, zkTokenAddress1, bobAccount, signature2, witness, allRight)
    })
    console.log("Pool Mina creation au", txn6.transaction.accountUpdates.length)
    await txn6.prove()
    // this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
    await txn6.sign([deployerKey, zkPoolMinaPrivateKey]).send()

    const { genesisTimestamp, slotTime } = Mina.getNetworkConstants()

    let start = BigInt(Date.now() + 10_000)
    start = (start - genesisTimestamp.toBigInt()) / slotTime.toBigInt()
    let end = start + BigInt(1_000_000)
    end = end / slotTime.toBigInt()

    const txn10 = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await zkFarmToken.deploy({
        owner: deployerAccount,
        pool: zkPoolMinaAddress,
        startSlot: new UInt64(start),
        endSlot: new UInt64(end)
      })
      await zkFarmTokenHolder.deploy({
        owner: deployerAccount
      })
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    console.log("zkFarmToken deploy", txn10.toPretty())
    await txn10.prove()
    await txn10.sign([zkFarmTokenPrivateKey, deployerKey]).send()

    // mint token to user
    await mintToken(deployerAccount)
    await mintToken(senderAccount)
    await mintToken(bobAccount)
    await mintToken(aliceAccount)

    const amt = UInt64.from(10 * 10 ** 9)
    const amtToken = UInt64.from(50 * 10 ** 9)
    const txn5 = await Mina.transaction(senderAccount, async () => {
      AccountUpdate.fundNewAccount(senderAccount, 1)
      await zkPoolMina.supplyFirstLiquidities(amt, amtToken)
    })
    await txn5.prove()
    await txn5.sign([senderKey]).send()

    const amtMina = UInt64.from(1 * 10 ** 9)
    const amtToken2 = UInt64.from(5 * 10 ** 9)
    let totalLiquidity = Mina.getBalance(zkPoolMinaAddress, zkPoolMina.deriveTokenId())
    let amtToken0 = Mina.getBalance(zkPoolMinaAddress)
    let amtToken1 = Mina.getBalance(zkPoolMinaAddress, zkToken1.deriveTokenId())
    let txn8 = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await zkPoolMina.supplyLiquidity(amtMina, amtToken2, amtToken0, amtToken1, totalLiquidity)
    })
    await txn8.prove()
    await txn8.sign([bobKey]).send()

    totalLiquidity = Mina.getBalance(zkPoolMinaAddress, zkPoolMina.deriveTokenId())
    amtToken0 = Mina.getBalance(zkPoolMinaAddress)
    amtToken1 = Mina.getBalance(zkPoolMinaAddress, zkToken1.deriveTokenId())
    txn8 = await Mina.transaction(aliceAccount, async () => {
      AccountUpdate.fundNewAccount(aliceAccount, 1)
      await zkPoolMina.supplyLiquidity(amtMina, amtToken2, amtToken0, amtToken1, totalLiquidity)
    })
    await txn8.prove()
    await txn8.sign([aliceKey]).send()
  })

  it("get reward", async () => {
    let txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await zkFarmToken.deposit(UInt64.from(1000))
    })
    await txn.prove()
    await txn.sign([bobKey]).send()

    local.setGlobalSlot(1)
    txn = await Mina.transaction(bobAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([bobKey]).send()
    local.setGlobalSlot(2)
    txn = await Mina.transaction(aliceAccount, async () => {
      AccountUpdate.fundNewAccount(aliceAccount, 1)
      await zkFarmToken.deposit(UInt64.from(5000))
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(3)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmToken.deposit(UInt64.from(333))
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(5)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1333))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(10)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(4000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()

    const dataReward = await generateRewardMerkle(zkFarmTokenAddress, zkPoolMina.deriveTokenId(), claimerNumber)
    console.log("rewardList", dataReward.rewardList)

    const key = PrivateKey.random()
    const farmReward = new FarmReward(key.toPublicKey())
    const farmRewardTokenHolder = new FarmRewardTokenHolder(key.toPublicKey(), zkToken2.deriveTokenId())
    // add more token for test
    const amountReward = dataReward.totalReward * 10n
    const timeUnlock = UInt64.from(Date.now()).add(minTimeUnlockFarmReward.mul(2))

    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await farmReward.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await farmRewardTokenHolder.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
      // we deploy and fund reward in one transaction
      await zkToken2.transfer(deployerAccount, key.toPublicKey(), UInt64.from(amountReward))
    })
    // console.log("farmReward deploy", txn.toPretty());
    await txn.prove()
    await txn.sign([key, deployerKey]).send()

    // bob withdraw reward
    const balanceBeforeBob = Mina.getBalance(bobAccount, zkToken2.deriveTokenId())
    const indexBob = dataReward.rewardList.findIndex(x => x.user === bobAccount.toBase58())
    const dataBob = dataReward.rewardList[indexBob]
    const witness = dataReward.merkle.getWitness(BigInt(indexBob))
    const farmWitness = new FarmMerkleWitness(witness)
    txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await farmRewardTokenHolder.claimReward(UInt64.from(dataBob.reward), farmWitness)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    console.log("farmReward claim", txn.toPretty())
    await txn.prove()
    await txn.sign([bobKey]).send()

    const balanceAfterBob = Mina.getBalance(bobAccount, zkToken2.deriveTokenId())
    // bob received the correct amount
    expect(balanceAfterBob.sub(balanceBeforeBob).value).toEqual(Field.from(dataBob.reward))

    txn = await Mina.transaction(bobAccount, async () => {
      await farmRewardTokenHolder.claimReward(UInt64.from(dataBob.reward), farmWitness)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    await txn.prove()
    // can't claim twice
    await expect(txn.sign([bobKey]).send()).rejects.toThrow()

    const indexAlice = dataReward.rewardList.findIndex(x => x.user === aliceAccount.toBase58())
    const dataAlice = dataReward.rewardList[indexAlice]
    const witnessAlice = dataReward.merkle.getWitness(BigInt(indexAlice))
    const farmWitnessAlice = new FarmMerkleWitness(witnessAlice)

    // bob can'claim for alice
    await expect(Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await farmRewardTokenHolder.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })).rejects.toThrow()

    // alice claim
    txn = await Mina.transaction(aliceAccount, async () => {
      AccountUpdate.fundNewAccount(aliceAccount, 1)
      await farmRewardTokenHolder.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()

    const amountDust = Field.from(amountReward).sub(Field.from(dataBob.reward)).sub(Field.from(dataAlice.reward))
    const balanceContract = Mina.getBalance(key.toPublicKey(), zkToken2.deriveTokenId())
    expect(balanceContract.value).toEqual(amountDust)
  })

  it("get mina reward", async () => {
    // method who test mina as reward, reward can be mina or fungible token
    let txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await zkFarmToken.deposit(UInt64.from(1000))
    })
    await txn.prove()
    await txn.sign([bobKey]).send()

    local.setGlobalSlot(1)
    txn = await Mina.transaction(bobAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([bobKey]).send()
    local.setGlobalSlot(2)
    txn = await Mina.transaction(aliceAccount, async () => {
      AccountUpdate.fundNewAccount(aliceAccount, 1)
      await zkFarmToken.deposit(UInt64.from(5000))
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(3)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmToken.deposit(UInt64.from(333))
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(5)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1333))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()
    local.setGlobalSlot(10)
    txn = await Mina.transaction(aliceAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(4000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()

    const dataReward = await generateRewardMerkle(zkFarmTokenAddress, zkPoolMina.deriveTokenId(), claimerNumber)
    console.log("rewardList", dataReward.rewardList)

    const key = PrivateKey.random()
    const farmReward = new FarmReward(key.toPublicKey())
    // add more token for test
    const amountReward = dataReward.totalReward * 10n
    const timeUnlock = UInt64.from(Date.now()).add(minTimeUnlockFarmReward.mul(2))
    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 1)
      await farmReward.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: PublicKey.empty()
      })
      // we deploy and fund reward with mina in one transaction
      const accountUpdate = AccountUpdate.createSigned(deployerAccount)
      accountUpdate.send({ to: farmReward, amount: UInt64.from(amountReward) })
    })
    console.log("farmReward deploy", txn.toPretty())
    await txn.prove()
    await txn.sign([key, deployerKey]).send()

    // bob withdraw reward
    const balanceBeforeBob = Mina.getBalance(bobAccount)
    const indexBob = dataReward.rewardList.findIndex(x => x.user === bobAccount.toBase58())
    const dataBob = dataReward.rewardList[indexBob]
    const witness = dataReward.merkle.getWitness(BigInt(indexBob))
    const farmWitness = new FarmMerkleWitness(witness)
    txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 1)
      await farmReward.claimReward(UInt64.from(dataBob.reward), farmWitness)
    })
    console.log("farmReward claim", txn.toPretty())
    await txn.prove()
    await txn.sign([bobKey, deployerKey]).send()

    const balanceAfterBob = Mina.getBalance(bobAccount)
    // bob received the correct amount
    expect(balanceAfterBob.sub(balanceBeforeBob).value).toEqual(Field.from(dataBob.reward))

    txn = await Mina.transaction(bobAccount, async () => {
      await farmReward.claimReward(UInt64.from(dataBob.reward), farmWitness)
    })
    await txn.prove()
    // can't claim twice
    await expect(txn.sign([bobKey]).send()).rejects.toThrow()

    const indexAlice = dataReward.rewardList.findIndex(x => x.user === aliceAccount.toBase58())
    const dataAlice = dataReward.rewardList[indexAlice]
    const witnessAlice = dataReward.merkle.getWitness(BigInt(indexAlice))
    const farmWitnessAlice = new FarmMerkleWitness(witnessAlice)

    // bob can'claim for alice
    await expect(Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await farmReward.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
    })).rejects.toThrow()

    // alice claim
    txn = await Mina.transaction(aliceAccount, async () => {
      AccountUpdate.fundNewAccount(aliceAccount, 1)
      await farmReward.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
    })
    await txn.prove()
    await txn.sign([aliceKey]).send()

    const amountDust = Field.from(amountReward).sub(Field.from(dataBob.reward)).sub(Field.from(dataAlice.reward))
    const balanceContract = Mina.getBalance(key.toPublicKey())
    expect(balanceContract.value).toEqual(amountDust)
  })

  it("update verification key", async () => {
    let txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await zkFarmToken.deposit(UInt64.from(1000))
    })
    await txn.prove()
    await txn.sign([bobKey]).send()

    local.setGlobalSlot(1)
    txn = await Mina.transaction(bobAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([bobKey]).send()
    local.setGlobalSlot(2)

    const timeUnlock = Date.now() + 1000_000
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmTokenHolder.initUpdate(UInt64.from(timeUnlock))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    local.setGlobalSlot(200)
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmTokenHolder.updateVerificationKey(compileKey)
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    // need to wait 1 day to update
    await expect(txn.sign([deployerKey]).send()).rejects.toThrow()

    local.setGlobalSlot(1000)
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmTokenHolder.updateVerificationKey(compileKey)
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    const farmv2 = new FarmUpgradeTest(zkFarmTokenAddress, zkPoolMina.deriveTokenId())
    const version = await farmv2.version()
    expect(version?.toBigInt()).toEqual(112n)

    const dataReward = await generateRewardMerkle(zkFarmTokenAddress, zkPoolMina.deriveTokenId(), claimerNumber)
    console.log("rewardList", dataReward.rewardList)

    const key = PrivateKey.random()
    const farmReward = new FarmReward(key.toPublicKey())
    const farmRewardTokenHolder = new FarmRewardTokenHolder(key.toPublicKey(), zkToken2.deriveTokenId())
    // add more token for test
    const amountReward = dataReward.totalReward * 10n

    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await farmReward.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await farmRewardTokenHolder.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
      // we deploy and fund reward in one transaction
      await zkToken2.transfer(deployerAccount, key.toPublicKey(), UInt64.from(amountReward))
    })
    // console.log("farmReward deploy", txn.toPretty());
    await txn.prove()
    await txn.sign([key, deployerKey]).send()

    const state = local.getNetworkState()
    const time = globalSlotToTimestamp(state.globalSlotSinceGenesis)
    console.log("time chain", time.toBigInt())
    const timeStart = time.add(100)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmReward.initUpdate(timeStart)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    local.setGlobalSlot(1200)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmReward.updateVerificationKey(compileKey)
    })
    await txn.prove()
    // need to wait 1 day to update
    await expect(txn.sign([deployerKey]).send()).rejects.toThrow()

    local.setGlobalSlot(2000)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmReward.updateVerificationKey(compileKey)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    const farmRewardv2 = new FarmUpgradeTest(farmReward.address)
    const versionReward = await farmRewardv2.version()
    expect(versionReward?.toBigInt()).toEqual(112n)
  })

  it("update verification key 2", async () => {
    // method who test mina as reward, reward can be mina or fungible token
    let txn = await Mina.transaction(bobAccount, async () => {
      AccountUpdate.fundNewAccount(bobAccount, 1)
      await zkFarmToken.deposit(UInt64.from(1000))
    })
    await txn.prove()
    await txn.sign([bobKey]).send()

    local.setGlobalSlot(1)
    txn = await Mina.transaction(bobAccount, async () => {
      await zkFarmTokenHolder.withdraw(UInt64.from(1000))
      await zkPoolMina.approveAccountUpdate(zkFarmTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([bobKey]).send()
    local.setGlobalSlot(2)

    const timeUnlock = Date.now() + 1000_000
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmToken.initUpdate(UInt64.from(timeUnlock))
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    local.setGlobalSlot(200)
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmToken.updateVerificationKey(compileKey)
    })
    await txn.prove()
    // need to wait 1 day to update
    await expect(txn.sign([deployerKey]).send()).rejects.toThrow()

    local.setGlobalSlot(1000)
    txn = await Mina.transaction(deployerAccount, async () => {
      await zkFarmToken.updateVerificationKey(compileKey)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    const farmv2 = new FarmUpgradeTest(zkFarmTokenAddress)
    const version = await farmv2.version()
    expect(version?.toBigInt()).toEqual(112n)

    const dataReward = await generateRewardMerkle(zkFarmTokenAddress, zkPoolMina.deriveTokenId(), claimerNumber)
    console.log("rewardList", dataReward.rewardList)

    const key = PrivateKey.random()
    const farmReward = new FarmReward(key.toPublicKey())
    const farmRewardTokenHolder = new FarmRewardTokenHolder(key.toPublicKey(), zkToken2.deriveTokenId())
    // add more token for test
    const amountReward = dataReward.totalReward * 10n

    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 2)
      await farmReward.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await farmRewardTokenHolder.deploy({
        owner: deployerAccount,
        merkleRoot: dataReward.merkle.getRoot(),
        token: zkTokenAddress2
      })
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
      // we deploy and fund reward in one transaction
      await zkToken2.transfer(deployerAccount, key.toPublicKey(), UInt64.from(amountReward))
    })
    // console.log("farmReward deploy", txn.toPretty());
    await txn.prove()
    await txn.sign([key, deployerKey]).send()

    const state = local.getNetworkState()
    const time = globalSlotToTimestamp(state.globalSlotSinceGenesis)
    console.log("time chain", time.toBigInt())
    const timeStart = time.add(100)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmRewardTokenHolder.initUpdate(timeStart)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    local.setGlobalSlot(1200)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmRewardTokenHolder.updateVerificationKey(compileKey)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    await txn.prove()
    // need to wait 1 day to update
    await expect(txn.sign([deployerKey]).send()).rejects.toThrow()

    local.setGlobalSlot(2000)
    txn = await Mina.transaction(deployerAccount, async () => {
      await farmRewardTokenHolder.updateVerificationKey(compileKey)
      await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
    })
    await txn.prove()
    await txn.sign([deployerKey]).send()

    const farmRewardv2 = new FarmUpgradeTest(key.toPublicKey(), zkToken2.deriveTokenId())
    const versionReward = await farmRewardv2.version()
    expect(versionReward?.toBigInt()).toEqual(112n)
  })

  async function mintToken(user: PublicKey) {
    // token are minted to original deployer, so just transfer it for test
    let txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 1)
      await zkToken0.mint(user, UInt64.from(1000 * 10 ** 9))
    })
    await txn.prove()
    await txn.sign([deployerKey, zkTokenPrivateKey0]).send()

    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 1)
      await zkToken1.mint(user, UInt64.from(1000 * 10 ** 9))
    })
    await txn.prove()
    await txn.sign([deployerKey, zkTokenPrivateKey1]).send()

    txn = await Mina.transaction(deployerAccount, async () => {
      AccountUpdate.fundNewAccount(deployerAccount, 1)
      await zkToken2.mint(user, UInt64.from(1000 * 10 ** 9))
    })
    await txn.prove()
    await txn.sign([deployerKey, zkTokenPrivateKey2]).send()
  }

  function globalSlotToTimestamp(slot: UInt32) {
    const { genesisTimestamp, slotTime } = Mina.getNetworkConstants()
    return UInt64.from(slot).mul(slotTime).add(genesisTimestamp)
  }

  function getSlotFromTimestamp(date: number) {
    const { genesisTimestamp, slotTime } = Mina.activeInstance.getNetworkConstants()
    let slotCalculated = UInt64.from(date)
    slotCalculated = (slotCalculated.sub(genesisTimestamp)).div(slotTime)
    Provable.log("slotCalculated64", slotCalculated)
    return slotCalculated.toUInt32()
  }
})
