import chai, { expect, assert } from "chai";
import chaiAsPromised from "chai-as-promised";
import chaiSpies from "chai-spies";
import { AddressZero } from "@ethersproject/constants";
import { BigNumber } from "@ethersproject/bignumber";
import { Signer } from "@ethersproject/abstract-signer";
import { ethers, network, deployLiquity } from "hardhat";

import {
  Decimal,
  Decimalish,
  Trove,
  StabilityDeposit,
  LiquityReceipt,
  SuccessfulReceipt,
  SentLiquityTransaction,
  TroveCreationParams,
  Fees,
  ZUSD_LIQUIDATION_RESERVE,
  MAXIMUM_BORROWING_RATE,
  MINIMUM_BORROWING_RATE,
  ZUSD_MINIMUM_DEBT,
  ZUSD_MINIMUM_NET_DEBT
} from "@sovryn-zero/lib-base";

import { HintHelpers } from "../types";

import {
  PopulatableEthersLiquity,
  PopulatedEthersLiquityTransaction,
  _redeemMaxIterations
} from "../src/PopulatableEthersLiquity";

import { _LiquityDeploymentJSON } from "../src/contracts";
import { _connectToDeployment } from "../src/EthersLiquityConnection";
import { EthersLiquity } from "../src/EthersLiquity";
import { ReadableEthersLiquity } from "../src/ReadableEthersLiquity";
import mockBalanceRedirectPresaleAbi from "../abi/MockBalanceRedirectPresale.json";

const provider = ethers.provider;

chai.use(chaiAsPromised);
chai.use(chaiSpies);

const connectToDeployment = async (
  deployment: _LiquityDeploymentJSON,
  signer: Signer,
  frontendTag?: string
) =>
  EthersLiquity._from(
    _connectToDeployment(deployment, signer, {
      userAddress: await signer.getAddress(),
      frontendTag
    })
  );

const increaseTime = async (timeJumpSeconds: number) => {
  await provider.send("evm_increaseTime", [timeJumpSeconds]);
};

function assertStrictEqual<T, U extends T>(
  actual: T,
  expected: U,
  message?: string
): asserts actual is U {
  assert.strictEqual(actual, expected, message);
}

function assertDefined<T>(actual: T | undefined): asserts actual is T {
  assert(actual !== undefined);
}

const waitForSuccess = async <T extends LiquityReceipt>(
  tx: Promise<SentLiquityTransaction<unknown, T>>
) => {
  const receipt = await (await tx).waitForReceipt();
  assertStrictEqual(receipt.status, "succeeded" as const);

  return receipt as Extract<T, SuccessfulReceipt>;
};

// TODO make the testcases isolated

// describe("EthersLiquity", () => {
//   let deployer: Signer;
//   let funder: Signer;
//   let user: Signer;
//   let otherUsers: Signer[];

//   let deployment: _LiquityDeploymentJSON;

//   let deployerLiquity: EthersLiquity;
//   let liquity: EthersLiquity;
//   let otherLiquities: EthersLiquity[];

//   const connectUsers = (users: Signer[]) =>
//     Promise.all(users.map(user => connectToDeployment(deployment, user)));

//   const openTroves = (users: Signer[], params: TroveCreationParams<Decimalish>[]) =>
//     params
//       .map((params, i) => () =>
//         Promise.all([
//           connectToDeployment(deployment, users[i]),
//           sendTo(users[i], params.depositCollateral).then(tx => tx.wait())
//         ]).then(async ([liquity]) => {
//           await liquity.openTrove(params, undefined, { gasPrice: 0 });
//         })
//       )
//       .reduce((a, b) => a.then(b), Promise.resolve());

//   const sendTo = (user: Signer, value: Decimalish, nonce?: number) =>
//     funder.sendTransaction({
//       to: user.getAddress(),
//       value: Decimal.from(value).hex,
//       nonce
//     });

//   const sendToEach = async (users: Signer[], value: Decimalish) => {
//     const txCount = await provider.getTransactionCount(funder.getAddress());
//     const txs = await Promise.all(users.map((user, i) => sendTo(user, value, txCount + i)));

//     // Wait for the last tx to be mined.
//     await txs[txs.length - 1].wait();
//   };

//   before(async () => {
//     [deployer, funder, user, ...otherUsers] = await ethers.getSigners();
//     deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//     liquity = await connectToDeployment(deployment, user);
//     expect(liquity).to.be.an.instanceOf(EthersLiquity);
//   });

//   // Always setup same initial balance for user
//   beforeEach(async () => {
//     const targetBalance = BigNumber.from(Decimal.from(100).hex);
//     const balance = await user.getBalance();
//     const gasPrice = 0;

//     if (balance.eq(targetBalance)) {
//       return;
//     }

//     if (balance.gt(targetBalance)) {
//       await user.sendTransaction({
//         to: funder.getAddress(),
//         value: balance.sub(targetBalance),
//         gasPrice
//       });
//     } else {
//       await funder.sendTransaction({
//         to: user.getAddress(),
//         value: targetBalance.sub(balance),
//         gasPrice
//       });
//     }

//     expect(`${await user.getBalance()}`).to.equal(`${targetBalance}`);
//   });

//   it("should get the price", async () => {
//     const price = await liquity.getPrice();
//     expect(price).to.be.an.instanceOf(Decimal);
//   });

//   describe("findHintForCollateralRatio", () => {
//     it("should pick the closest approx hint", async () => {
//       type Resolved<T> = T extends Promise<infer U> ? U : never;
//       type ApproxHint = Resolved<ReturnType<HintHelpers["getApproxHint"]>>;

//       const fakeHints: ApproxHint[] = [
//         { diff: BigNumber.from(3), hintAddress: "alice", latestRandomSeed: BigNumber.from(1111) },
//         { diff: BigNumber.from(4), hintAddress: "bob", latestRandomSeed: BigNumber.from(2222) },
//         { diff: BigNumber.from(1), hintAddress: "carol", latestRandomSeed: BigNumber.from(3333) },
//         { diff: BigNumber.from(2), hintAddress: "dennis", latestRandomSeed: BigNumber.from(4444) }
//       ];

//       const borrowerOperations = {
//         estimateAndPopulate: {
//           openTrove: () => ({})
//         }
//       };

//       const hintHelpers = chai.spy.interface({
//         getApproxHint: () => Promise.resolve(fakeHints.shift())
//       });

//       const sortedTroves = chai.spy.interface({
//         findInsertPosition: () => Promise.resolve(["fake insert position"])
//       });

//       const fakeLiquity = new PopulatableEthersLiquity(({
//         getNumberOfTroves: () => Promise.resolve(1000000),
//         getFees: () => Promise.resolve(new Fees(0, 0.99, 1, new Date(), new Date(), false)),

//         connection: {
//           signerOrProvider: user,
//           _contracts: {
//             borrowerOperations,
//             hintHelpers,
//             sortedTroves
//           }
//         }
//       } as unknown) as ReadableEthersLiquity);

//       const nominalCollateralRatio = Decimal.from(0.5);

//       const params = Trove.recreate(new Trove(Decimal.from(1), ZUSD_MINIMUM_DEBT));
//       const trove = Trove.create(params);
//       expect(`${trove._nominalCollateralRatio}`).to.equal(`${nominalCollateralRatio}`);

//       await fakeLiquity.openTrove(params);

//       expect(hintHelpers.getApproxHint).to.have.been.called.exactly(4);
//       expect(hintHelpers.getApproxHint).to.have.been.called.with(nominalCollateralRatio.hex);

//       // returned latestRandomSeed should be passed back on the next call
//       expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(1111));
//       expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(2222));
//       expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(3333));

//       expect(sortedTroves.findInsertPosition).to.have.been.called.once;
//       expect(sortedTroves.findInsertPosition).to.have.been.called.with(
//         nominalCollateralRatio.hex,
//         "carol"
//       );
//     });
//   });

//   describe("Trove", () => {
//     it("should have no Trove initially", async () => {
//       const trove = await liquity.getTrove();
//       expect(trove.isEmpty).to.be.true;
//     });

//     it("should fail to create an undercollateralized Trove", async () => {
//       const price = await liquity.getPrice();
//       const undercollateralized = new Trove(ZUSD_MINIMUM_DEBT.div(price), ZUSD_MINIMUM_DEBT);

//       await expect(liquity.openTrove(Trove.recreate(undercollateralized))).to.eventually.be.rejected;
//     });

//     it("should fail to create a Trove with too little debt", async () => {
//       const withTooLittleDebt = new Trove(Decimal.from(50), ZUSD_MINIMUM_DEBT.sub(1));

//       await expect(liquity.openTrove(Trove.recreate(withTooLittleDebt))).to.eventually.be.rejected;
//     });

//     const withSomeBorrowing = { depositCollateral: 50, borrowZUSD: ZUSD_MINIMUM_NET_DEBT.add(100) };

//     it("should create a Trove with some borrowing", async () => {
//       const { newTrove, fee } = await liquity.openTrove(withSomeBorrowing);
//       expect(newTrove).to.deep.equal(Trove.create(withSomeBorrowing));
//       expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(withSomeBorrowing.borrowZUSD)}`);
//     });

//     it("should fail to withdraw all the collateral while the Trove has debt", async () => {
//       const trove = await liquity.getTrove();

//       await expect(liquity.withdrawCollateral(trove.collateral)).to.eventually.be.rejected;
//     });

//     const repaySomeDebt = { repayZUSD: 10 };

//     it("should repay some debt", async () => {
//       const { newTrove, fee } = await liquity.repayZUSD(repaySomeDebt.repayZUSD);
//       expect(newTrove).to.deep.equal(Trove.create(withSomeBorrowing).adjust(repaySomeDebt));
//       expect(`${fee}`).to.equal("0");
//     });

//     const borrowSomeMore = { borrowZUSD: 20 };

//     it("should borrow some more", async () => {
//       const { newTrove, fee } = await liquity.borrowZUSD(borrowSomeMore.borrowZUSD);
//       expect(newTrove).to.deep.equal(
//         Trove.create(withSomeBorrowing).adjust(repaySomeDebt).adjust(borrowSomeMore)
//       );
//       expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(borrowSomeMore.borrowZUSD)}`);
//     });

//     const depositMoreCollateral = { depositCollateral: 1 };

//     it("should deposit more collateral", async () => {
//       const { newTrove } = await liquity.depositCollateral(depositMoreCollateral.depositCollateral);
//       expect(newTrove).to.deep.equal(
//         Trove.create(withSomeBorrowing)
//           .adjust(repaySomeDebt)
//           .adjust(borrowSomeMore)
//           .adjust(depositMoreCollateral)
//       );
//     });

//     const repayAndWithdraw = { repayZUSD: 60, withdrawCollateral: 0.5 };

//     it("should repay some debt and withdraw some collateral at the same time", async () => {
//       const { newTrove } = await liquity.adjustTrove(repayAndWithdraw, undefined, { gasPrice: 0 });

//       expect(newTrove).to.deep.equal(
//         Trove.create(withSomeBorrowing)
//           .adjust(repaySomeDebt)
//           .adjust(borrowSomeMore)
//           .adjust(depositMoreCollateral)
//           .adjust(repayAndWithdraw)
//       );

//       const ethBalance = Decimal.fromBigNumberString(`${await user.getBalance()}`);
//       expect(`${ethBalance}`).to.equal("100.5");
//     });

//     const borrowAndDeposit = { borrowZUSD: 60, depositCollateral: 0.5 };

//     it("should borrow more and deposit some collateral at the same time", async () => {
//       const { newTrove, fee } = await liquity.adjustTrove(borrowAndDeposit, undefined, {
//         gasPrice: 0
//       });

//       expect(newTrove).to.deep.equal(
//         Trove.create(withSomeBorrowing)
//           .adjust(repaySomeDebt)
//           .adjust(borrowSomeMore)
//           .adjust(depositMoreCollateral)
//           .adjust(repayAndWithdraw)
//           .adjust(borrowAndDeposit)
//       );

//       expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(borrowAndDeposit.borrowZUSD)}`);

//       const ethBalance = Decimal.fromBigNumberString(`${await user.getBalance()}`);
//       expect(`${ethBalance}`).to.equal("99.5");
//     });

//     it("should close the Trove with some ZUSD from another user", async () => {
//       const price = await liquity.getPrice();
//       const initialTrove = await liquity.getTrove();
//       const zusdBalance = await liquity.getZEROBalance();
//       const zusdShortage = initialTrove.netDebt.sub(zusdBalance);

//       let funderTrove = Trove.create({ depositCollateral: 1, borrowZUSD: zusdShortage });
//       funderTrove = funderTrove.setDebt(Decimal.max(funderTrove.debt, ZUSD_MINIMUM_DEBT));
//       funderTrove = funderTrove.setCollateral(funderTrove.debt.mulDiv(1.51, price));

//       const funderLiquity = await connectToDeployment(deployment, funder);
//       await funderLiquity.openTrove(Trove.recreate(funderTrove));
//       await funderLiquity.sendZUSD(await user.getAddress(), zusdShortage);

//       const { params } = await liquity.closeTrove();

//       expect(params).to.deep.equal({
//         withdrawCollateral: initialTrove.collateral,
//         repayZUSD: initialTrove.netDebt
//       });

//       const finalTrove = await liquity.getTrove();
//       expect(finalTrove.isEmpty).to.be.true;
//     });
//   });

//   describe("SendableEthersLiquity", () => {
//     it("should parse failed transactions without throwing", async () => {
//       // By passing a gasLimit, we avoid automatic use of estimateGas which would throw
//       const tx = await liquity.send.openTrove(
//         { depositCollateral: 0.01, borrowZUSD: 0.01 },
//         undefined,
//         { gasLimit: 1e6 }
//       );
//       const { status } = await tx.waitForReceipt();

//       expect(status).to.equal("failed");
//     });
//   });

//   describe("Frontend", () => {
//     it("should have no frontend initially", async () => {
//       const frontend = await liquity.getFrontendStatus(await user.getAddress());

//       assertStrictEqual(frontend.status, "unregistered" as const);
//     });

//     it("should register a frontend", async () => {
//       await liquity.registerFrontend(0.75);
//     });

//     it("should have a frontend now", async () => {
//       const frontend = await liquity.getFrontendStatus(await user.getAddress());

//       assertStrictEqual(frontend.status, "registered" as const);
//       expect(`${frontend.kickbackRate}`).to.equal("0.75");
//     });

//     it("other user's deposit should be tagged with the frontend's address", async () => {
//       const frontendTag = await user.getAddress();

//       await funder.sendTransaction({
//         to: otherUsers[0].getAddress(),
//         value: Decimal.from(20.1).hex
//       });

//       const otherLiquity = await connectToDeployment(deployment, otherUsers[0], frontendTag);
//       await otherLiquity.openTrove({ depositCollateral: 20, borrowZUSD: ZUSD_MINIMUM_DEBT });

//       if (deployment.presaleAddress) {
//         const presale = new ethers.Contract(
//           deployment.presaleAddress,
//           mockBalanceRedirectPresaleAbi,
//           provider
//         );
//         await presale.connect(deployer).closePresale();
//       }
//       await otherLiquity.depositZUSDInStabilityPool(ZUSD_MINIMUM_DEBT);

//       const deposit = await otherLiquity.getStabilityDeposit();
//       expect(deposit.frontendTag).to.equal(frontendTag);
//     });
//   });

//   describe("StabilityPool", () => {
//     before(async () => {
//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//       [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([
//         deployer,
//         user,
//         ...otherUsers.slice(0, 1)
//       ]);

//       await funder.sendTransaction({
//         to: otherUsers[0].getAddress(),
//         value: ZUSD_MINIMUM_DEBT.div(170).hex
//       });
//     });

//     const initialTroveOfDepositor = Trove.create({
//       depositCollateral: ZUSD_MINIMUM_DEBT.div(100),
//       borrowZUSD: ZUSD_MINIMUM_NET_DEBT
//     });

//     const smallStabilityDeposit = Decimal.from(10);

//     it("should fail if Zero presale is open", async () => {
//       await expect(liquity.depositZUSDInStabilityPool(smallStabilityDeposit)).to.eventually.be
//         .rejected;
//     });

//     it("should make a small stability deposit", async () => {
//       const { newTrove } = await liquity.openTrove(Trove.recreate(initialTroveOfDepositor));
//       expect(newTrove).to.deep.equal(initialTroveOfDepositor);

//       if (deployment.presaleAddress) {
//         const presale = new ethers.Contract(
//           deployment.presaleAddress,
//           mockBalanceRedirectPresaleAbi,
//           provider
//         );
//         await presale.connect(deployer).closePresale();
//       }

//       const details = await liquity.depositZUSDInStabilityPool(smallStabilityDeposit);

//       expect(details).to.deep.equal({
//         zusdLoss: Decimal.from(0),
//         newZUSDDeposit: smallStabilityDeposit,
//         collateralGain: Decimal.from(0),
//         zeroReward: Decimal.from(0),

//         change: {
//           depositZUSD: smallStabilityDeposit
//         }
//       });
//     });

//     const troveWithVeryLowICR = Trove.create({
//       depositCollateral: ZUSD_MINIMUM_DEBT.div(180),
//       borrowZUSD: ZUSD_MINIMUM_NET_DEBT
//     });

//     it("other user should make a Trove with very low ICR", async () => {
//       const { newTrove } = await otherLiquities[0].openTrove(Trove.recreate(troveWithVeryLowICR));

//       const price = await liquity.getPrice();
//       expect(Number(`${newTrove.collateralRatio(price)}`)).to.be.below(1.15);
//     });

//     const dippedPrice = Decimal.from(190);

//     it("the price should take a dip", async () => {
//       await deployerLiquity.setPrice(dippedPrice);

//       const price = await liquity.getPrice();
//       expect(`${price}`).to.equal(`${dippedPrice}`);
//     });

//     it("should liquidate other user's Trove", async () => {
//       const details = await liquity.liquidateUpTo(1);

//       expect(details).to.deep.equal({
//         liquidatedAddresses: [await otherUsers[0].getAddress()],

//         collateralGasCompensation: troveWithVeryLowICR.collateral.mul(0.005), // 0.5%
//         zusdGasCompensation: ZUSD_LIQUIDATION_RESERVE,

//         totalLiquidated: new Trove(
//           troveWithVeryLowICR.collateral
//             .mul(0.995) // -0.5% gas compensation
//             .add("0.000000000000000001"), // tiny imprecision
//           troveWithVeryLowICR.debt
//         )
//       });

//       const otherTrove = await otherLiquities[0].getTrove();
//       expect(otherTrove.isEmpty).to.be.true;
//     });

//     it("should have a depleted stability deposit and some collateral gain", async () => {
//       const stabilityDeposit = await liquity.getStabilityDeposit();

//       expect(stabilityDeposit).to.deep.equal(
//         new StabilityDeposit(
//           smallStabilityDeposit,
//           Decimal.ZERO,
//           troveWithVeryLowICR.collateral
//             .mul(0.995) // -0.5% gas compensation
//             .mulDiv(smallStabilityDeposit, troveWithVeryLowICR.debt)
//             .sub("0.000000000000000005"), // tiny imprecision
//           Decimal.ZERO,
//           AddressZero
//         )
//       );
//     });

//     it("the Trove should have received some liquidation shares", async () => {
//       const trove = await liquity.getTrove();

//       expect(trove).to.deep.equal({
//         ownerAddress: await user.getAddress(),
//         status: "open",

//         ...initialTroveOfDepositor
//           .addDebt(troveWithVeryLowICR.debt.sub(smallStabilityDeposit))
//           .addCollateral(
//             troveWithVeryLowICR.collateral
//               .mul(0.995) // -0.5% gas compensation
//               .mulDiv(troveWithVeryLowICR.debt.sub(smallStabilityDeposit), troveWithVeryLowICR.debt)
//               .add("0.000000000000000001") // tiny imprecision
//           )
//       });
//     });

//     it("total should equal the Trove", async () => {
//       const trove = await liquity.getTrove();

//       const numberOfTroves = await liquity.getNumberOfTroves();
//       expect(numberOfTroves).to.equal(1);

//       const total = await liquity.getTotal();
//       expect(total).to.deep.equal(
//         trove.addCollateral("0.000000000000000001") // tiny imprecision
//       );
//     });

//     it("should transfer the gains to the Trove", async () => {
//       const details = await liquity.transferCollateralGainToTrove();

//       expect(details).to.deep.equal({
//         zusdLoss: smallStabilityDeposit,
//         newZUSDDeposit: Decimal.ZERO,
//         zeroReward: Decimal.ZERO,

//         collateralGain: troveWithVeryLowICR.collateral
//           .mul(0.995) // -0.5% gas compensation
//           .mulDiv(smallStabilityDeposit, troveWithVeryLowICR.debt)
//           .sub("0.000000000000000005"), // tiny imprecision

//         newTrove: initialTroveOfDepositor
//           .addDebt(troveWithVeryLowICR.debt.sub(smallStabilityDeposit))
//           .addCollateral(
//             troveWithVeryLowICR.collateral
//               .mul(0.995) // -0.5% gas compensation
//               .sub("0.000000000000000005") // tiny imprecision
//           )
//       });

//       const stabilityDeposit = await liquity.getStabilityDeposit();
//       expect(stabilityDeposit.isEmpty).to.be.true;
//     });

//     describe("when people overstay", () => {
//       before(async () => {
//         // Deploy new instances of the contracts, for a clean slate
//         deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//         const otherUsersSubset = otherUsers.slice(0, 5);
//         [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([
//           deployer,
//           user,
//           ...otherUsersSubset
//         ]);

//         if (deployment.presaleAddress) {
//           const presale = new ethers.Contract(
//             deployment.presaleAddress,
//             mockBalanceRedirectPresaleAbi,
//             provider
//           );
//           await presale.connect(deployer).closePresale();
//         }
//         await sendToEach(otherUsersSubset, 21.1);

//         let price = Decimal.from(200);
//         await deployerLiquity.setPrice(price);

//         // Use this account to print ZUSD
//         await liquity.openTrove({ depositCollateral: 50, borrowZUSD: 5000 });

//         // otherLiquities[0-2] will be independent stability depositors
//         await liquity.sendZUSD(await otherUsers[0].getAddress(), 3000);
//         await liquity.sendZUSD(await otherUsers[1].getAddress(), 1000);
//         await liquity.sendZUSD(await otherUsers[2].getAddress(), 1000);

//         // otherLiquities[3-4] will be Trove owners whose Troves get liquidated
//         await otherLiquities[3].openTrove({ depositCollateral: 21, borrowZUSD: 2900 });
//         await otherLiquities[4].openTrove({ depositCollateral: 21, borrowZUSD: 2900 });

//         await otherLiquities[0].depositZUSDInStabilityPool(3000);
//         await otherLiquities[1].depositZUSDInStabilityPool(1000);
//         // otherLiquities[2] doesn't deposit yet

//         // Tank the price so we can liquidate
//         price = Decimal.from(150);
//         await deployerLiquity.setPrice(price);

//         // Liquidate first victim
//         await liquity.liquidate(await otherUsers[3].getAddress());
//         expect((await otherLiquities[3].getTrove()).isEmpty).to.be.true;

//         // Now otherLiquities[2] makes their deposit too
//         await otherLiquities[2].depositZUSDInStabilityPool(1000);

//         // Liquidate second victim
//         await liquity.liquidate(await otherUsers[4].getAddress());
//         expect((await otherLiquities[4].getTrove()).isEmpty).to.be.true;

//         // Stability Pool is now empty
//         expect(`${await liquity.getZUSDInStabilityPool()}`).to.equal("0");
//       });

//       it("should still be able to withdraw remaining deposit", async () => {
//         for (const l of [otherLiquities[0], otherLiquities[1], otherLiquities[2]]) {
//           const stabilityDeposit = await l.getStabilityDeposit();
//           await l.withdrawZUSDFromStabilityPool(stabilityDeposit.currentZUSD);
//         }
//       });
//     });
//   });

//   describe("Redemption", () => {
//     const troveCreations = [
//       { depositCollateral: 99, borrowZUSD: 4600 },
//       { depositCollateral: 20, borrowZUSD: 2000 }, // net debt: 2010
//       { depositCollateral: 20, borrowZUSD: 2100 }, // net debt: 2110.5
//       { depositCollateral: 20, borrowZUSD: 2200 } //  net debt: 2211
//     ];

//     before(async function () {
//       if (network.name !== "hardhat") {
//         // Redemptions are only allowed after a bootstrap phase of 2 weeks.
//         // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere.
//         this.skip();
//       }

//       // Deploy new instances of the contracts, for a clean slate
//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//       const otherUsersSubset = otherUsers.slice(0, 3);
//       [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([
//         deployer,
//         user,
//         ...otherUsersSubset
//       ]);

//       await sendToEach(otherUsersSubset, 20.1);
//     });

//     it("should fail to redeem during the bootstrap phase", async () => {
//       await liquity.openTrove(troveCreations[0]);
//       await otherLiquities[0].openTrove(troveCreations[1]);
//       await otherLiquities[1].openTrove(troveCreations[2]);
//       await otherLiquities[2].openTrove(troveCreations[3]);

//       await expect(liquity.redeemZUSD(4326.5, undefined, { gasPrice: 0 })).to.eventually.be.rejected;
//     });

//     const someZUSD = Decimal.from(4326.5);

//     it("should redeem some ZUSD after the bootstrap phase", async () => {
//       // Fast-forward 15 days
//       increaseTime(60 * 60 * 24 * 15);

//       expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0");
//       expect(`${await otherLiquities[1].getCollateralSurplusBalance()}`).to.equal("0");
//       expect(`${await otherLiquities[2].getCollateralSurplusBalance()}`).to.equal("0");

//       const expectedTotal = troveCreations
//         .map(params => Trove.create(params))
//         .reduce((a, b) => a.add(b));

//       const total = await liquity.getTotal();
//       expect(total).to.deep.equal(expectedTotal);

//       const expectedDetails = {
//         attemptedZUSDAmount: someZUSD,
//         actualZUSDAmount: someZUSD,
//         collateralTaken: someZUSD.div(200),
//         fee: new Fees(0, 0.99, 2, new Date(), new Date(), false)
//           .redemptionRate(someZUSD.div(total.debt))
//           .mul(someZUSD.div(200))
//       };

//       const details = await liquity.redeemZUSD(someZUSD, undefined, { gasPrice: 0 });
//       expect(details).to.deep.equal(expectedDetails);

//       const balance = Decimal.fromBigNumberString(`${await provider.getBalance(user.getAddress())}`);
//       expect(`${balance}`).to.equal(
//         `${expectedDetails.collateralTaken.sub(expectedDetails.fee).add(100)}`
//       );

//       expect(`${await liquity.getZUSDBalance()}`).to.equal("273.5");

//       expect(`${(await otherLiquities[0].getTrove()).debt}`).to.equal(
//         `${Trove.create(troveCreations[1]).debt.sub(
//           someZUSD
//             .sub(Trove.create(troveCreations[2]).netDebt)
//             .sub(Trove.create(troveCreations[3]).netDebt)
//         )}`
//       );

//       expect((await otherLiquities[1].getTrove()).isEmpty).to.be.true;
//       expect((await otherLiquities[2].getTrove()).isEmpty).to.be.true;
//     });

//     it("should claim the collateral surplus after redemption", async () => {
//       const balanceBefore1 = await provider.getBalance(otherUsers[1].getAddress());
//       const balanceBefore2 = await provider.getBalance(otherUsers[2].getAddress());

//       expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0");

//       const surplus1 = await otherLiquities[1].getCollateralSurplusBalance();
//       const trove1 = Trove.create(troveCreations[2]);
//       expect(`${surplus1}`).to.equal(`${trove1.collateral.sub(trove1.netDebt.div(200))}`);

//       const surplus2 = await otherLiquities[2].getCollateralSurplusBalance();
//       const trove2 = Trove.create(troveCreations[3]);
//       expect(`${surplus2}`).to.equal(`${trove2.collateral.sub(trove2.netDebt.div(200))}`);

//       await otherLiquities[1].claimCollateralSurplus({ gasPrice: 0 });
//       await otherLiquities[2].claimCollateralSurplus({ gasPrice: 0 });

//       expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0");
//       expect(`${await otherLiquities[1].getCollateralSurplusBalance()}`).to.equal("0");
//       expect(`${await otherLiquities[2].getCollateralSurplusBalance()}`).to.equal("0");

//       const balanceAfter1 = await provider.getBalance(otherUsers[1].getAddress());
//       const balanceAfter2 = await provider.getBalance(otherUsers[2].getAddress());
//       expect(`${balanceAfter1}`).to.equal(`${balanceBefore1.add(surplus1.hex)}`);
//       expect(`${balanceAfter2}`).to.equal(`${balanceBefore2.add(surplus2.hex)}`);
//     });

//     it("borrowing rate should be maxed out now", async () => {
//       const borrowZUSD = Decimal.from(10);

//       const { fee, newTrove } = await liquity.borrowZUSD(borrowZUSD);
//       expect(`${fee}`).to.equal(`${borrowZUSD.mul(MAXIMUM_BORROWING_RATE)}`);

//       expect(newTrove).to.deep.equal(
//         Trove.create(troveCreations[0]).adjust({ borrowZUSD }, MAXIMUM_BORROWING_RATE)
//       );
//     });
//   });

//   describe("Redemption (truncation)", () => {
//     const troveCreationParams = { depositCollateral: 20, borrowZUSD: 2000 };
//     const netDebtPerTrove = Trove.create(troveCreationParams).netDebt;
//     const amountToAttempt = Decimal.from(3900);
//     const expectedRedeemable = netDebtPerTrove.mul(2).sub(ZUSD_MINIMUM_NET_DEBT);

//     before(function () {
//       if (network.name !== "hardhat") {
//         // Redemptions are only allowed after a bootstrap phase of 2 weeks.
//         // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere.
//         this.skip();
//       }
//     });

//     beforeEach(async function () {
//       this.timeout("1m");
//       // Deploy new instances of the contracts, for a clean slate
//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//       const otherUsersSubset = otherUsers.slice(0, 3);
//       [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([
//         deployer,
//         user,
//         ...otherUsersSubset
//       ]);

//       await sendToEach(otherUsersSubset, 20.1);

//       await liquity.openTrove({ depositCollateral: 99, borrowZUSD: 5000 });
//       await otherLiquities[0].openTrove(troveCreationParams);
//       await otherLiquities[1].openTrove(troveCreationParams);
//       await otherLiquities[2].openTrove(troveCreationParams);

//       increaseTime(60 * 60 * 24 * 15);
//     });

//     it("should truncate the amount if it would put the last Trove below the min debt", async () => {
//       const redemption = await liquity.populate.redeemZUSD(amountToAttempt);
//       expect(`${redemption.attemptedZUSDAmount}`).to.equal(`${amountToAttempt}`);
//       expect(`${redemption.redeemableZUSDAmount}`).to.equal(`${expectedRedeemable}`);
//       expect(redemption.isTruncated).to.be.true;

//       const { details } = await waitForSuccess(redemption.send());
//       expect(`${details.attemptedZUSDAmount}`).to.equal(`${expectedRedeemable}`);
//       expect(`${details.actualZUSDAmount}`).to.equal(`${expectedRedeemable}`);
//     });

//     it("should increase the amount to the next lowest redeemable value", async () => {
//       const increasedRedeemable = expectedRedeemable.add(ZUSD_MINIMUM_NET_DEBT);

//       const initialRedemption = await liquity.populate.redeemZUSD(amountToAttempt);
//       const increasedRedemption = await initialRedemption.increaseAmountByMinimumNetDebt();
//       expect(`${increasedRedemption.attemptedZUSDAmount}`).to.equal(`${increasedRedeemable}`);
//       expect(`${increasedRedemption.redeemableZUSDAmount}`).to.equal(`${increasedRedeemable}`);
//       expect(increasedRedemption.isTruncated).to.be.false;

//       const { details } = await waitForSuccess(increasedRedemption.send());
//       expect(`${details.attemptedZUSDAmount}`).to.equal(`${increasedRedeemable}`);
//       expect(`${details.actualZUSDAmount}`).to.equal(`${increasedRedeemable}`);
//     });

//     it("should fail to increase the amount if it's not truncated", async () => {
//       const redemption = await liquity.populate.redeemZUSD(netDebtPerTrove);
//       expect(redemption.isTruncated).to.be.false;

//       expect(() => redemption.increaseAmountByMinimumNetDebt()).to.throw(
//         "can only be called when amount is truncated"
//       );
//     });
//   });

//   describe("Redemption (gas checks)", function () {
//     this.timeout("10m");

//     const massivePrice = Decimal.from(1000000);

//     const amountToBorrowPerTrove = Decimal.from(2000);
//     const netDebtPerTrove = MINIMUM_BORROWING_RATE.add(1).mul(amountToBorrowPerTrove);
//     const collateralPerTrove = netDebtPerTrove
//       .add(ZUSD_LIQUIDATION_RESERVE)
//       .mulDiv(1.5, massivePrice);

//     const amountToRedeem = netDebtPerTrove.mul(_redeemMaxIterations);
//     const amountToDeposit = MINIMUM_BORROWING_RATE.add(1)
//       .mul(amountToRedeem)
//       .add(ZUSD_LIQUIDATION_RESERVE)
//       .mulDiv(2, massivePrice);

//     before(async function () {
//       if (network.name !== "hardhat") {
//         // Redemptions are only allowed after a bootstrap phase of 2 weeks.
//         // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere.
//         this.skip();
//       }

//       // Deploy new instances of the contracts, for a clean slate
//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);
//       const otherUsersSubset = otherUsers.slice(0, _redeemMaxIterations);
//       expect(otherUsersSubset).to.have.length(_redeemMaxIterations);

//       [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([
//         deployer,
//         user,
//         ...otherUsersSubset
//       ]);

//       await deployerLiquity.setPrice(massivePrice);
//       await sendToEach(otherUsersSubset, collateralPerTrove);

//       for (const otherLiquity of otherLiquities) {
//         await otherLiquity.openTrove(
//           {
//             depositCollateral: collateralPerTrove,
//             borrowZUSD: amountToBorrowPerTrove
//           },
//           undefined,
//           { gasPrice: 0 }
//         );
//       }

//       increaseTime(60 * 60 * 24 * 15);
//     });

//     it("should redeem using the maximum iterations and almost all gas", async () => {
//       await liquity.openTrove({
//         depositCollateral: amountToDeposit,
//         borrowZUSD: amountToRedeem
//       });

//       const { rawReceipt } = await waitForSuccess(liquity.send.redeemZUSD(amountToRedeem));

//       const gasUsed = rawReceipt.gasUsed.toNumber();
//       // gasUsed is ~half the real used amount because of how refunds work, see:
//       // https://ethereum.stackexchange.com/a/859/9205
//       expect(gasUsed).to.be.at.least(4900000, "should use close to 10M gas");
//     });
//   });

//   describe("Gas estimation", () => {
//     const troveWithICRBetween = (a: Trove, b: Trove) => a.add(b).multiply(0.5);

//     let rudeUser: Signer;
//     let fiveOtherUsers: Signer[];
//     let rudeLiquity: EthersLiquity;

//     before(async function () {
//       this.timeout("10m");
//       if (network.name !== "hardhat") {
//         this.skip();
//       }
//       this.timeout("1m");

//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);

//       [rudeUser, ...fiveOtherUsers] = otherUsers.slice(0, 6);

//       [deployerLiquity, liquity, rudeLiquity, ...otherLiquities] = await connectUsers([
//         deployer,
//         user,
//         rudeUser,
//         ...fiveOtherUsers
//       ]);

//       await openTroves(fiveOtherUsers, [
//         { depositCollateral: 20, borrowZUSD: 2040 },
//         { depositCollateral: 20, borrowZUSD: 2050 },
//         { depositCollateral: 20, borrowZUSD: 2060 },
//         { depositCollateral: 20, borrowZUSD: 2070 },
//         { depositCollateral: 20, borrowZUSD: 2080 }
//       ]);

//       increaseTime(60 * 60 * 24 * 15);
//     });

//     it("should include enough gas for updating lastFeeOperationTime", async () => {
//       await liquity.openTrove({ depositCollateral: 20, borrowZUSD: 2090 });

//       // We just updated lastFeeOperationTime, so this won't anticipate having to update that
//       // during estimateGas
//       const tx = await liquity.populate.redeemZUSD(1);
//       const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);

//       // Fast-forward 2 minutes.
//       await increaseTime(120);

//       // Required gas has just went up.
//       const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);
//       const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber();
//       expect(gasIncrease).to.be.within(4900, 10000);

//       // This will now have to update lastFeeOperationTime
//       await waitForSuccess(tx.send());

//       // Decay base-rate back to 0
//       await increaseTime(100000000);
//     });

//     it("should include enough gas for one extra traversal", async () => {
//       const troves = await liquity.getTroves({ first: 10, sortedBy: "ascendingCollateralRatio" });

//       const trove = await liquity.getTrove();
//       const newTrove = troveWithICRBetween(troves[3], troves[4]);

//       // First, we want to test a non-borrowing case, to make sure we're not passing due to any
//       // extra gas we add to cover a potential lastFeeOperationTime update
//       const adjustment = trove.adjustTo(newTrove);
//       expect(adjustment.borrowZUSD).to.be.undefined;

//       const tx = await liquity.populate.adjustTrove(adjustment);
//       const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);

//       // A terribly rude user interferes
//       const rudeTrove = newTrove.addDebt(1);
//       const rudeCreation = Trove.recreate(rudeTrove);
//       await openTroves([rudeUser], [rudeCreation]);

//       const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);
//       const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber();

//       await waitForSuccess(tx.send());
//       expect(gasIncrease).to.be.within(10000, 30000);

//       assertDefined(rudeCreation.borrowZUSD);
//       const zusdShortage = rudeTrove.debt.sub(rudeCreation.borrowZUSD);

//       await liquity.sendZUSD(await rudeUser.getAddress(), zusdShortage);
//       await rudeLiquity.closeTrove({ gasPrice: 0 });
//     });

//     it("should include enough gas for both when borrowing", async () => {
//       const troves = await liquity.getTroves({ first: 10, sortedBy: "ascendingCollateralRatio" });

//       const trove = await liquity.getTrove();
//       const newTrove = troveWithICRBetween(troves[1], troves[2]);

//       // Make sure we're borrowing
//       const adjustment = trove.adjustTo(newTrove);
//       expect(adjustment.borrowZUSD).to.not.be.undefined;

//       const tx = await liquity.populate.adjustTrove(adjustment);
//       const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);

//       // A terribly rude user interferes again
//       await openTroves([rudeUser], [Trove.recreate(newTrove.addDebt(1))]);

//       // On top of that, we'll need to update lastFeeOperationTime
//       await increaseTime(120);

//       const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction);
//       const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber();

//       await waitForSuccess(tx.send());
//       expect(gasIncrease).to.be.within(15000, 35000);
//     });
//   });

//   describe("Gas estimation (ZERO issuance)", () => {
//     const estimate = (tx: PopulatedEthersLiquityTransaction) =>
//       provider.estimateGas(tx.rawPopulatedTransaction);

//     before(async function () {
//       if (network.name !== "hardhat") {
//         this.skip();
//       }

//       deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true);
//       [deployerLiquity, liquity] = await connectUsers([deployer, user]);
//     });

//     it("should include enough gas for issuing ZERO", async function () {
//       this.timeout("2m");

//       if (deployment.presaleAddress) {
//         const presale = new ethers.Contract(
//           deployment.presaleAddress,
//           mockBalanceRedirectPresaleAbi,
//           provider
//         );
//         await presale.connect(deployer).closePresale();
//       }

//       await liquity.openTrove({ depositCollateral: 40, borrowZUSD: 4000 });
//       await liquity.depositZUSDInStabilityPool(19);

//       await increaseTime(60);

//       // This will issue ZERO for the first time ever. That uses a whole lotta gas, and we don't
//       // want to pack any extra gas to prepare for this case specifically, because it only happens
//       // once.
//       await liquity.withdrawGainsFromStabilityPool();

//       const claim = await liquity.populate.withdrawGainsFromStabilityPool();
//       const deposit = await liquity.populate.depositZUSDInStabilityPool(1);
//       const withdraw = await liquity.populate.withdrawZUSDFromStabilityPool(1);

//       for (let i = 0; i < 5; ++i) {
//         for (const tx of [claim, deposit, withdraw]) {
//           const gasLimit = tx.rawPopulatedTransaction.gasLimit?.toNumber();
//           const requiredGas = (await estimate(tx)).toNumber();

//           assertDefined(gasLimit);
//           expect(requiredGas).to.be.at.most(gasLimit);
//         }

//         await increaseTime(60);
//       }

//       await waitForSuccess(claim.send());

//       const creation = Trove.recreate(new Trove(Decimal.from(11.1), Decimal.from(2000.1)));

//       await deployerLiquity.openTrove(creation);
//       await deployerLiquity.depositZUSDInStabilityPool(creation.borrowZUSD);
//       await deployerLiquity.setPrice(198);

//       const liquidateTarget = await liquity.populate.liquidate(await deployer.getAddress());
//       const liquidateMultiple = await liquity.populate.liquidateUpTo(40);

//       for (let i = 0; i < 5; ++i) {
//         for (const tx of [liquidateTarget, liquidateMultiple]) {
//           const gasLimit = tx.rawPopulatedTransaction.gasLimit?.toNumber();
//           const requiredGas = (await estimate(tx)).toNumber();

//           assertDefined(gasLimit);
//           expect(requiredGas).to.be.at.most(gasLimit);
//         }

//         await increaseTime(60);
//       }

//       await waitForSuccess(liquidateMultiple.send());
//     });
//   });
// });
