import * as anchor from "@project-serum/anchor";
import assert from "assert";
import { Token, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import {
  AccountMeta,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";
import {
  feeAmountPerContract,
  FEE_OWNER_KEY,
  NFT_MINT_LAMPORTS,
} from "../packages/psyoptions-ts/src/fees";
import {
  createExerciser,
  createMinter,
  exerciseOptionTx,
  initNewTokenAccount,
  initNewTokenMint,
  initOptionMarket,
  initSetup,
  wait,
} from "../utils/helpers";
import {
  OptionMarketWithKey,
  instructions as psyAmericanInstructions,
  parseTransactionError,
} from "@mithraic-labs/psy-american";
import {
  AnchorError,
  AnchorProvider,
  BN,
  Program,
  Wallet,
} from "@project-serum/anchor";
import { PsyAmerican } from "../target/types/psy_american";

describe("exerciseOption", () => {
  const payer = anchor.web3.Keypair.generate();
  const mintAuthority = anchor.web3.Keypair.generate();
  const program = anchor.workspace.PsyAmerican as anchor.Program<PsyAmerican>;
  const provider = program.provider;
  // @ts-ignore
  const wallet = provider.wallet as unknown as anchor.Wallet;

  const minter = anchor.web3.Keypair.generate();
  const minterProvider = new AnchorProvider(
    provider.connection,
    new Wallet(minter),
    {}
  );
  const minterProgram = new Program(
    program.idl,
    program.programId,
    minterProvider
  );
  const exerciser = anchor.web3.Keypair.generate();
  const exerciserProvider = new AnchorProvider(
    provider.connection,
    new Wallet(exerciser),
    {}
  );
  const exerciserProgram = new Program(
    program.idl,
    program.programId,
    exerciserProvider
  );

  let quoteToken: Token;
  let underlyingToken: Token;
  let optionToken: Token;
  let underlyingAmountPerContract: anchor.BN;
  let quoteAmountPerContract: anchor.BN;
  let optionMarketKey: PublicKey;
  let optionMarket: OptionMarketWithKey;
  let exerciseFeeKey: PublicKey;
  let exerciserOptionAcct: Keypair;
  let exerciserQuoteAcct: Keypair;
  let exerciserUnderlyingAcct: Keypair;
  let remainingAccounts: AccountMeta[] = [];
  let instructions: TransactionInstruction[] = [];

  let size = new u64(2);

  before(async () => {
    // airdrop SOL to the payer and minter
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        payer.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        minter.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(
        exerciser.publicKey,
        100 * LAMPORTS_PER_SOL
      ),
      "confirmed"
    );
  });

  describe("Non-nft OptionMarket", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        underlyingToken.publicKey
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });

    it("should be properly setup", async () => {
      const exerciserOption = await optionToken.getAccountInfo(
        exerciserOptionAcct.publicKey
      );
      assert.equal(exerciserOption.amount.toString(), new u64(100).toString());
    });

    describe("proper exercise", () => {
      it("should burn the option token, swap the quote and underlying assets", async () => {
        const optionTokenBefore = await optionToken.getMintInfo();
        const underlyingPoolBefore = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const quotePoolBefore = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const exerciserQuoteBefore = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              optionMarket,
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
        } catch (err) {
          console.error((err as AnchorError).error.errorMessage);
          throw err;
        }
        const optionTokenAfter = await optionToken.getMintInfo();
        const optionTokenDiff = optionTokenAfter.supply.sub(
          optionTokenBefore.supply
        );
        assert.equal(optionTokenDiff.toString(), size.neg().toString());

        const underlyingPoolAfter = await underlyingToken.getAccountInfo(
          optionMarket.underlyingAssetPool
        );
        const underlyingPoolDiff = underlyingPoolAfter.amount.sub(
          underlyingPoolBefore.amount
        );
        assert.equal(
          underlyingPoolDiff.toString(),
          size.mul(underlyingAmountPerContract).neg().toString()
        );

        const quotePoolAfter = await quoteToken.getAccountInfo(
          optionMarket.quoteAssetPool
        );
        const quotePoolDiff = quotePoolAfter.amount.sub(quotePoolBefore.amount);
        assert.equal(
          quotePoolDiff.toString(),
          size.mul(quoteAmountPerContract).toString()
        );

        const exerciserQuoteAfter = await quoteToken.getAccountInfo(
          exerciserQuoteAcct.publicKey
        );
        const exerciserQuoteDiff = exerciserQuoteAfter.amount.sub(
          exerciserQuoteBefore.amount
        );
        const exerciseFee = new BN(0);
        assert.equal(
          exerciserQuoteDiff.neg().toString(),
          exerciseFee.add(size.mul(quoteAmountPerContract)).toString()
        );
      });
    });
    describe("quote asset pool is not the same as the OptionMarket", () => {
      let badQuoteAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badQuoteAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              {
                ...optionMarket,
                quoteAssetPool: badQuoteAssetPoolAcct.publicKey,
              },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          console.log("*** programError", programError);
          const errMsg =
            "Quote pool account does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("Underlying asset pool is not the same as the OptionMarket", () => {
      let badUnderlyingAssetPoolAcct: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          underlyingToken.publicKey,
          payer
        );
        badUnderlyingAssetPoolAcct = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              {
                ...optionMarket,
                underlyingAssetPool: badUnderlyingAssetPoolAcct.publicKey,
              },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "Underlying pool account does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("Underlying destination mint is not the same as the underlying asset", () => {
      let badUnderlyingDest: Keypair;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { tokenAccount } = await initNewTokenAccount(
          provider.connection,
          FEE_OWNER_KEY,
          quoteToken.publicKey,
          payer
        );
        badUnderlyingDest = tokenAccount;
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              optionMarket,
              exerciserOptionAcct.publicKey,
              badUnderlyingDest.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "Underlying destination mint must match underlying asset mint address";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
    describe("OptionToken Mint is not the same as the OptionMarket", () => {
      let badOptionToken: Token;
      beforeEach(async () => {
        // Create a new token account and set it as the mintFeeKey
        const { mintAccount } = await initNewTokenMint(
          provider.connection,
          FEE_OWNER_KEY,
          payer
        );
        badOptionToken = new Token(
          provider.connection,
          mintAccount.publicKey,
          TOKEN_PROGRAM_ID,
          payer
        );
      });
      it("should error", async () => {
        try {
          const instruction =
            psyAmericanInstructions.exerciseOptionsV2Instruction(
              exerciserProgram,
              size,
              { ...optionMarket, optionMint: badOptionToken.publicKey },
              exerciserOptionAcct.publicKey,
              exerciserUnderlyingAcct.publicKey,
              exerciserQuoteAcct.publicKey
            );
          await exerciserProgram.provider.sendAndConfirm!(
            new Transaction().add(instruction)
          );
          assert.ok(false);
        } catch (err) {
          const programError = parseTransactionError(err);
          const errMsg =
            "OptionToken mint does not match the value on the OptionMarket";
          assert.equal(programError.msg, errMsg);
        }
      });
    });
  });

  describe("Expired option market", () => {
    before(async () => {
      // Initialize a new OptionMarket
      ({
        quoteToken,
        underlyingToken,
        optionToken,
        underlyingAmountPerContract,
        quoteAmountPerContract,
        optionMarketKey,
        exerciseFeeKey,
        optionMarket,
        remainingAccounts,
        instructions,
      } = await initSetup(provider, payer, mintAuthority, program, {
        // set expiration to 2 seconds from now
        expiration: new anchor.BN(new Date().getTime() / 1000 + 4),
      }));
      await initOptionMarket(
        program,
        payer,
        optionMarket,
        remainingAccounts,
        instructions
      );
      // Create a new minter
      const {
        optionAccount: minterOptionAcct,
        underlyingAccount: minterUnderlyingAccount,
        writerTokenAccount: minterWriterAcct,
      } = await createMinter(
        provider.connection,
        minter,
        mintAuthority,
        underlyingToken,
        new anchor.BN(100)
          .mul(optionMarket.underlyingAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.writerTokenMint,
        quoteToken
      );
      // Mint a bunch of contracts to the minter
      const { ix: mintOptionsIx } =
        await psyAmericanInstructions.mintOptionV2Instruction(
          minterProgram,
          minterOptionAcct.publicKey,
          minterWriterAcct.publicKey,
          minterUnderlyingAccount.publicKey,
          new anchor.BN(100),
          optionMarket
        );
      await program.provider.sendAndConfirm!(
        new Transaction().add(mintOptionsIx),
        [minter]
      );
      // Create an exerciser
      ({
        optionAccount: exerciserOptionAcct,
        quoteAccount: exerciserQuoteAcct,
        underlyingAccount: exerciserUnderlyingAcct,
      } = await createExerciser(
        provider.connection,
        exerciser,
        mintAuthority,
        quoteToken,
        new anchor.BN(100)
          .mul(optionMarket.quoteAmountPerContract)
          .muln(2)
          .toNumber(),
        optionMarket.optionMint,
        optionMarket.underlyingAssetMint
      ));

      // Transfer a options to the exerciser
      await optionToken.transfer(
        minterOptionAcct.publicKey,
        exerciserOptionAcct.publicKey,
        minter,
        [],
        new u64(100)
      );
    });
    beforeEach(async () => {
      size = new u64(2);
    });
    it("should error", async () => {
      try {
        await wait(3000);
        const instruction =
          psyAmericanInstructions.exerciseOptionsV2Instruction(
            exerciserProgram,
            size,
            optionMarket,
            exerciserOptionAcct.publicKey,
            exerciserUnderlyingAcct.publicKey,
            exerciserQuoteAcct.publicKey
          );
        await exerciserProgram.provider.sendAndConfirm!(
          new Transaction().add(instruction)
        );
        assert.ok(false);
      } catch (err) {
        const programError = parseTransactionError(err);
        const errMsg = "OptionMarket is expired, can't exercise";
        assert.equal(programError.msg, errMsg);
      }
    });
  });
});