import { default as hre, ethers, upgrades } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { BigNumber, Contract, Signer } from "ethers";
import { expect } from "chai";
import {
  GoodMarketMaker,
  CERC20,
  GoodReserveCDai,
  SimpleStaking,
  GReputation
} from "../../types";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
import {
  createDAO,
  increaseTime,
  advanceBlocks,
  deployUniswap
} from "../helpers";
import ContributionCalculation from "@gooddollar/goodcontracts/stakingModel/build/contracts/ContributionCalculation.json";
import { getStakingFactory } from "../helpers";

const BN = ethers.BigNumber;
export const NULL_ADDRESS = ethers.constants.AddressZero;
export const BLOCK_INTERVAL = 30;

describe("StakersDistribution - staking with GD  and get Rewards in GDAO", () => {
  let dai: Contract;
  let cDAI: Contract;
  let goodReserve: GoodReserveCDai;
  let stakersDistribution: Contract;
  let goodFundManager: Contract;
  let simpleStaking: Contract;
  let simpleUsdcStaking: Contract;
  let usdcUsdOracle: Contract;
  let usdc: Contract;
  let cUsdc: Contract;
  let comp: Contract;
  let grep: GReputation;
  let avatar,
    goodDollar,
    identity,
    marketMaker: GoodMarketMaker,
    contribution,
    controller,
    founder,
    staker,
    sender,
    receiver,
    schemeMock,
    signers,
    nameService,
    initializeToken,
    genericCall,
    setDAOAddress,
    daiEthOracle,
    ethUsdOracle,
    gasFeeOracle,
    daiUsdOracle,
    compUsdOracle: Contract,
    deployDaiStaking;

  before(async () => {
    [founder, staker, sender, receiver, ...signers] = await ethers.getSigners();
    schemeMock = signers.pop();
    const cdaiFactory = await ethers.getContractFactory("cDAIMock");
    const cUsdcFactory = await ethers.getContractFactory("cUSDCMock");
    const usdcFactory = await ethers.getContractFactory("USDCMock");
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );
    const stakersDistributiongFactory = await ethers.getContractFactory(
      "StakersDistribution"
    );

    let {
      controller: ctrl,
      avatar: av,
      gd,
      identity,
      daoCreator,
      nameService: ns,
      setDAOAddress: sda,
      setSchemes,
      marketMaker: mm,
      daiAddress,
      cdaiAddress,
      reserve,
      reputation,
      setReserveToken,
      genericCall: gc,
      COMP
    } = await loadFixture(createDAO);

    genericCall = gc;
    dai = await ethers.getContractAt("DAIMock", daiAddress);
    cDAI = await ethers.getContractAt("cDAIMock", cdaiAddress);
    avatar = av;
    controller = ctrl;
    setDAOAddress = sda;
    nameService = ns;
    initializeToken = setReserveToken;
    goodReserve = reserve as GoodReserveCDai;
    console.log("deployed dao", {
      founder: founder.address,
      gd,
      identity,
      controller,
      avatar
    });
    goodFundManager = await upgrades.deployProxy(
      goodFundManagerFactory,
      [nameService.address],
      { kind: "uups" }
    );
    grep = (await ethers.getContractAt(
      "GReputation",
      reputation
    )) as GReputation;
    console.log("Deployed goodfund manager", {
      manager: goodFundManager.address
    });
    goodDollar = await ethers.getContractAt("IGoodDollar", gd);
    contribution = await ethers.getContractAt(
      ContributionCalculation.abi,
      await nameService.getAddress("CONTRIBUTION_CALCULATION")
    );

    marketMaker = mm;

    console.log("deployed contribution, deploying reserve...", {
      founder: founder.address
    });

    console.log("setting permissions...");
    await setDAOAddress("MARKET_MAKER", marketMaker.address);
    await setDAOAddress("FUND_MANAGER", goodFundManager.address);
    const tokenUsdOracleFactory = await ethers.getContractFactory(
      "BatUSDMockOracle"
    );
    compUsdOracle = await (
      await ethers.getContractFactory("CompUSDMockOracle")
    ).deploy();

    daiUsdOracle = await tokenUsdOracleFactory.deploy();
    usdc = await usdcFactory.deploy();
    cUsdc = await cUsdcFactory.deploy(usdc.address);
    usdcUsdOracle = await tokenUsdOracleFactory.deploy();
    comp = COMP;
    await setDAOAddress("COMP", comp.address);
    const uniswap = await deployUniswap(comp, dai);
    const router = uniswap.router;
    await setDAOAddress("UNISWAP_ROUTER", router.address);

    let simpleStakingFactory = await getStakingFactory("GoodCompoundStakingV2");

    simpleUsdcStaking = await simpleStakingFactory
      .deploy()
      .then(async contract => {
        await contract.init(
          usdc.address,
          cUsdc.address,
          nameService.address,
          "Good USDC",
          "gUSDC",
          "172800",
          usdcUsdOracle.address,
          compUsdOracle.address,
          [usdc.address, daiAddress]
        );
        return contract;
      });

    deployDaiStaking = async () => {
      return simpleStakingFactory.deploy().then(async contract => {
        await contract.init(
          dai.address,
          cDAI.address,
          nameService.address,
          "Good DAI",
          "gDAI",
          "200",
          daiUsdOracle.address,
          compUsdOracle.address,
          []
        );
        return contract;
      });
    };

    simpleStaking = await deployDaiStaking();

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking.address,
        currentBlockNumber,
        currentBlockNumber + 1000,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    stakersDistribution = await upgrades.deployProxy(
      stakersDistributiongFactory,
      [nameService.address],
      { kind: "uups" }
    );

    await setDAOAddress("GDAO_STAKERS", stakersDistribution.address);
  });

  it("it should have 2M monthly Reputation distribution", async () => {
    const monthlyReputationDistribution =
      await stakersDistribution.monthlyReputationDistribution();
    expect(monthlyReputationDistribution).to.be.equal(
      ethers.utils.parseEther("2000000")
    );
  });

  it("it should have 0 monthly rewards since staking amount was zero while initializing stakersDistribution", async () => {
    const rewardsPerBlock = await stakersDistribution.rewardsPerBlock(
      simpleStaking.address
    );
    expect(rewardsPerBlock).to.be.equal(0);
  });

  it("It should update monthly rewards according to staking amount of staking contract after one month passed from initialized", async () => {
    const stakingAmount = ethers.utils.parseEther("1000");
    const rewardsPerBlockBeforeStake =
      await stakersDistribution.rewardsPerBlock(simpleStaking.address);

    await dai["mint(address,uint256)"](staker.address, stakingAmount.mul(2));
    await dai
      .connect(staker)
      .approve(simpleStaking.address, stakingAmount.mul(2));
    await simpleStaking.connect(staker).stake(stakingAmount, 0, false);
    await increaseTime(86700 * 30);
    await simpleStaking.connect(staker).stake(stakingAmount, 0, false);
    const rewardsPerBlockAfterStake = await stakersDistribution.rewardsPerBlock(
      simpleStaking.address
    );
    await simpleStaking
      .connect(staker)
      .withdrawStake(stakingAmount.mul(2), false);
    const rewardsPerBlockAfterWithdraw =
      await stakersDistribution.rewardsPerBlock(simpleStaking.address);
    const chainBlockPerMonth =
      await stakersDistribution.getChainBlocksPerMonth();
    expect(rewardsPerBlockBeforeStake).to.be.equal(BN.from("0"));
    expect(rewardsPerBlockAfterStake).to.be.equal(
      ethers.utils.parseEther("2000000").div(chainBlockPerMonth)
    );
    expect(rewardsPerBlockAfterStake).to.be.equal(rewardsPerBlockAfterWithdraw);
  });

  it("it should not be set monthly reputation when not Avatar", async () => {
    const transaction = await stakersDistribution
      .setMonthlyReputationDistribution("1000000")
      .catch(e => e);
    expect(transaction.message).to.have.string(
      "only avatar can call this method"
    );
  });

  it("it should set monthly reputation when Avatar", async () => {
    let encoded = stakersDistribution.interface.encodeFunctionData(
      "setMonthlyReputationDistribution",
      [ethers.utils.parseEther("1000000")]
    );
    await genericCall(stakersDistribution.address, encoded);
    const monthlyReputationDistribution =
      await stakersDistribution.monthlyReputationDistribution();
    expect(monthlyReputationDistribution).to.be.equal(
      ethers.utils.parseEther("1000000")
    );
    encoded = stakersDistribution.interface.encodeFunctionData(
      "setMonthlyReputationDistribution",
      [ethers.utils.parseEther("2000000")]
    );
    await genericCall(stakersDistribution.address, encoded);
  });

  it("it should distribute monthly rewards according to staking amount of contracts so in this particular case simpleStaking contract should get %75 of the monthly rewards ", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber,
        currentBlockNumber + 1000,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("1000");
    const rewardsPerBlockBeforeStakeContractOne =
      await stakersDistribution.rewardsPerBlock(simpleStaking.address);
    const rewardsPerBlockBeforeStakeContractTwo =
      await stakersDistribution.rewardsPerBlock(simpleStaking1.address);

    await dai["mint(address,uint256)"](staker.address, stakingAmount.mul(100));
    await dai
      .connect(staker)
      .approve(simpleStaking.address, stakingAmount.mul(75));
    await simpleStaking.connect(staker).stake(stakingAmount.mul(50), 0, false);
    await dai
      .connect(staker)
      .approve(simpleStaking1.address, stakingAmount.mul(25));
    await simpleStaking1.connect(staker).stake(stakingAmount.mul(25), 0, false);
    await increaseTime(86700 * 30); // Increase one month
    await simpleStaking.connect(staker).stake(stakingAmount.mul(25), 0, false);

    const rewardsPerBlockAfterStakeContractOne =
      await stakersDistribution.rewardsPerBlock(simpleStaking.address);
    const rewardsPerBlockAftereStakeContractTwo =
      await stakersDistribution.rewardsPerBlock(simpleStaking1.address);
    await simpleStaking
      .connect(staker)
      .withdrawStake(stakingAmount.mul(75), false);
    expect(rewardsPerBlockAfterStakeContractOne).to.be.equal(
      rewardsPerBlockAftereStakeContractTwo.mul(3).add(1)
    ); // added 1 cause of precision loss
    encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber,
        currentBlockNumber + 1000,
        true
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
  });

  it("It should not update monthly rewards if staking contract's blockEnd Passed", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("1000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount.mul(2));
    await dai
      .connect(staker)
      .approve(simpleStaking.address, stakingAmount.mul(2));
    const rewardsPerBlockBeforeStake =
      await stakersDistribution.rewardsPerBlock(simpleStaking.address);
    await simpleStaking.connect(staker).stake(stakingAmount, 0, false);
    await advanceBlocks(40);
    await increaseTime(86700 * 30); // Increase one month
    const stakerGDAOBalanceBeforeStake = await grep.balanceOf(staker.address);
    await simpleStaking.connect(staker).stake(stakingAmount, 0, false);
    const stakerGDAOBalanceAfterStake = await grep.balanceOf(staker.address);
    await simpleStaking.connect(staker).withdrawRewards();

    const rewardsPerBlockAfterStake = await stakersDistribution.rewardsPerBlock(
      simpleStaking.address
    );
    const GDAOBalanceBeforeWithdraw = await grep.balanceOf(staker.address);
    await advanceBlocks(10);
    await simpleStaking.connect(staker).withdrawStake(stakingAmount, false);
    const GDAOBalanceAfterWithdraw = await grep.balanceOf(staker.address);
    expect(rewardsPerBlockAfterStake).to.not.be.equal(
      rewardsPerBlockBeforeStake
    ); // Should update rewards per block every stake/withdraw
    expect(GDAOBalanceBeforeWithdraw).to.be.equal(GDAOBalanceAfterWithdraw); // Should not earn any GDAO since simplestaking blockend passed
    expect(stakerGDAOBalanceAfterStake.gt(stakerGDAOBalanceBeforeStake)).to.be
      .true;
  });

  it("it should give distribute if blockend passed but some of the rewards during reward period was not distributed", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("1000");
    await simpleStaking.connect(staker).withdrawStake(stakingAmount, false);

    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking.address, stakingAmount);
    const rewardsPerBlock = await stakersDistribution.rewardsPerBlock(
      simpleStaking.address
    );
    const blockNumberOfStake = (await ethers.provider.getBlockNumber()) + 1;
    await simpleStaking.connect(staker).stake(stakingAmount, 0, false);
    await advanceBlocks(30);
    const GDAOBalanceBeforeWithdraw = await grep.balanceOf(staker.address);
    await simpleStaking.connect(staker).withdrawStake(stakingAmount, false);

    const GDAOBalanceAfterWithdraw = await grep.balanceOf(staker.address);
    expect(GDAOBalanceAfterWithdraw).to.be.gt(GDAOBalanceBeforeWithdraw);

    expect(GDAOBalanceAfterWithdraw).to.be.equal(
      GDAOBalanceBeforeWithdraw.add(
        rewardsPerBlock.mul(currentBlockNumber + 20 - blockNumberOfStake)
      )
    );
  });

  it("it should not increaseProductivity of staking contract which is blacklisted", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        true
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("1000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    const productivityOfStaker = await stakersDistribution.getProductivity(
      simpleStaking1.address,
      staker.address
    );
    expect(productivityOfStaker[0]).to.be.equal(0);
  });

  it("it should not decreaseProductivity of staking contract which is blacklisted", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "100000",
        simpleStaking1.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    await increaseTime(86700 * 30); // Increase one month
    const stakingAmount = ethers.utils.parseEther("1000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    await advanceBlocks(5); //should accumulate some gdao rewards

    let pendingGDAO = await stakersDistribution.getUserPendingRewards(
      [simpleStaking1.address],
      staker.address
    );

    expect(pendingGDAO).gt(0);

    encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        true
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    await simpleStaking1.connect(staker).withdrawStake(stakingAmount, false);
    const productivityOfStaker = await stakersDistribution.getProductivity(
      simpleStaking1.address,
      staker.address
    );
    expect(productivityOfStaker[0]).to.be.equal(stakingAmount);

    const repBefore = await grep["balanceOf(address)"](staker.address);
    await stakersDistribution.claimReputation(staker.address, [
      simpleStaking1.address
    ]);
    const repAfter = await grep["balanceOf(address)"](staker.address);

    expect(repBefore).to.equal(repAfter); //should not have been awarded rep accumulated before blacklisting contract

    pendingGDAO = await stakersDistribution.getUserPendingRewards(
      [simpleStaking1.address],
      staker.address
    );
    expect(pendingGDAO).equal(0); //should have 0 pending after contract was blacklisted
  });

  it("it should not earn rewards when current block < startBlock", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();

    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber + 500,
        currentBlockNumber + 1000,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("1000");
    const userProductivityBeforeStaking =
      await stakersDistribution.getProductivity(
        simpleStaking1.address,
        staker.address
      );
    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    const userProductivityAfterStaking =
      await stakersDistribution.getProductivity(
        simpleStaking1.address,
        staker.address
      );
    await advanceBlocks(10);
    const userPendingRewards = await stakersDistribution.getUserPendingReward(
      simpleStaking1.address,
      currentBlockNumber + 500,
      currentBlockNumber + 1000,
      staker.address
    );

    expect(userProductivityAfterStaking[0]).to.be.equal(stakingAmount);
    expect(userProductivityAfterStaking[0]).to.be.gt(
      userProductivityBeforeStaking[0]
    );
    expect(userPendingRewards).to.be.equal(0);
    encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber + 500,
        currentBlockNumber + 1000,
        true
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
  });

  it("Accumulated per share has enough precision when reward << totalproductivity", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();
    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber - 10,
        currentBlockNumber + 100,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmount = ethers.utils.parseEther("10000000000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount); // 10 billion dai to stake
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    const stakeBlockNumber = (await ethers.provider.getBlockNumber()) + 1;
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    const rewardsPerBlock = await stakersDistribution.rewardsPerBlock(
      simpleStaking1.address
    );
    await advanceBlocks(4);
    const gdaoBalanceBeforeWithdraw = await grep.balanceOf(staker.address);
    await simpleStaking1.connect(staker).withdrawStake(stakingAmount, false);
    const withdrawBlockNumber = await ethers.provider.getBlockNumber();
    await stakersDistribution.claimReputation(staker.address, [
      simpleUsdcStaking.address,
      simpleStaking.address
    ]);

    const gdaoBalanceAfterWithdraw = await grep.balanceOf(staker.address);
    const calculatedNewBalance = gdaoBalanceBeforeWithdraw.add(
      rewardsPerBlock.mul(withdrawBlockNumber - stakeBlockNumber)
    );
    expect(gdaoBalanceAfterWithdraw).to.be.gt(gdaoBalanceBeforeWithdraw);

    expect(gdaoBalanceAfterWithdraw).to.be.equal(calculatedNewBalance);

    encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking1.address,
        currentBlockNumber + 500,
        currentBlockNumber + 1000,
        true
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
  });

  it("it should distribute rewards properly when staking contract's token is different decimals than 18", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );
    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleUsdcStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmountDai = ethers.utils.parseEther("10000");
    const stakingAmountUsdc = ethers.utils.parseUnits("10000", 6);
    await dai["mint(address,uint256)"](staker.address, stakingAmountDai);
    await dai.connect(staker).approve(simpleStaking.address, stakingAmountDai);
    await usdc["mint(address,uint256)"](staker.address, stakingAmountUsdc);
    await usdc
      .connect(staker)
      .approve(simpleUsdcStaking.address, stakingAmountUsdc);
    await simpleStaking.connect(staker).stake(stakingAmountDai, 0, false);
    await increaseTime(86700 * 30); // Increase one month
    await simpleUsdcStaking.connect(staker).stake(stakingAmountUsdc, 0, false);
    const usdcStakingProductivity = await stakersDistribution.getProductivity(
      simpleUsdcStaking.address,
      staker.address
    );
    const daiStakingProductivity = await stakersDistribution.getProductivity(
      simpleStaking.address,
      staker.address
    );
    await advanceBlocks(10);
    const UserPendingGdaos = await stakersDistribution.getUserPendingRewards(
      [simpleStaking.address, simpleUsdcStaking.address],
      staker.address
    );
    expect(UserPendingGdaos).to.be.gt(0);
    const usdcStakingPendingGdaos =
      await stakersDistribution.getUserPendingReward(
        simpleUsdcStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        staker.address
      );
    expect(usdcStakingPendingGdaos).to.be.gt(0);
    const daiStakingPendingGdaos =
      await stakersDistribution.getUserPendingReward(
        simpleStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        staker.address
      );
    expect(daiStakingPendingGdaos).to.be.gt(0);
    const usdcStakingRewardsPerBlock =
      await stakersDistribution.rewardsPerBlock(simpleUsdcStaking.address);
    const daiStakingRewardsPerBlock = await stakersDistribution.rewardsPerBlock(
      simpleStaking.address
    );

    expect(usdcStakingRewardsPerBlock).to.equal(daiStakingRewardsPerBlock);

    await simpleUsdcStaking
      .connect(staker)
      .withdrawStake(stakingAmountUsdc, false);
    await simpleStaking.connect(staker).withdrawStake(stakingAmountDai, false);

    await stakersDistribution.claimReputation(staker.address, [
      simpleUsdcStaking.address,
      simpleStaking.address
    ]);

    expect(UserPendingGdaos).to.be.equal(
      usdcStakingPendingGdaos.add(daiStakingPendingGdaos)
    );
    expect(usdcStakingProductivity[0]).to.be.equal(stakingAmountUsdc.mul(1e12)); // mul with 1e12 since usdc in 6 decimals but we keep productivity in 18 decimals
    expect(daiStakingProductivity[0]).to.be.equal(stakingAmountDai);
    expect(usdcStakingProductivity[0]).to.be.equal(daiStakingProductivity[0]);
    expect(usdcStakingProductivity[1]).to.be.equal(daiStakingProductivity[1]);
    expect(usdcStakingRewardsPerBlock).to.be.equal(daiStakingRewardsPerBlock);
  });

  it("should be able to transfer staking token when using stakersdistribution", async () => {
    const stakingAmountDai = ethers.utils.parseEther("10000");
    const stakingAmountUsdc = ethers.utils.parseUnits("10000", 6);
    await dai["mint(address,uint256)"](staker.address, stakingAmountDai);
    await dai.connect(staker).approve(simpleStaking.address, stakingAmountDai);
    await usdc["mint(address,uint256)"](staker.address, stakingAmountUsdc);
    await usdc
      .connect(staker)
      .approve(simpleUsdcStaking.address, stakingAmountUsdc);
    await simpleStaking.connect(staker).stake(stakingAmountDai, 0, false);
    await increaseTime(86700 * 30); // Increase one month
    await simpleUsdcStaking.connect(staker).stake(stakingAmountUsdc, 0, false);
    await advanceBlocks(10);

    await simpleUsdcStaking
      .connect(staker)
      .transfer(signers[1].address, stakingAmountUsdc);

    expect(await simpleUsdcStaking.balanceOf(signers[1].address)).to.eq(
      stakingAmountUsdc
    );
    expect(await simpleUsdcStaking.balanceOf(staker.address)).to.eq(0);
    // await expect(
    //   simpleStaking
    //     .connect(staker)
    //     .transfer(signers[1].address, ethers.utils.parseEther("10000"))
    // ).to.not.reverted;
  });

  it("it should distribute rewards properly when transeferring a staking contract's token that's different decimals than 18 decimals", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );
    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "1000",
        simpleUsdcStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);
    const stakingAmountUsdc = ethers.utils.parseUnits("10000", 6);
    await usdc["mint(address,uint256)"](sender.address, stakingAmountUsdc);
    await usdc
      .connect(sender)
      .approve(simpleUsdcStaking.address, stakingAmountUsdc);
    await increaseTime(86700 * 30); // Increase one month
    await simpleUsdcStaking.connect(sender).stake(stakingAmountUsdc, 0, false);
    await simpleUsdcStaking
      .connect(sender)
      .transfer(receiver.address, stakingAmountUsdc);
    await advanceBlocks(10);
    const UserPendingGdaos = await stakersDistribution.getUserPendingRewards(
      [simpleUsdcStaking.address],
      receiver.address
    );
    expect(UserPendingGdaos).to.be.gt(0);
    expect(UserPendingGdaos.toString().length).to.be.gt(18);
    const usdcStakingPendingGdaos =
      await stakersDistribution.getUserPendingReward(
        simpleUsdcStaking.address,
        currentBlockNumber - 5,
        currentBlockNumber + 200,
        receiver.address
      );
    expect(usdcStakingPendingGdaos).to.be.gt(0);
    expect(usdcStakingPendingGdaos.toString().length).to.be.gt(18);
  });

  it("should get user minted and pending rewards", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();
    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "100000",
        simpleStaking1.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        false
      ] // set 10 gd per block
    );
    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);

    let [userMintedRewardBeforeStake, userPendingRewardBeforeStake] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardBeforeStake).to.eq("0");
    expect(userPendingRewardBeforeStake).to.eq("0");

    const stakingAmount = ethers.utils.parseEther("1000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    await advanceBlocks(5); //should accumulate some gdao rewards

    let [userMintedRewardAfterStake, userPendingRewardAfterStake] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardAfterStake).eq(0);
    expect(userPendingRewardAfterStake).gt(0);

    await stakersDistribution.claimReputation(staker.address, [
      simpleStaking1.address
    ]);

    let [userMintedRewardAfterClaim, userPendingRewardAfterClaim] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardAfterClaim).gt(0);
    expect(userPendingRewardAfterClaim).eq(0);
  });

  async function getUserMintedAndPendingRewards(
    stakerAddress,
    stakingContractAddress
  ) {
    const userMintedAndPending =
      await stakersDistribution.getUserMintedAndPending(
        [stakingContractAddress],
        stakerAddress
      );
    const userMintedReward = userMintedAndPending[0];
    const userPendingReward = userMintedAndPending[1];

    return [userMintedReward, userPendingReward];
  }

  it("should update user rewards when monthly rate has changed", async () => {
    const goodFundManagerFactory = await ethers.getContractFactory(
      "GoodFundManager"
    );

    const simpleStaking1 = await deployDaiStaking();
    const ictrl = await ethers.getContractAt(
      "Controller",
      controller,
      schemeMock
    );
    const currentBlockNumber = await ethers.provider.getBlockNumber();
    let encodedData = goodFundManagerFactory.interface.encodeFunctionData(
      "setStakingReward",
      [
        "100000",
        simpleStaking1.address,
        currentBlockNumber - 5,
        currentBlockNumber + 20,
        false
      ] // set 10 gd per block
    );

    await ictrl.genericCall(goodFundManager.address, encodedData, avatar, 0);

    let [userMintedRewardBeforeStake, userPendingRewardBeforeStake] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardBeforeStake).to.eq("0");
    expect(userPendingRewardBeforeStake).to.eq("0");

    const stakingAmount = ethers.utils.parseEther("1000");
    await dai["mint(address,uint256)"](staker.address, stakingAmount);
    await dai.connect(staker).approve(simpleStaking1.address, stakingAmount);
    await simpleStaking1.connect(staker).stake(stakingAmount, 0, false);
    await advanceBlocks(5); //should accumulate some gdao rewards

    const rewardsPerBlockBefore = await stakersDistribution.rewardsPerBlock(
      simpleStaking1.address
    );

    // reduce rewarsd speed by half from 2m to 1m
    let encoded = stakersDistribution.interface.encodeFunctionData(
      "setMonthlyReputationDistribution",
      [ethers.utils.parseEther("1000000")]
    );
    await genericCall(stakersDistribution.address, encoded);
    const rewardsPerBlockAfter = await stakersDistribution.rewardsPerBlock(
      simpleStaking1.address
    );
    let [userMintedRewardAfterStake, userPendingRewardAfterStake] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardAfterStake).eq(0);
    expect(userPendingRewardAfterStake).gt(0);

    await advanceBlocks(4); //should accumulate some gdao rewards
    let [userMintedRewardAfterUpdate, userPendingRewardAfterUpdate] =
      await getUserMintedAndPendingRewards(
        staker.address,
        simpleStaking1.address
      );
    expect(userMintedRewardAfterUpdate).eq(0);
    // rewards speed has been reduced by half,
    expect(rewardsPerBlockAfter).lt(rewardsPerBlockBefore.mul(55).div(100));
    // was forced 4 blocks to pass since reward update
    expect(userPendingRewardAfterUpdate).eq(
      userPendingRewardAfterStake.add(rewardsPerBlockAfter.mul(4))
    );
  });
});
