import { ethers, waffle } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { getAirSwapOrder } from "../utils/orders";

import { expect } from "chai";
import {
  MockOToken,
  CTokenTreasury,
  LongOTokenWithCToken,
  MockERC20,
  OpynPerpVault,
  MockWETH,
  CTokenProxy,
  MockCErc20,
  MockSwap,
  MockController,
  MockWhitelist,
  MockOpynOracle,
  MockPool,
} from "../../typechain";
import { BigNumber } from "ethers";
import * as fs from "fs";

const mnemonic = fs.existsSync(".secret")
  ? fs.readFileSync(".secret").toString().trim()
  : "test test test test test test test test test test test junk";

enum VaultState {
  Locked,
  Unlocked,
  Emergency,
}

describe("PPN Vault", function () {
  const counterpartyWallet = ethers.Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/30");

  const provider = waffle.provider;

  const ethPrice = 2000 * 1e8;
  const putStrikePrice = 1800 * 1e8;

  // core components
  let proxy: CTokenProxy;
  let vault: OpynPerpVault;
  let action1: CTokenTreasury;
  let action2: LongOTokenWithCToken;

  // asset used by this action: in this case, weth
  let cusdc: MockCErc20;
  let weth: MockWETH;
  let usdc: MockERC20;

  let otoken1: MockOToken;

  let accounts: SignerWithAddress[] = [];

  let owner: SignerWithAddress;
  let depositor1: SignerWithAddress;
  let depositor2: SignerWithAddress;
  let optionSeller: SignerWithAddress;
  let feeRecipient: SignerWithAddress;

  // mock external contracts
  let swap: MockSwap;
  let controller: MockController;
  let whitelist: MockWhitelist;
  let oracle: MockOpynOracle;
  let pool: MockPool;

  this.beforeAll("Set accounts", async () => {
    accounts = await ethers.getSigners();
    const [_owner, _feeRecipient, _depositor1, _depositor2, _seller] = accounts;
    owner = _owner;
    feeRecipient = _feeRecipient;
    depositor1 = _depositor1;
    depositor2 = _depositor2;
    optionSeller = _seller;
  });

  this.beforeAll("Deploy Mock Token contracts", async () => {
    const MockWETHContract = await ethers.getContractFactory("MockWETH");
    weth = (await MockWETHContract.deploy()) as MockWETH;
    await weth.init("WETH", "WETH", 18);

    const ERC20 = await ethers.getContractFactory("MockERC20");
    usdc = (await ERC20.deploy()) as MockERC20;
    await usdc.init("USDC", "USDC", 6);

    // setup cusdc
    const MockCERC20Contract = await ethers.getContractFactory("MockCErc20");
    cusdc = (await MockCERC20Contract.deploy(usdc.address, "compound USDC", "cUSDC", 8)) as MockCErc20;

    await cusdc.setExchangeRate("240000000000000");
    await usdc.mint(cusdc.address, "1000000000000");
  });

  this.beforeAll("Deploy Mock external contracts", async () => {
    const Swap = await ethers.getContractFactory("MockSwap");
    swap = (await Swap.deploy()) as MockSwap;

    const Controller = await ethers.getContractFactory("MockController");
    controller = (await Controller.deploy()) as MockController;

    const Whitelist = await ethers.getContractFactory("MockWhitelist");
    whitelist = (await Whitelist.deploy()) as MockWhitelist;

    const MockPool = await ethers.getContractFactory("MockPool");
    pool = (await MockPool.deploy()) as MockPool;

    const MockOracle = await ethers.getContractFactory("MockOpynOracle");
    oracle = (await MockOracle.deploy()) as MockOpynOracle;

    await controller.setPool(pool.address);
    await controller.setWhitelist(whitelist.address);
    await controller.setOracle(oracle.address);

    await oracle.setAssetPrice(weth.address, ethPrice);

    await usdc.mint(pool.address, "1000000000000");
  });

  this.beforeAll("Mint USDC for participants", async () => {
    await usdc.mint(depositor1.address, 1000000 * 1e6);
    await usdc.mint(depositor2.address, 1000000 * 1e6);
  });

  this.beforeAll("Deploy vault and actions", async () => {
    const VaultContract = await ethers.getContractFactory("OpynPerpVault");
    vault = (await VaultContract.deploy()) as OpynPerpVault;

    const ProxyContract = await ethers.getContractFactory("CTokenProxy");
    proxy = (await ProxyContract.deploy(vault.address, usdc.address, cusdc.address)) as CTokenProxy;

    // deploy 2 mock actions
    const CTokenTreasuryContract = await ethers.getContractFactory("CTokenTreasury");
    action1 = (await CTokenTreasuryContract.deploy(vault.address, cusdc.address)) as CTokenTreasury;

    const LongOToken = await ethers.getContractFactory("LongOTokenWithCToken");
    action2 = (await LongOToken.deploy(
      vault.address,
      cusdc.address,
      usdc.address,
      action1.address, // treasury address
      swap.address,
      controller.address,
      true // put
    )) as LongOTokenWithCToken;
  });

  describe("init", async () => {
    it("should init the contract successfully", async () => {
      await vault
        .connect(owner)
        .init(cusdc.address, owner.address, feeRecipient.address, cusdc.address, 18, "PPN share", "sPPN", [
          action1.address,
          action2.address,
        ]);
      // init state
      expect((await vault.state()) === VaultState.Unlocked).to.be.true;
      expect((await vault.totalAsset()).isZero(), "total asset should be zero").to.be.true;
    });
  });

  describe("Round 0, vault unlocked", async () => {
    const depositAmount = "10000000000"; // 10000 USDC

    it("unlocked state checks", async () => {
      expect(await vault.state()).to.eq(VaultState.Unlocked);
      expect(await vault.round()).to.eq(0);
    });
    it("should be able to deposit cUSDC", async () => {
      await usdc.connect(depositor1).approve(cusdc.address, ethers.constants.MaxUint256);
      await cusdc.connect(depositor1).mint(depositAmount);
      const cusdcBalance = await cusdc.balanceOf(depositor1.address);
      const shares1Before = await vault.balanceOf(depositor1.address);
      const expectedShares = await vault.getSharesByDepositAmount(cusdcBalance);

      // depositor 1 deposits 10000 cUSDC directly
      await cusdc.connect(depositor1).approve(vault.address, ethers.constants.MaxUint256);
      await vault.connect(depositor1).deposit(cusdcBalance);

      const shares1After = await vault.balanceOf(depositor1.address);
      expect(shares1After.sub(shares1Before).eq(expectedShares)).to.be.true;
    });

    it("should be able to deposit USDC through Proxy", async () => {
      await usdc.connect(depositor2).approve(proxy.address, ethers.constants.MaxUint256);
      // depositor 2 deposits 10000 USDC through proxy
      await proxy.connect(depositor2).depositUnderlying(depositAmount);
      const d2Shares = await vault.balanceOf(depositor2.address);
      const d1Shares = await vault.balanceOf(depositor1.address);
      expect(d2Shares.lt(d1Shares)).to.be.true;
    });

    it("should rollover to the first round without committing otoken", async () => {
      const vaultBalanceBefore = await cusdc.balanceOf(vault.address);
      const action1BalanceBefore = await cusdc.balanceOf(action1.address);
      const action2BalanceBefore = await cusdc.balanceOf(action2.address);
      const totalValueBefore = await vault.totalAsset();

      // Distribution:
      // 100% - action1
      // 0% - action2
      await vault.connect(owner).rollOver([10000, 0]);

      const vaultBalanceAfter = await cusdc.balanceOf(vault.address);
      const action1BalanceAfter = await cusdc.balanceOf(action1.address);
      const action2BalanceAfter = await cusdc.balanceOf(action2.address);
      const totalValueAfter = await vault.totalAsset();

      expect(action1BalanceAfter.sub(action1BalanceBefore).eq(vaultBalanceBefore)).to.be.true;
      expect(action2BalanceAfter.sub(action2BalanceBefore).isZero()).to.be.true;

      expect(vaultBalanceAfter.isZero()).to.be.true;
      expect(totalValueAfter.eq(totalValueBefore), "total value should stay unaffected").to.be.true;
    });
  });
  describe("Round 0, vault Locked", async () => {
    it("increase exchange rate over time", async () => {
      const oldExchangeRate = (await cusdc.exchangeRateStored()).toNumber();
      // cusdc value increase by 1%
      await cusdc.setExchangeRate(Math.floor(oldExchangeRate * 1.01));
    });
    it("should be able to close position, once there's interest to collect ", async () => {
      const vaultBalanceBefore = await cusdc.balanceOf(vault.address);
      const action1BalanceBefore = await cusdc.balanceOf(action1.address);

      await vault.connect(owner).closePositions();

      const vaultBalanceAfter = await cusdc.balanceOf(vault.address);
      const action1BalanceAfter = await cusdc.balanceOf(action1.address);
      expect(vaultBalanceAfter.sub(vaultBalanceBefore).eq(action1BalanceBefore.sub(action1BalanceAfter))).to.be.true;

      const profit = await action1.lastRoundProfit();
      expect(profit.gt(0)).to.be.true;
    });
  });

  describe("Round 1: vault Unlocked", async () => {
    it("should be able to commit to an otoken to buy with the interest", async () => {
      const blockNumber = await provider.getBlockNumber();
      const block = await provider.getBlock(blockNumber);
      const currentTimestamp = block.timestamp;
      const expiry = currentTimestamp + 86400 * 7;

      const MockOToken = await ethers.getContractFactory("MockOToken");
      otoken1 = (await MockOToken.deploy()) as MockOToken;
      await otoken1.init("oWETHUSDP", "oWETHUSDP", 18);
      await otoken1.initMockOTokenDetail(weth.address, usdc.address, usdc.address, putStrikePrice, expiry, true);

      await action2.connect(owner).commitOToken(otoken1.address);

      // pass commit period
      const minPeriod = await action2.MIN_COMMIT_PERIOD();
      await provider.send("evm_increaseTime", [minPeriod.toNumber()]); // increase time
      await provider.send("evm_mine", []);
    });
    it("should revert when trying to rollover with incorrect percentage", async () => {
      await expect(vault.connect(owner).rollOver([9850, 150])).to.be.revertedWith("too many cTokens");
    });
    it("should be able to rollover again", async () => {
      const action1BalanceBefore = await cusdc.balanceOf(action1.address);
      const action2BalanceBefore = await cusdc.balanceOf(action2.address);
      const totalValueBefore = await vault.totalAsset();
      // Distribution:
      // 99% - action1
      // 1% - action2
      await vault.connect(owner).rollOver([9900, 100]);

      const action1BalanceAfter = await cusdc.balanceOf(action1.address);
      const action2BalanceAfter = await cusdc.balanceOf(action2.address);

      expect(action1BalanceAfter.sub(action1BalanceBefore).eq(totalValueBefore.mul(99).div(100))).to.be.true;
      expect(action2BalanceAfter.sub(action2BalanceBefore).eq(totalValueBefore.mul(1).div(100))).to.be.true;
    });
  });

  describe("Round 1: vault Locked", async () => {
    it("should be able to buy otoken", async () => {
      const premium = 12 * 1e6; // 12 USD
      const buyAmount = 0.2 * 1e8;
      const order = await getAirSwapOrder(
        action2.address,
        usdc.address,
        premium,
        counterpartyWallet.address,
        otoken1.address,
        buyAmount,
        swap.address,
        counterpartyWallet.privateKey
      );

      const cTokenBefore = await cusdc.balanceOf(action2.address);
      await action2.connect(owner).tradeAirswapOTC(order);

      const cTokenAfter = await cusdc.balanceOf(action2.address);
      expect(cTokenBefore.gt(cTokenAfter)).to.be.true;
    });

    it("increase exchange rate over time", async () => {
      const oldExchangeRate = (await cusdc.exchangeRateStored()).toNumber();
      // cusdc value increase by 1%
      await cusdc.setExchangeRate(Math.floor(oldExchangeRate * 1.01));
    });

    it("should close a round with profit in cusdc", async () => {
      const payout = 100 * 1e6;

      const expiry = (await otoken1.expiryTimestamp()).toNumber();
      await provider.send("evm_setNextBlockTimestamp", [expiry + 60]);
      await provider.send("evm_mine", []);

      const totalAssetBefore = await vault.totalAsset();

      await controller.setRedeemPayout(usdc.address, payout);
      await vault.connect(owner).closePositions();

      const totalAssetAfter = await vault.totalAsset();
      expect(totalAssetAfter.gt(totalAssetBefore)).to.be.true;
    });
  });
});