import BigNumber from 'bignumber.js';

import {
  calcExpectedReserveDataAfterBorrow,
  calcExpectedReserveDataAfterDeposit,
  calcExpectedReserveDataAfterRepay,
  calcExpectedReserveDataAfterStableRateRebalance,
  calcExpectedReserveDataAfterSwapRateMode,
  calcExpectedReserveDataAfterWithdraw,
  calcExpectedUserDataAfterBorrow,
  calcExpectedUserDataAfterDeposit,
  calcExpectedUserDataAfterRepay,
  calcExpectedUserDataAfterSetUseAsCollateral,
  calcExpectedUserDataAfterStableRateRebalance,
  calcExpectedUserDataAfterSwapRateMode,
  calcExpectedUserDataAfterWithdraw,
} from './utils/calculations';
import { getReserveAddressFromSymbol, getReserveData, getUserData } from './utils/helpers';

import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers';
import {
  getAToken,
  getMintableERC20,
  getStableDebtToken,
  getVariableDebtToken,
} from '../../helpers/contracts-getters';
import { MAX_UINT_AMOUNT, ONE_YEAR } from '../../helpers/constants';
import { SignerWithAddress, TestEnv } from './make-suite';
import { DRE, increaseTime, timeLatest, waitForTx } from '../../helpers/misc-utils';

import chai from 'chai';
import { ReserveData, UserReserveData } from './utils/interfaces';
import { ContractReceipt } from 'ethers';
import { AToken } from '../../types/AToken';
import { RateMode, tEthereumAddress } from '../../helpers/types';

const { expect } = chai;

const almostEqualOrEqual = function (
  this: any,
  expected: ReserveData | UserReserveData,
  actual: ReserveData | UserReserveData
) {
  const keys = Object.keys(actual);

  keys.forEach((key) => {
    if (
      key === 'lastUpdateTimestamp' ||
      key === 'marketStableRate' ||
      key === 'symbol' ||
      key === 'aTokenAddress' ||
      key === 'decimals' ||
      key === 'totalStableDebtLastUpdated'
    ) {
      // skipping consistency check on accessory data
      return;
    }

    this.assert(actual[key] != undefined, `Property ${key} is undefined in the actual data`);
    expect(expected[key] != undefined, `Property ${key} is undefined in the expected data`);

    if (expected[key] == null || actual[key] == null) {
      console.log('Found a undefined value for Key ', key, ' value ', expected[key], actual[key]);
    }

    if (actual[key] instanceof BigNumber) {
      const actualValue = (<BigNumber>actual[key]).decimalPlaces(0, BigNumber.ROUND_DOWN);
      const expectedValue = (<BigNumber>expected[key]).decimalPlaces(0, BigNumber.ROUND_DOWN);

      this.assert(
        actualValue.eq(expectedValue) ||
          actualValue.plus(1).eq(expectedValue) ||
          actualValue.eq(expectedValue.plus(1)) ||
          actualValue.plus(2).eq(expectedValue) ||
          actualValue.eq(expectedValue.plus(2)) ||
          actualValue.plus(3).eq(expectedValue) ||
          actualValue.eq(expectedValue.plus(3)),
        `expected #{act} to be almost equal or equal #{exp} for property ${key}`,
        `expected #{act} to be almost equal or equal #{exp} for property ${key}`,
        expectedValue.toFixed(0),
        actualValue.toFixed(0)
      );
    } else {
      this.assert(
        actual[key] !== null &&
          expected[key] !== null &&
          actual[key].toString() === expected[key].toString(),
        `expected #{act} to be equal #{exp} for property ${key}`,
        `expected #{act} to be equal #{exp} for property ${key}`,
        expected[key],
        actual[key]
      );
    }
  });
};

chai.use(function (chai: any, utils: any) {
  chai.Assertion.overwriteMethod('almostEqualOrEqual', function (original: any) {
    return function (this: any, expected: ReserveData | UserReserveData) {
      const actual = (expected as ReserveData)
        ? <ReserveData>this._obj
        : <UserReserveData>this._obj;

      almostEqualOrEqual.apply(this, [expected, actual]);
    };
  });
});

interface ActionsConfig {
  skipIntegrityCheck: boolean;
}

export const configuration: ActionsConfig = <ActionsConfig>{};

export const mint = async (reserveSymbol: string, amount: string, user: SignerWithAddress) => {
  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const token = await getMintableERC20(reserve);

  await waitForTx(
    await token.connect(user.signer).mint(await convertToCurrencyDecimals(reserve, amount))
  );
};

export const approve = async (reserveSymbol: string, user: SignerWithAddress, testEnv: TestEnv) => {
  const { pool } = testEnv;
  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const token = await getMintableERC20(reserve);

  await token.connect(user.signer).approve(pool.address, '100000000000000000000000000000');
};

export const deposit = async (
  reserveSymbol: string,
  amount: string,
  sender: SignerWithAddress,
  onBehalfOf: tEthereumAddress,
  sendValue: string,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const amountToDeposit = await convertToCurrencyDecimals(reserve, amount);

  const txOptions: any = {};

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    onBehalfOf,
    testEnv,
    sender.address
  );

  if (sendValue) {
    txOptions.value = await convertToCurrencyDecimals(reserve, sendValue);
  }
  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool
        .connect(sender.signer)
        .deposit(reserve, amountToDeposit, onBehalfOf, '0', txOptions)
    );

    const {
      reserveData: reserveDataAfter,
      userData: userDataAfter,
      timestamp,
    } = await getContractsData(reserve, onBehalfOf, testEnv, sender.address);

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    const expectedReserveData = calcExpectedReserveDataAfterDeposit(
      amountToDeposit.toString(),
      reserveDataBefore,
      txTimestamp
    );

    const expectedUserReserveData = calcExpectedUserDataAfterDeposit(
      amountToDeposit.toString(),
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      txTimestamp,
      timestamp,
      txCost
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserReserveData);

    // truffleAssert.eventEmitted(txResult, "Deposit", (ev: any) => {
    //   const {_reserve, _user, _amount} = ev;
    //   return (
    //     _reserve === reserve &&
    //     _user === user &&
    //     new BigNumber(_amount).isEqualTo(new BigNumber(amountToDeposit))
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(
      pool.connect(sender.signer).deposit(reserve, amountToDeposit, onBehalfOf, '0', txOptions),
      revertMessage
    ).to.be.reverted;
  }
};

export const withdraw = async (
  reserveSymbol: string,
  amount: string,
  user: SignerWithAddress,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const {
    aTokenInstance,
    reserve,
    userData: userDataBefore,
    reserveData: reserveDataBefore,
  } = await getDataBeforeAction(reserveSymbol, user.address, testEnv);

  let amountToWithdraw = '0';

  if (amount !== '-1') {
    amountToWithdraw = (await convertToCurrencyDecimals(reserve, amount)).toString();
  } else {
    amountToWithdraw = MAX_UINT_AMOUNT;
  }

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool.connect(user.signer).withdraw(reserve, amountToWithdraw, user.address)
    );

    const {
      reserveData: reserveDataAfter,
      userData: userDataAfter,
      timestamp,
    } = await getContractsData(reserve, user.address, testEnv);

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    const expectedReserveData = calcExpectedReserveDataAfterWithdraw(
      amountToWithdraw,
      reserveDataBefore,
      userDataBefore,
      txTimestamp
    );

    const expectedUserData = calcExpectedUserDataAfterWithdraw(
      amountToWithdraw,
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      txTimestamp,
      timestamp,
      txCost
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserData);

    // truffleAssert.eventEmitted(txResult, "Redeem", (ev: any) => {
    //   const {_from, _value} = ev;
    //   return (
    //     _from === user && new BigNumber(_value).isEqualTo(actualAmountRedeemed)
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(
      pool.connect(user.signer).withdraw(reserve, amountToWithdraw, user.address),
      revertMessage
    ).to.be.reverted;
  }
};

export const delegateBorrowAllowance = async (
  reserve: string,
  amount: string,
  interestRateMode: string,
  user: SignerWithAddress,
  receiver: tEthereumAddress,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserveAddress: tEthereumAddress = await getReserveAddressFromSymbol(reserve);

  const amountToDelegate: string = await (
    await convertToCurrencyDecimals(reserveAddress, amount)
  ).toString();

  const reserveData = await pool.getReserveData(reserveAddress);

  const debtToken =
    interestRateMode === '1'
      ? await getStableDebtToken(reserveData.stableDebtTokenAddress)
      : await getVariableDebtToken(reserveData.variableDebtTokenAddress);

  const delegateAllowancePromise = debtToken
    .connect(user.signer)
    .approveDelegation(receiver, amountToDelegate);

  if (expectedResult === 'revert' && revertMessage) {
    await expect(delegateAllowancePromise, revertMessage).to.be.revertedWith(revertMessage);
    return;
  } else {
    await delegateAllowancePromise;
    const allowance = await debtToken.borrowAllowance(user.address, receiver);
    expect(allowance.toString()).to.be.equal(
      amountToDelegate,
      'borrowAllowance is set incorrectly'
    );
  }
};

export const borrow = async (
  reserveSymbol: string,
  amount: string,
  interestRateMode: string,
  user: SignerWithAddress,
  onBehalfOf: tEthereumAddress,
  timeTravel: string,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    onBehalfOf,
    testEnv,
    user.address
  );

  const amountToBorrow = await convertToCurrencyDecimals(reserve, amount);

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool
        .connect(user.signer)
        .borrow(reserve, amountToBorrow, interestRateMode, '0', onBehalfOf)
    );

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    if (timeTravel) {
      const secondsToTravel = new BigNumber(timeTravel).multipliedBy(ONE_YEAR).div(365).toNumber();

      await increaseTime(secondsToTravel);
    }

    const {
      reserveData: reserveDataAfter,
      userData: userDataAfter,
      timestamp,
    } = await getContractsData(reserve, onBehalfOf, testEnv, user.address);

    const expectedReserveData = calcExpectedReserveDataAfterBorrow(
      amountToBorrow.toString(),
      interestRateMode,
      reserveDataBefore,
      userDataBefore,
      txTimestamp,
      timestamp
    );

    const expectedUserData = calcExpectedUserDataAfterBorrow(
      amountToBorrow.toString(),
      interestRateMode,
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      txTimestamp,
      timestamp
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserData);

    // truffleAssert.eventEmitted(txResult, "Borrow", (ev: any) => {
    //   const {
    //     _reserve,
    //     _user,
    //     _amount,
    //     _borrowRateMode,
    //     _borrowRate,
    //     _originationFee,
    //   } = ev;
    //   return (
    //     _reserve.toLowerCase() === reserve.toLowerCase() &&
    //     _user.toLowerCase() === user.toLowerCase() &&
    //     new BigNumber(_amount).eq(amountToBorrow) &&
    //     new BigNumber(_borrowRateMode).eq(expectedUserData.borrowRateMode) &&
    //     new BigNumber(_borrowRate).eq(expectedUserData.borrowRate) &&
    //     new BigNumber(_originationFee).eq(
    //       expectedUserData.originationFee.minus(userDataBefore.originationFee)
    //     )
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(
      pool.connect(user.signer).borrow(reserve, amountToBorrow, interestRateMode, '0', onBehalfOf),
      revertMessage
    ).to.be.reverted;
  }
};

export const repay = async (
  reserveSymbol: string,
  amount: string,
  rateMode: string,
  user: SignerWithAddress,
  onBehalfOf: SignerWithAddress,
  sendValue: string,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;
  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    onBehalfOf.address,
    testEnv
  );

  let amountToRepay = '0';

  if (amount !== '-1') {
    amountToRepay = (await convertToCurrencyDecimals(reserve, amount)).toString();
  } else {
    amountToRepay = MAX_UINT_AMOUNT;
  }
  amountToRepay = '0x' + new BigNumber(amountToRepay).toString(16);

  const txOptions: any = {};

  if (sendValue) {
    const valueToSend = await convertToCurrencyDecimals(reserve, sendValue);
    txOptions.value = '0x' + new BigNumber(valueToSend.toString()).toString(16);
  }

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool
        .connect(user.signer)
        .repay(reserve, amountToRepay, rateMode, onBehalfOf.address, txOptions)
    );

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    const {
      reserveData: reserveDataAfter,
      userData: userDataAfter,
      timestamp,
    } = await getContractsData(reserve, onBehalfOf.address, testEnv);

    const expectedReserveData = calcExpectedReserveDataAfterRepay(
      amountToRepay,
      <RateMode>rateMode,
      reserveDataBefore,
      userDataBefore,
      txTimestamp,
      timestamp
    );

    const expectedUserData = calcExpectedUserDataAfterRepay(
      amountToRepay,
      <RateMode>rateMode,
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      user.address,
      onBehalfOf.address,
      txTimestamp,
      timestamp
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserData);

    // truffleAssert.eventEmitted(txResult, "Repay", (ev: any) => {
    //   const {_reserve, _user, _repayer} = ev;

    //   return (
    //     _reserve.toLowerCase() === reserve.toLowerCase() &&
    //     _user.toLowerCase() === onBehalfOf.toLowerCase() &&
    //     _repayer.toLowerCase() === user.toLowerCase()
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(
      pool
        .connect(user.signer)
        .repay(reserve, amountToRepay, rateMode, onBehalfOf.address, txOptions),
      revertMessage
    ).to.be.reverted;
  }
};

export const setUseAsCollateral = async (
  reserveSymbol: string,
  user: SignerWithAddress,
  useAsCollateral: string,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    user.address,
    testEnv
  );

  const useAsCollateralBool = useAsCollateral.toLowerCase() === 'true';

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool.connect(user.signer).setUserUseReserveAsCollateral(reserve, useAsCollateralBool)
    );

    const { txCost } = await getTxCostAndTimestamp(txResult);

    const { userData: userDataAfter } = await getContractsData(reserve, user.address, testEnv);

    const expectedUserData = calcExpectedUserDataAfterSetUseAsCollateral(
      useAsCollateral.toLocaleLowerCase() === 'true',
      reserveDataBefore,
      userDataBefore,
      txCost
    );

    expectEqual(userDataAfter, expectedUserData);
    // if (useAsCollateralBool) {
    //   truffleAssert.eventEmitted(txResult, 'ReserveUsedAsCollateralEnabled', (ev: any) => {
    //     const {_reserve, _user} = ev;
    //     return _reserve === reserve && _user === user;
    //   });
    // } else {
    //   truffleAssert.eventEmitted(txResult, 'ReserveUsedAsCollateralDisabled', (ev: any) => {
    //     const {_reserve, _user} = ev;
    //     return _reserve === reserve && _user === user;
    //   });
    // }
  } else if (expectedResult === 'revert') {
    await expect(
      pool.connect(user.signer).setUserUseReserveAsCollateral(reserve, useAsCollateralBool),
      revertMessage
    ).to.be.reverted;
  }
};

export const swapBorrowRateMode = async (
  reserveSymbol: string,
  user: SignerWithAddress,
  rateMode: string,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    user.address,
    testEnv
  );

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool.connect(user.signer).swapBorrowRateMode(reserve, rateMode)
    );

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    const { reserveData: reserveDataAfter, userData: userDataAfter } = await getContractsData(
      reserve,
      user.address,
      testEnv
    );

    const expectedReserveData = calcExpectedReserveDataAfterSwapRateMode(
      reserveDataBefore,
      userDataBefore,
      rateMode,
      txTimestamp
    );

    const expectedUserData = calcExpectedUserDataAfterSwapRateMode(
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      rateMode,
      txCost,
      txTimestamp
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserData);

    // truffleAssert.eventEmitted(txResult, "Swap", (ev: any) => {
    //   const {_user, _reserve, _newRateMode, _newRate} = ev;
    //   return (
    //     _user === user &&
    //     _reserve == reserve &&
    //     new BigNumber(_newRateMode).eq(expectedUserData.borrowRateMode) &&
    //     new BigNumber(_newRate).eq(expectedUserData.borrowRate)
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(pool.connect(user.signer).swapBorrowRateMode(reserve, rateMode), revertMessage).to
      .be.reverted;
  }
};

export const rebalanceStableBorrowRate = async (
  reserveSymbol: string,
  user: SignerWithAddress,
  target: SignerWithAddress,
  expectedResult: string,
  testEnv: TestEnv,
  revertMessage?: string
) => {
  const { pool } = testEnv;

  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData(
    reserve,
    target.address,
    testEnv
  );

  if (expectedResult === 'success') {
    const txResult = await waitForTx(
      await pool.connect(user.signer).rebalanceStableBorrowRate(reserve, target.address)
    );

    const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult);

    const { reserveData: reserveDataAfter, userData: userDataAfter } = await getContractsData(
      reserve,
      target.address,
      testEnv
    );

    const expectedReserveData = calcExpectedReserveDataAfterStableRateRebalance(
      reserveDataBefore,
      userDataBefore,
      txTimestamp
    );

    const expectedUserData = calcExpectedUserDataAfterStableRateRebalance(
      reserveDataBefore,
      expectedReserveData,
      userDataBefore,
      txCost,
      txTimestamp
    );

    expectEqual(reserveDataAfter, expectedReserveData);
    expectEqual(userDataAfter, expectedUserData);

    // truffleAssert.eventEmitted(txResult, 'RebalanceStableBorrowRate', (ev: any) => {
    //   const {_user, _reserve, _newStableRate} = ev;
    //   return (
    //     _user.toLowerCase() === target.toLowerCase() &&
    //     _reserve.toLowerCase() === reserve.toLowerCase() &&
    //     new BigNumber(_newStableRate).eq(expectedUserData.borrowRate)
    //   );
    // });
  } else if (expectedResult === 'revert') {
    await expect(
      pool.connect(user.signer).rebalanceStableBorrowRate(reserve, target.address),
      revertMessage
    ).to.be.reverted;
  }
};

const expectEqual = (
  actual: UserReserveData | ReserveData,
  expected: UserReserveData | ReserveData
) => {
  if (!configuration.skipIntegrityCheck) {
    // @ts-ignore
    expect(actual).to.be.almostEqualOrEqual(expected);
  }
};

interface ActionData {
  reserve: string;
  reserveData: ReserveData;
  userData: UserReserveData;
  aTokenInstance: AToken;
}

const getDataBeforeAction = async (
  reserveSymbol: string,
  user: tEthereumAddress,
  testEnv: TestEnv
): Promise<ActionData> => {
  const reserve = await getReserveAddressFromSymbol(reserveSymbol);

  const { reserveData, userData } = await getContractsData(reserve, user, testEnv);
  const aTokenInstance = await getAToken(reserveData.aTokenAddress);
  return {
    reserve,
    reserveData,
    userData,
    aTokenInstance,
  };
};

export const getTxCostAndTimestamp = async (tx: ContractReceipt) => {
  if (!tx.blockNumber || !tx.transactionHash || !tx.cumulativeGasUsed) {
    throw new Error('No tx blocknumber');
  }
  const txTimestamp = new BigNumber((await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp);

  const txInfo = await DRE.ethers.provider.getTransaction(tx.transactionHash);
  const txCost = new BigNumber(tx.cumulativeGasUsed.toString()).multipliedBy(
    txInfo.gasPrice.toString()
  );

  return { txCost, txTimestamp };
};

export const getContractsData = async (
  reserve: string,
  user: string,
  testEnv: TestEnv,
  sender?: string
) => {
  const { pool, helpersContract } = testEnv;

  const [userData, reserveData, timestamp] = await Promise.all([
    getUserData(pool, helpersContract, reserve, user, sender || user),
    getReserveData(helpersContract, reserve),
    timeLatest(),
  ]);

  return {
    reserveData,
    userData,
    timestamp: new BigNumber(timestamp),
  };
};