import hre, { ethers, upgrades } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { InvitesV1, IGoodDollar, IIdentity } from "../../types";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address";
import { createDAO } from "../helpers";

const BN = ethers.BigNumber;

describe("InvitesV1", () => {
  let invites: InvitesV1, founder: SignerWithAddress;
  let inviter1,
    inviter2,
    invitee1,
    invitee2,
    invitee3,
    invitee4,
    invitee5,
    invitee6,
    invitee7,
    invitee8;

  let avatar, gd: IGoodDollar, Controller, id: IIdentity;

  before(async () => {
    [
      founder,
      inviter1,
      inviter2,
      invitee1,
      invitee2,
      invitee3,
      invitee4,
      invitee5,
      invitee6,
      invitee7,
      invitee8
    ] = await ethers.getSigners();

    const InvitesV1 = await ethers.getContractFactory("InvitesV1");

    let {
      daoCreator,
      controller,
      avatar: av,
      gd: gooddollar,
      identity
    } = await loadFixture(createDAO);

    Controller = controller;
    avatar = av;

    invites = (await upgrades.deployProxy(
      InvitesV1,
      [avatar, identity, gooddollar, 500],
      {
        unsafeAllowCustomTypes: true,
        kind: "transparent"
      }
    )) as InvitesV1;

    gd = (await ethers.getContractAt(
      "IGoodDollar",
      gooddollar,
      founder
    )) as IGoodDollar;
    id = (await ethers.getContractAt(
      "IIdentity",
      identity,
      founder
    )) as IIdentity;

    await gd["mint(address,uint256)"](invites.address, BN.from(5000));
    // await gd.transfer(invites.address, BN.from(5000));
  });

  it("should have balance", async () => {
    const balance = await gd.balanceOf(invites.address);
    expect(balance).to.equal(5000);
  });

  it("should have version", async () => {
    expect(await invites.active()).to.be.true;
    const version = await invites.version();
    expect(version).to.be.equal("1.5.0");
  });

  it("should let anyone join", async () => {
    await invites
      .connect(inviter1)
      .join(ethers.utils.hexZeroPad("0xfa", 32), ethers.constants.HashZero);
    let inviter = await invites.users(inviter1.address);
    expect(inviter.inviteCode).to.equal(ethers.utils.hexZeroPad("0xfa", 32));
  });

  it("should allow to join only once", async () => {
    await expect(
      invites
        .connect(inviter1)
        .join(
          ethers.utils.hexZeroPad("0xfa", 32),
          ethers.utils.hexZeroPad("0x01", 32)
        )
    ).to.revertedWith(/user already joined/);
  });

  it("should not allow code reuse", async () => {
    // const invites = await Invites.deployed();
    await expect(
      invites
        .connect(inviter2)
        .join(ethers.utils.hexZeroPad("0xfa", 32), ethers.constants.HashZero)
    ).to.revertedWith(/invite code already in use/);
  });

  it("should mark inviter", async () => {
    await invites
      .connect(invitee1)
      .join(
        ethers.utils.hexZeroPad("0xaa", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );
    let invitee = await invites.users(invitee1.address);
    let inviterInvitees = await invites.getInvitees(inviter1.address);
    expect(invitee.invitedBy).to.be.equal(inviter1.address);
    expect(inviterInvitees).to.include(invitee1.address);
  });

  it("should not pay bounty for non whitelisted invitee", async () => {
    await expect(
      invites.connect(inviter1).bountyFor(invitee1.address)
    ).to.revertedWith(/user not elligble for bounty yet/);
  });

  it("should not pay bounty for non whitelisted inviter", async () => {
    await id.addWhitelistedWithDID(invitee1.address, Math.random() + "");
    expect(await id.isWhitelisted(invitee1.address)).to.be.true;
    expect(await invites.canCollectBountyFor(invitee1.address)).to.be.false;
    await expect(
      invites.connect(inviter1).bountyFor(invitee1.address)
    ).to.revertedWith(/user not elligble for bounty yet/);
  });

  it("should pay bounty for whitelisted invitee and inviter", async () => {
    const bounty = (await invites.levels(0)).bounty.toNumber();
    await id
      .addWhitelistedWithDID(inviter1.address, Math.random() + "")
      .catch(e => e);
    const startBalance = await gd
      .balanceOf(inviter1.address)
      .then(_ => _.toNumber());
    expect(await id.isWhitelisted(inviter1.address)).to.be.true;
    let pending = await invites.getPendingInvitees(inviter1.address);
    expect(pending.length, "pending").to.be.equal(1);
    const inviteeBalance = await gd
      .balanceOf(invitee1.address)
      .then(_ => _.toNumber());
    await invites.connect(inviter1).bountyFor(invitee1.address);

    let invitee = await invites.users(invitee1.address);
    let inviter = await invites.users(inviter1.address);
    const endBalance = await gd
      .balanceOf(inviter1.address)
      .then(_ => _.toNumber());

    pending = await invites.getPendingInvitees(inviter1.address);
    const txFee = await gd["getFees(uint256)"](bounty).then(_ =>
      _["0"].toNumber()
    ); //gd might have a tx fee
    const txFee2 = await gd["getFees(uint256)"](bounty / 2).then(_ =>
      _["0"].toNumber()
    ); //gd might have a tx fee

    expect(pending.length, "pending").to.be.equal(0);
    expect(invitee.bountyPaid).to.be.true;
    expect(inviter.totalApprovedInvites.toNumber()).to.be.equal(1);
    expect(inviter.totalEarned.toNumber()).to.be.equal(bounty);
    expect(
      endBalance - startBalance + txFee,
      "inviter rewards not matching bounty"
    ).to.be.equal(bounty);
    expect(
      (await gd.balanceOf(invitee1.address).then(_ => _.toNumber())) -
        inviteeBalance,
      "invitee rewrad should be bounty/2"
    ).to.be.equal(bounty / 2 - txFee2); //test that invitee  got half bonus
  });

  it("should update global stats", async () => {
    const bounty = (await invites.levels(0)).bounty.toNumber();
    const stats = await invites.stats();
    expect(stats.totalApprovedInvites.toNumber()).to.be.equal(
      1,
      "approved invites"
    );
    expect(stats.totalInvited.toNumber()).to.be.equal(1, "total  invited");
    expect(stats.totalBountiesPaid.toNumber()).to.be.equal(bounty);
  });

  it("should not pay bounty twice", async () => {
    await expect(
      invites.connect(inviter2).bountyFor(invitee1.address)
    ).to.revertedWith(/user not elligble for bounty yet/);
  });

  it("should not fail in collectBounties for invalid invitees", async () => {
    await invites
      .connect(invitee7)
      .join(
        ethers.utils.hexZeroPad("0x01", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );
    await invites
      .connect(invitee8)
      .join(
        ethers.utils.hexZeroPad("0x02", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );

    let pending = await invites.getPendingInvitees(inviter1.address);
    expect(pending.length, "pending").to.be.equal(2);
    await expect(invites.connect(inviter1).collectBounties()).to.not.reverted;
    let user1 = await invites.users(invitee7.address);
    let user2 = await invites.users(invitee8.address);
    pending = await invites.getPendingInvitees(inviter1.address);
    expect(
      await invites.getPendingBounties(inviter1.address).then(_ => _.toNumber())
    ).to.be.equal(0);
    expect(user1.bountyPaid).to.be.false;
    expect(user2.bountyPaid).to.be.false;
    expect(pending.length, "pending").to.be.equal(2);
  });

  it("should collectBounties for inviter", async () => {
    await id
      .addWhitelistedWithDID(invitee7.address, Math.random() + "")
      .catch(e => e);
    await id
      .addWhitelistedWithDID(invitee8.address, Math.random() + "")
      .catch(e => e);
    expect(
      await invites.getPendingBounties(inviter1.address).then(_ => _.toNumber())
    ).to.be.equal(2);
    const res = await invites
      .connect(inviter1)
      .collectBounties()
      .catch(e => e);

    let user1 = await invites.users(invitee7.address);
    let user2 = await invites.users(invitee8.address);
    let pending = await invites.getPendingInvitees(inviter1.address);
    expect(
      await invites.getPendingBounties(inviter1.address).then(_ => _.toNumber())
    ).to.be.equal(0);
    expect(pending.length, "pending").to.be.equal(0);
    expect(user1.bountyPaid, "user1").to.be.true;
    expect(user2.bountyPaid, "user2").to.be.true;
  });

  it("should not set level not by owner", async () => {
    await expect(
      invites.connect(inviter1).setLevel(0, 1, 5, 1)
    ).to.revertedWith(/Only owner or avatar can perform this action/);
  });

  it("should set level by owner", async () => {
    await invites.setLevel(0, 1, 5, 1);
    let lvl = await invites.levels(0);
    expect(lvl.toNext.toNumber()).to.be.equal(1);
    expect(lvl.daysToComplete.toNumber()).to.be.equal(1);
    await invites.setLevel(1, 0, 10, 2);
    lvl = await invites.levels(1);
    expect(lvl.toNext.toNumber()).to.be.equal(0);
    expect(lvl.daysToComplete.toNumber()).to.be.equal(2);
    expect(lvl.bounty.toNumber()).to.be.equal(10);
  });

  it("should update inviter level", async () => {
    await invites
      .connect(inviter1)
      .join(ethers.utils.hexZeroPad("0xfa", 32), ethers.constants.HashZero)
      .then(_ => _.wait())
      .catch(e => e);

    await id
      .addWhitelistedWithDID(inviter1.address, Math.random() + "")
      .catch(e => e);
    await invites.setLevel(0, 1, 5, 1); //1 inviter to level up
    await invites.setLevel(1, 0, 10, 2); // 10 bounty for second level

    await invites
      .connect(invitee4)
      .join(
        ethers.utils.hexZeroPad("0x03", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );
    await invites
      .connect(invitee5)
      .join(
        ethers.utils.hexZeroPad("0x04", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );
    await id
      .addWhitelistedWithDID(invitee4.address, Math.random() + "")
      .catch(e => e);
    await id
      .addWhitelistedWithDID(invitee5.address, Math.random() + "")
      .catch(e => e);
    const res1 = await (await invites.bountyFor(invitee4.address)).wait();

    const log1 = res1.events.find(_ => _.event === "InviterBounty");
    expect(log1.event).to.be.equal("InviterBounty");
    expect(log1.args.inviterLevel.toNumber()).to.be.equal(1);
    expect(log1.args.earnedLevel).to.be.equal(true);
    expect(log1.args.bountyPaid.toNumber()).to.be.equal(5);

    let inviter = await invites.users(inviter1.address);
    expect(inviter.level.toNumber()).to.be.equal(1);
    const res2 = await (
      await invites.connect(inviter1).collectBounties()
    ).wait();
    const log2 = res2.events.find(_ => _.event === "InviterBounty");
    expect(log2.event).to.be.equal("InviterBounty");
    expect(log2.args.inviterLevel.toNumber()).to.be.equal(1);
    expect(log2.args.earnedLevel).to.be.equal(false);
    expect(log2.args.bountyPaid.toNumber()).to.be.equal(10);
  });

  it("should allow to set inviter later and pay bounty", async () => {
    await invites
      .connect(invitee6)
      .join(ethers.utils.hexZeroPad("0xfd", 32), ethers.constants.HashZero);
    await invites
      .connect(invitee6)
      .join(
        ethers.utils.hexZeroPad("0xfd", 32),
        ethers.utils.hexZeroPad("0xfa", 32)
      );
    const invitee = await invites.users(invitee6.address);
    expect(invitee.invitedBy).to.equal(inviter1.address);
    await id
      .addWhitelistedWithDID(invitee6.address, Math.random() + "")
      .catch(e => e);
    await expect(invites.bountyFor(invitee6.address)).to.emit(
      invites,
      "InviterBounty"
    );
  });

  it("should end contract by owner", async () => {
    expect(
      await gd.balanceOf(invites.address).then(_ => _.toNumber())
    ).to.be.gt(0);
    await invites.end();
    expect(
      await gd.balanceOf(invites.address).then(_ => _.toNumber())
    ).to.be.eq(0);
  });
});
