import { ethers } from "hardhat";
import { expect } from "chai";
import { ContractTransactionReceipt } from "ethers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { Balloons, DEX } from "../typechain-types";

describe("🚩 Challenge: ⚖️ 🪙 DEX", () => {
  // this.timeout(45000);

  let dexContract: DEX;
  let balloonsContract: Balloons;
  let deployer: HardhatEthersSigner;
  let user2: HardhatEthersSigner;
  let user3: HardhatEthersSigner;

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

  async function getEventValue(txReceipt: ContractTransactionReceipt | null, eventNumber: number) {
    if (!txReceipt) return;
    const dexContractAddress = await dexContract.getAddress();
    const log = txReceipt.logs.find(log => log.address === dexContractAddress);
    const logDescr = log && dexContract.interface.parseLog(log);
    const args = logDescr?.args;
    return args && args[eventNumber]; // index of ethAmount in event
  }

  async function deployNewInstance() {
    before("Deploying fresh contracts", async function () {
      console.log("\t", " 🛫 Deploying new contracts...");

      const BalloonsContract = await ethers.getContractFactory("Balloons");
      balloonsContract = await BalloonsContract.deploy();
      const balloonsAddress = await balloonsContract.getAddress();

      const DexContract = await ethers.getContractFactory(contractArtifact);
      dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
      const dexContractAddress = await dexContract.getAddress();

      await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));

      await dexContract.init(ethers.parseEther("5"), {
        value: ethers.parseEther("5"),
        gasLimit: 200000,
      });

      [deployer, user2, user3] = await ethers.getSigners();

      await balloonsContract.transfer(user2.address, ethers.parseEther("10"));
      await balloonsContract.transfer(user3.address, ethers.parseEther("10"));
    });
  }

  // quick fix to let gas reporter fetch data from gas station & coinmarketcap
  before(done => {
    setTimeout(done, 2000);
  });

  // --------------------- START OF CHECKPOINT 2 ---------------------
  describe("Checkpoint 2: Reserves", function () {
    describe("Deploying the contracts and testing the init function", () => {
      it("Should deploy contracts", async function () {
        const BalloonsContract = await ethers.getContractFactory("Balloons");
        balloonsContract = await BalloonsContract.deploy();
        const balloonsAddress = await balloonsContract.getAddress();

        const DexContract = await ethers.getContractFactory(contractArtifact);
        dexContract = (await DexContract.deploy(balloonsAddress)) as DEX;
        const dexContractAddress = await dexContract.getAddress();

        await balloonsContract.approve(dexContractAddress, ethers.parseEther("100"));

        const lpBefore = await dexContract.totalLiquidity();
        console.log(
          "\t",
          " 💦 Expecting total liquidity to be 0 before initializing.  Total liquidity:",
          ethers.formatEther(lpBefore),
        );
        expect(lpBefore).to.equal(0);
        console.log("\t", " 🔰 Calling init with 5 Eth");
        await dexContract.init(ethers.parseEther("5"), {
          value: ethers.parseEther("5"),
          gasLimit: 200000,
        });
        const lpAfter = await dexContract.totalLiquidity();
        console.log("\t", " 💦 Expecting new total liquidity to be 5.  Total liquidity:", ethers.formatEther(lpAfter));
      });
    });
  });
  // ----------------- END OF CHECKPOINT 2 -----------------
  // ----------------- START OF CHECKPOINT 3 -----------------
  describe("Checkpoint 3: Price", async () => {
    describe("price()", async () => {
      // https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#readContract
      // in Uniswap the fee is build in getAmountOut() function
      it("Should calculate the price correctly", async function () {
        let xInput = ethers.parseEther("1");
        let xReserves = ethers.parseEther("5");
        let yReserves = ethers.parseEther("5");
        let yOutput = await dexContract.price(xInput, xReserves, yReserves);
        expect(
          yOutput.toString(),
          "Check your price function's calculations.  Don't forget the 3% fee for liquidity providers, and the function should be view or pure.",
        ).to.equal("831248957812239453");
        xInput = ethers.parseEther("1");
        xReserves = ethers.parseEther("10");
        yReserves = ethers.parseEther("15");
        yOutput = await dexContract.price(xInput, xReserves, yReserves);
        expect(yOutput.toString()).to.equal("1359916340820223697");
      });
    });
  });
  // ----------------- END OF CHECKPOINT 3 -----------------
  // ----------------- START OF CHECKPOINT 4 -----------------
  describe("Checkpoint 4: Trading", function () {
    deployNewInstance();
    describe("ethToToken()", function () {
      it("Should be able to send 1 Ether to DEX in exchange for _ $BAL", async function () {
        const dexContractAddress = await dexContract.getAddress();

        const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
        console.log("\t", " 💵 Dex contract's initial Eth balance:", ethers.formatEther(dex_eth_start));
        console.log("\t", " 📞 Calling ethToToken with a value of 1 Eth...");
        const tx1 = await dexContract.ethToToken({
          value: ethers.parseEther("1"),
        });

        expect(tx1, "ethToToken should revert before initalization").not.to.be.reverted;

        console.log("\t", " 🔰 Initializing...");
        const tx1_receipt = await tx1.wait();
        const ethSent_1 = await getEventValue(tx1_receipt, 2);

        console.log("\t", " 🔼 Expecting the Eth value emitted to be 1.  Value:", ethers.formatEther(ethSent_1));
        expect(ethSent_1, "Check you are emitting the correct Eth value and in the correct order").to.equal(
          ethers.parseEther("1"),
        );

        const dex_eth_after = await ethers.provider.getBalance(dexContractAddress);
        console.log("\t", " 💵 Dex contract's new Eth balance:", ethers.formatEther(dex_eth_after));

        console.log("\t", " 💵 Expecting final Dex balance to have increased by 1...");
        expect(await ethers.provider.getBalance(dexContractAddress)).to.equal(ethers.parseEther("6"));
      });

      it("Should revert if 0 ETH sent", async function () {
        await expect(
          dexContract.ethToToken({ value: ethers.parseEther("0") }),
          "ethToToken should revert when sending 0 value...",
        ).to.be.reverted;
      });

      it("Should send less tokens after the first trade (ethToToken called)", async function () {
        const user2BalBefore = await balloonsContract.balanceOf(user2.address);
        console.log("\t", " 💵 User2 initial $BAL balance:", ethers.formatEther(user2BalBefore));
        console.log("\t", " 🥈 User2 calling ethToToken with value of 1 ETH...");
        await dexContract.connect(user2).ethToToken({ value: ethers.parseEther("1") });

        const user2BalAfter = await balloonsContract.balanceOf(user2.address);
        console.log("\t", " 💵 User2 new $BAL balance:", ethers.formatEther(user2BalAfter));

        const user3BalBefore = await balloonsContract.balanceOf(user3.address);
        console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3BalBefore));
        console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
        await dexContract.connect(user3).ethToToken({ value: ethers.parseEther("1") });

        const user3BalAfter = await balloonsContract.balanceOf(user3.address);
        console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3BalAfter));

        console.log("\t", " 💵 Expecting User2 to have aquired more $BAL than User3...");
        expect(user2BalAfter).to.greaterThan(user3BalAfter);
      });

      it("Should emit an event when ethToToken() called", async function () {
        await expect(
          dexContract.ethToToken({ value: ethers.parseEther("1") }),
          "Make sure you're emitting the EthToTokenSwap event correctly",
        ).to.emit(dexContract, "EthToTokenSwap");
      });

      it("Should transfer tokens to purchaser after trade", async function () {
        const user3_token_before = await balloonsContract.balanceOf(user3.address);
        console.log("\t", " 💵 User3 initial $BAL balance:", ethers.formatEther(user3_token_before));

        console.log("\t", " 🥉 User3 calling ethToToken with value of 1 ETH...");
        const tx1 = await dexContract.connect(user3).ethToToken({
          value: ethers.parseEther("1"),
        });
        await tx1.wait();

        const user3_token_after = await balloonsContract.balanceOf(user3.address);
        console.log("\t", " 💵 User3 new $BAL balance:", ethers.formatEther(user3_token_after));

        const tokenDifferece = user3_token_after - user3_token_before;
        console.log("\t", " 🥉 Expecting user3's $BAL balance to increase by the correct amount...");
        expect(tokenDifferece).to.be.equal("277481486896167099");
      });
      // could insert more tests to show the declining price, and what happens when the pool becomes very imbalanced.
    });

    describe("tokenToEth()", async () => {
      it("Should send 1 $BAL to DEX in exchange for _ $ETH", async function () {
        const dexContractAddress = await dexContract.getAddress();
        const balloons_bal_start = await balloonsContract.balanceOf(dexContractAddress);
        console.log("\t", " 💵 Initial Ballons $BAL balance:", ethers.formatEther(balloons_bal_start));
        const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
        console.log("\t", " 💵 Initial DEX Eth balance:", ethers.formatEther(dex_eth_start));

        console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
        const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));

        const balloons_bal_end = await balloonsContract.balanceOf(dexContractAddress);
        console.log("\t", " 💵 Final Ballons $BAL balance:", ethers.formatEther(balloons_bal_end));
        const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
        console.log("\t", " 💵 Final DEX Eth balance:", ethers.formatEther(dex_eth_end));

        await expect(tx1).not.to.be.revertedWith("Contract not initialized");
        // Checks that the balance of the DEX contract has decreased by 1 $BAL
        console.log("\t", " 🎈 Expecting the $BAL balance of the Ballon contract to have increased by 1...");
        expect(await balloonsContract.balanceOf(dexContractAddress)).to.equal(
          balloons_bal_start + ethers.parseEther("1"),
        );
        // Checks that the balance of the DEX contract has increased
        console.log("\t", " ⚖️ Expecting the balance of the Dex contract to have decreased...");
        expect(await ethers.provider.getBalance(dexContractAddress)).to.lessThan(dex_eth_start);
      });

      it("Should revert if 0 tokens sent to the DEX", async function () {
        await expect(dexContract.tokenToEth(ethers.parseEther("0"))).to.be.reverted;
      });

      it("Should emit event TokenToEthSwap when tokenToEth() called", async function () {
        await expect(
          dexContract.tokenToEth(ethers.parseEther("1")),
          "Make sure you're emitting the TokenToEthSwap event correctly",
        ).to.emit(dexContract, "TokenToEthSwap");
      });

      it("Should send less eth after the first trade (tokenToEth() called)", async function () {
        const dexContractAddress = await dexContract.getAddress();
        const dex_eth_start = await ethers.provider.getBalance(dexContractAddress);
        console.log("\t", " 💵 Initial Dex balance:", ethers.formatEther(dex_eth_start));
        console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
        const tx1 = await dexContract.tokenToEth(ethers.parseEther("1"));
        await tx1.wait();

        const dex_eth_next = await ethers.provider.getBalance(dexContractAddress);
        const tx1difference = dex_eth_next - dex_eth_start;
        console.log(
          "\t",
          " 💵 Next Dex balance:",
          ethers.formatEther(dex_eth_next),
          " Eth sent:",
          ethers.formatEther(tx1difference * -1n),
        );

        console.log("\t", " 📞 Calling tokenToEth with 1 Eth...");
        const tx2 = await dexContract.tokenToEth(ethers.parseEther("1"));
        await tx2.wait();

        const dex_eth_end = await ethers.provider.getBalance(dexContractAddress);
        const tx2difference = dex_eth_end - dex_eth_next;
        console.log(
          "\t",
          " 💵 Final Dex balance:",
          ethers.formatEther(dex_eth_end),
          " Eth sent:",
          ethers.formatEther(tx2difference * -1n),
        );

        console.log("\t", " ➖ Expecting the first call to get more Eth than the second...");
        expect(tx2difference).to.greaterThan(tx1difference);
      });
    });
  });
  // ----------------- END OF CHECKPOINT 4 -----------------
  // ----------------- START OF CHECKPOINT 5 -----------------
  describe("Checkpoint 5: Liquidity", async () => {
    describe("deposit()", async () => {
      deployNewInstance();
      it("Should increase liquidity in the pool when ETH is deposited", async function () {
        const dexContractAddress = await dexContract.getAddress();
        console.log("\t", " 💵 Approving 100 ETH...");
        await balloonsContract.connect(user2).approve(dexContractAddress, ethers.parseEther("100"));
        const liquidity_start = await dexContract.totalLiquidity();
        console.log("\t", " ⚖️ Starting Dex liquidity", ethers.formatEther(liquidity_start));
        const user2liquidity = await dexContract.getLiquidity(user2.address);
        console.log("\t", " ⚖️ Expecting user's liquidity to be 0. Liquidity:", ethers.formatEther(user2liquidity));
        expect(user2liquidity).to.equal("0");
        console.log("\t", " 🔼 Expecting the deposit function to emit correctly...");
        await expect(
          dexContract.connect(user2).deposit((ethers.parseEther("5"), { value: ethers.parseEther("5") })),
          "Check the order of the values when emitting LiquidityProvided: msg.sender, liquidityMinted, msg.value, tokenDeposit",
        )
          .to.emit(dexContract, "LiquidityProvided")
          .withArgs(anyValue, ethers.parseEther("5"), ethers.parseEther("5"), anyValue);
        const liquidity_end = await dexContract.totalLiquidity();
        console.log(
          "\t",
          " 💸 Final liquidity should increase by 5.  Final liquidity:",
          ethers.formatEther(liquidity_end),
        );
        expect(liquidity_end, "Total liquidity should increase").to.equal(liquidity_start + ethers.parseEther("5"));
        const user_lp = await dexContract.getLiquidity(user2.address);
        console.log("\t", " ⚖️ User's liquidity provided should be 5. LP:", ethers.formatEther(user_lp));
        expect(user_lp.toString(), "User's liquidity provided should be 5.").to.equal(ethers.parseEther("5"));
      });

      it("Should revert if 0 ETH deposited", async function () {
        await expect(
          dexContract.deposit((ethers.parseEther("0"), { value: ethers.parseEther("0") })),
          "Should revert if 0 value is sent",
        ).to.be.reverted;
      });
    });
    // pool should have 5:5 ETH:$BAL ratio
    describe("withdraw()", async () => {
      deployNewInstance();
      it("Should withdraw 1 ETH and 1 $BAL when pool is at a 1:1 ratio", async function () {
        const startingLiquidity = await dexContract.totalLiquidity();
        console.log("\t", " ⚖️ Starting liquidity:", ethers.formatEther(startingLiquidity));
        const userBallonsBalance = await balloonsContract.balanceOf(deployer.address);
        console.log("\t", " 💵 User's starting $BAL balance:", ethers.formatEther(userBallonsBalance));

        console.log("\t", " 🔽 Calling withdraw with with a value of 1 Eth...");
        const tx1 = await dexContract.withdraw(ethers.parseEther("1"));
        const userBallonsBalanceAfter = await balloonsContract.balanceOf(deployer.address);
        const tx1_receipt = await tx1.wait();

        const eth_out = await getEventValue(tx1_receipt, 2);
        const token_out = await getEventValue(tx1_receipt, 3);

        console.log("\t", " 💵 User's new $BAL balance:", ethers.formatEther(userBallonsBalanceAfter));
        console.log(
          "\t",
          " 🎈 Expecting the balance to have increased by 1",
          ethers.formatEther(userBallonsBalanceAfter),
        );
        expect(userBallonsBalance, "User $BAL balance shoud increase").to.equal(
          userBallonsBalanceAfter - ethers.parseEther("1"),
        );

        console.log("\t", " 🔽 Expecting the Eth withdrawn emit to be 1...");
        expect(eth_out, "ethWithdrawn incorrect in emit").to.be.equal(ethers.parseEther("1"));
        console.log("\t", " 🔽 Expecting the $BAL withdrawn emit to be 1...");
        expect(token_out, "tokenAmount incorrect in emit").to.be.equal(ethers.parseEther("1"));
      });

      it("Should revert if sender does not have enough liqudity", async function () {
        console.log("\t", " ✋ Expecting withdraw to revert when there is not enough liquidity...");
        await expect(dexContract.withdraw(ethers.parseEther("100"))).to.be.reverted;
      });

      it("Should decrease total liquidity", async function () {
        const totalLpBefore = await dexContract.totalLiquidity();
        console.log("\t", " ⚖️ Initial liquidity:", ethers.formatEther(totalLpBefore));
        console.log("\t", " 📞 Calling withdraw with 1 Eth...");
        const txWithdraw = await dexContract.withdraw(ethers.parseEther("1"));
        const totalLpAfter = await dexContract.totalLiquidity();
        console.log("\t", " ⚖️ Final liquidity:", ethers.formatEther(totalLpAfter));
        const txWithdraw_receipt = await txWithdraw.wait();
        const liquidityBurned = await getEventValue(txWithdraw_receipt, 2);
        console.log("\t", " 🔼 Emitted liquidity removed:", ethers.formatEther(liquidityBurned));

        expect(totalLpAfter, "Emitted incorrect liquidity amount burned").to.be.equal(totalLpBefore - liquidityBurned);
        expect(totalLpBefore, "Emitted total liquidity should decrease").to.be.above(totalLpAfter);
      });

      it("Should emit event LiquidityWithdrawn when withdraw() called", async function () {
        await expect(
          dexContract.withdraw(ethers.parseEther("1.5")),
          "Make sure you emit the LiquidityRemoved event correctly",
        )
          .to.emit(dexContract, "LiquidityRemoved")
          .withArgs(deployer.address, ethers.parseEther("1.5"), ethers.parseEther("1.5"), ethers.parseEther("1.5"));
      });
    });
  });
  // ----------------- END OF CHECKPOINT 5 -----------------
});
