import hre from "hardhat";
import { expect } from "chai";
// import { parseEther } from "ethers";
import { DiceGame, RiggedRoll, RiggedRoll__factory } from "../typechain-types";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";

const { ethers } = hre;

describe("🚩 Challenge: 🎲 Dice Game", function () {
  let diceGame: DiceGame;
  let riggedRoll: RiggedRoll;
  let deployer: HardhatEthersSigner;
  const { provider } = ethers;

  const rollAmountString = "0.002";
  const rollAmount = ethers.parseEther(rollAmountString);

  async function deployContracts() {
    [deployer] = await ethers.getSigners();
    const DiceGame = await ethers.getContractFactory("DiceGame");
    diceGame = await DiceGame.deploy();

    const contractAddress = process.env.CONTRACT_ADDRESS;
    let contractArtifact;
    if (contractAddress) {
      contractArtifact = `contracts/download-${contractAddress}.sol:RiggedRoll`;
    } else {
      contractArtifact = "contracts/RiggedRoll.sol:RiggedRoll";
    }

    const diceGameAddress = await diceGame.getAddress();
    const RiggedRoll = (await ethers.getContractFactory(contractArtifact)) as RiggedRoll__factory;
    riggedRoll = await RiggedRoll.deploy(diceGameAddress);
  }

  async function fundRiggedContract() {
    const riggedRollAddress = await riggedRoll.getAddress();
    return deployer.sendTransaction({
      to: riggedRollAddress,
      value: rollAmount,
    });
  }

  async function getRoll(getRollLessThanFive: boolean) {
    let expectedRoll;
    while (true) {
      const latestBlockNumber = await provider.getBlockNumber();
      const block = await provider.getBlock(latestBlockNumber);
      if (!block) {
        return;
      }
      const prevHash = block.hash;
      const nonce = await diceGame.nonce();

      const diceGameAddress = await diceGame.getAddress();
      const hash = ethers.solidityPackedKeccak256(
        ["bytes32", "address", "uint256"],
        [prevHash, diceGameAddress, nonce],
      );

      const bigInt = BigInt(hash);
      expectedRoll = bigInt % 16n;
      if (expectedRoll < 5n == getRollLessThanFive) {
        break;
      }

      const options = { value: rollAmount };
      await diceGame.rollTheDice(options);
    }
    return expectedRoll;
  }

  describe("Checkpoint2: 🔑 Rigged Contract", function () {
    beforeEach(async function () {
      await deployContracts();
    });

    it("Checkpoint2: Should deploy contracts", async function () {
      const diceGameAddress = await diceGame.getAddress();
      expect(await riggedRoll.diceGame()).to.equal(diceGameAddress);
    });

    it(`Checkpoint2: Should revert if balance is less than ${rollAmountString} ethers`, async function () {
      await expect(riggedRoll.riggedRoll())
        .to.be.revertedWithCustomError(riggedRoll, "NotEnoughETH")
        .withArgs(rollAmount, 0);
    });

    it("Checkpoint2: Should transfer sufficient eth to RiggedRoll", async function () {
      console.log("\t", "💸 Funding RiggedRoll contract");
      await fundRiggedContract();
      const riggedRollAddress = await riggedRoll.getAddress();
      const balance = await provider.getBalance(riggedRollAddress);
      console.log("\t", "💲 RiggedRoll balance: ", ethers.formatEther(balance));
      expect(balance).to.gte(rollAmount, `Error when expecting DiceGame contract to have >= ${rollAmount} eth`);
    });

    it("Checkpoint2: Should call diceGame.rollTheDice for a roll <= 5", async () => {
      console.log("\t", "💸 Funding RiggedRoll contract");
      await fundRiggedContract();

      const getRollLessThanFive = true;
      const expectedRoll = await getRoll(getRollLessThanFive);
      console.log("\t", "🎲 Expect roll to be less than or equal to 5. Dice Game Roll:", Number(expectedRoll));

      const tx = await riggedRoll.riggedRoll();
      const riggedRollAddress = await riggedRoll.getAddress();

      await expect(tx).to.emit(diceGame, "Roll").withArgs(riggedRollAddress, rollAmount, expectedRoll);
      await expect(tx).to.emit(diceGame, "Winner");
    });

    it("Checkpoint2: Should not call diceGame.rollTheDice for a roll > 5", async () => {
      console.log("\t", "💸 Funding RiggedRoll contract");
      await fundRiggedContract();

      const getRollLessThanFive = false;
      const expectedRoll = await getRoll(getRollLessThanFive);
      console.log("\t", "🎲 Expect roll to be greater than 5. Dice Game Roll:", Number(expectedRoll));
      console.log("\t", "◀  Expect riggedRoll to be reverted");

      await expect(riggedRoll.riggedRoll())
        .to.be.revertedWithCustomError(riggedRoll, "NotWinningRoll")
        .withArgs(expectedRoll);
    });
  });

  describe("Checkpoint3: 💵 Where's my money?!?", function () {
    beforeEach(async function () {
      await deployContracts();
    });

    it("Checkpoint3: Should withdraw funds", async () => {
      console.log("\t", "💸 Funding RiggedRoll contract");
      await fundRiggedContract();

      const deployerPrevBalance = await provider.getBalance(deployer.address);
      console.log("\t", "💲 Current RiggedRoll balance: ", ethers.formatEther(deployerPrevBalance));
      const riggedRollAddress = await riggedRoll.getAddress();
      const riggedRollBalance = await provider.getBalance(riggedRollAddress);
      await riggedRoll.withdraw(deployer.address, riggedRollBalance);

      const deployerCurrentBalance = await provider.getBalance(deployer.address);
      console.log("\t", "💲 New RiggedRoll balance: ", ethers.formatEther(deployerCurrentBalance));

      expect(
        deployerPrevBalance < deployerCurrentBalance,
        "Error when expecting RiggedRoll balance to increase when calling withdraw",
      ).to.true;
    });

    it("Checkpoint3: Should revert withdraw when amount exceeds contract balance", async () => {
      console.log("\t", "💸 Funding RiggedRoll contract");
      await fundRiggedContract();

      const riggedRollAddress = await riggedRoll.getAddress();
      const riggedRollBalance = await provider.getBalance(riggedRollAddress);
      const tooMuch = riggedRollBalance + rollAmount;

      await expect(riggedRoll.withdraw(deployer.address, tooMuch))
        .to.be.revertedWithCustomError(riggedRoll, "InsufficientBalance")
        .withArgs(tooMuch, riggedRollBalance);
    });
  });
});
