const { assert } = require("chai");
const { Token } = require("@solana/spl-token");
const anchor = require("@project-serum/anchor");
const serumCmn = require("@project-serum/common");
const { Market } = require("@project-serum/serum");
const utf8 = anchor.utils.bytes.utf8;
const { PublicKey, SystemProgram, Keypair, SYSVAR_RENT_PUBKEY } = anchor.web3;
const utils = require("./utils");
const { setupStakePool } = require("./utils/stake");

const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const SWAP_PID = new PublicKey("22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD");
const TOKEN_PID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const REGISTRY_PID = new PublicKey(
  "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv"
);
const LOCKUP_PID = new PublicKey(
  "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
);
const SYSVAR_INSTRUCTIONS_PUBKEY = new PublicKey(
  "Sysvar1nstructions1111111111111111111111111"
);
const FEES = "6160355581";

describe("cfo", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.Cfo;
  // hack so we don't have to update serum-common library
  // to the new AnchorProvider class and Provider interface
  program.provider.send = provider.sendAndConfirm;
  const sweepAuthority = program.provider.wallet.publicKey;
  let officer, srmVault, usdcVault, bVault, stake, treasury;
  let officerBump, srmBump, usdcBump, bBump, stakeBump, treasuryBump;
  let openOrders, openOrdersBump;
  let openOrdersB, openOrdersBumpB;
  let USDC_TOKEN_CLIENT, A_TOKEN_CLIENT, B_TOKEN_CLIENT;
  let officerAccount;
  let marketAClient, marketBClient;
  let marketAuth, marketAuthBump;
  let marketAuthB, marketAuthBumpB;
  let distribution;

  // Accounts used to setup the orderbook.
  let ORDERBOOK_ENV,
    // Accounts used for A -> USDC swap transactions.
    SWAP_A_USDC_ACCOUNTS,
    // Accounts used for  USDC -> A swap transactions.
    SWAP_USDC_A_ACCOUNTS,
    // Serum DEX vault PDA for market A/USDC.
    marketAVaultSigner,
    // Serum DEX vault PDA for market B/USDC.
    marketBVaultSigner;

  let registrar, msrmRegistrar;

  it("BOILERPLATE: Sets up a market with funded fees", async () => {
    ORDERBOOK_ENV = await utils.initMarket({
      provider: program.provider,
    });
    console.log("Token A: ", ORDERBOOK_ENV.marketA.baseMintAddress.toString());
    console.log(
      "Token USDC: ",
      ORDERBOOK_ENV.marketA.quoteMintAddress.toString()
    );
    USDC_TOKEN_CLIENT = new Token(
      program.provider.connection,
      ORDERBOOK_ENV.usdc,
      TOKEN_PID,
      program.provider.wallet.payer
    );
    SRM_TOKEN_CLIENT = new Token(
      program.provider.connection,
      ORDERBOOK_ENV.mintA,
      TOKEN_PID,
      program.provider.wallet.payer
    );
    B_TOKEN_CLIENT = new Token(
      program.provider.connection,
      ORDERBOOK_ENV.mintB,
      TOKEN_PID,
      program.provider.wallet.payer
    );

    await USDC_TOKEN_CLIENT.transfer(
      ORDERBOOK_ENV.godUsdc,
      ORDERBOOK_ENV.marketA._decoded.quoteVault,
      program.provider.wallet.payer,
      [],
      10000000000000
    );

    const tokenAccount = await USDC_TOKEN_CLIENT.getAccountInfo(
      ORDERBOOK_ENV.marketA._decoded.quoteVault
    );
    assert.strictEqual(tokenAccount.amount.toString(), "10000902263700");
  });

  it("BOILERPLATE: Executes trades to generate fees", async () => {
    await utils.runTradeBot(
      ORDERBOOK_ENV.marketA._decoded.ownAddress,
      program.provider,
      1
    );
    marketAClient = await Market.load(
      program.provider.connection,
      ORDERBOOK_ENV.marketA.address,
      { commitment: "processed" },
      DEX_PID
    );
    marketBClient = await Market.load(
      program.provider.connection,
      ORDERBOOK_ENV.marketB.address,
      { commitment: "processed" },
      DEX_PID
    );
    assert.strictEqual(
      marketAClient._decoded.quoteFeesAccrued.toString(),
      FEES
    );
  });

  it("BOILERPLATE: Sets up the staking pools", async () => {
    await setupStakePool(ORDERBOOK_ENV.mintA, ORDERBOOK_ENV.godA);
    registrar = ORDERBOOK_ENV.usdc;
    msrmRegistrar = registrar;
  });

  it("BOILERPLATE: Finds PDA addresses", async () => {
    const [_officer, _officerBump] = await PublicKey.findProgramAddress(
      [DEX_PID.toBuffer()],
      program.programId
    );
    const [_openOrders, _openOrdersBump] = await PublicKey.findProgramAddress(
      [
        utf8.encode("open-orders"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.marketA.address.toBuffer(),
      ],
      program.programId
    );
    const [_openOrdersB, _openOrdersBumpB] = await PublicKey.findProgramAddress(
      [
        utf8.encode("open-orders"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.marketB.address.toBuffer(),
      ],
      program.programId
    );
    const [_srmVault, _srmBump] = await PublicKey.findProgramAddress(
      [
        utf8.encode("token"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.mintA.toBuffer(),
      ],
      program.programId
    );
    const [_bVault, _bBump] = await PublicKey.findProgramAddress(
      [
        utf8.encode("token"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.mintB.toBuffer(),
      ],
      program.programId
    );
    const [_usdcVault, _usdcBump] = await PublicKey.findProgramAddress(
      [
        utf8.encode("token"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.usdc.toBuffer(),
      ],
      program.programId
    );
    const [_stake, _stakeBump] = await PublicKey.findProgramAddress(
      [utf8.encode("stake"), _officer.toBuffer()],
      program.programId
    );
    const [_treasury, _treasuryBump] = await PublicKey.findProgramAddress(
      [utf8.encode("treasury"), _officer.toBuffer()],
      program.programId
    );
    const [_marketAuth, _marketAuthBump] = await PublicKey.findProgramAddress(
      [
        utf8.encode("market-auth"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.marketA.address.toBuffer(),
      ],
      program.programId
    );
    const [_marketAuthB, _marketAuthBumpB] = await PublicKey.findProgramAddress(
      [
        utf8.encode("market-auth"),
        _officer.toBuffer(),
        ORDERBOOK_ENV.marketB.address.toBuffer(),
      ],
      program.programId
    );

    officer = _officer;
    officerBump = _officerBump;
    openOrders = _openOrders;
    openOrdersBump = _openOrdersBump;
    openOrdersB = _openOrdersB;
    openOrdersBumpB = _openOrdersBumpB;
    srmVault = _srmVault;
    srmBump = _srmBump;
    usdcVault = _usdcVault;
    usdcBump = _usdcBump;
    bVault = _bVault;
    bBump = _bBump;
    stake = _stake;
    stakeBump = _stakeBump;
    treasury = _treasury;
    treasuryBump = _treasuryBump;
    marketAuth = _marketAuth;
    marketAuthBump = _marketAuthBump;
    marketAuthB = _marketAuthB;
    marketAuthBumpB = _marketAuthBumpB;
  });

  it("Creates a CFO!", async () => {
    distribution = {
      burn: 80,
      stake: 20,
      treasury: 0,
    };
    const bumps = {
      bump: officerBump,
      srm: srmBump,
      usdc: usdcBump,
      stake: stakeBump,
      treasury: treasuryBump,
    };
    await program.methods
      .createOfficer(bumps, distribution, registrar, msrmRegistrar)
      .accounts({
        officer,
        srmVault,
        usdcVault,
        stake,
        treasury,
        srmMint: ORDERBOOK_ENV.mintA,
        usdcMint: ORDERBOOK_ENV.usdc,
        authority: program.provider.wallet.publicKey,
        dexProgram: DEX_PID,
        swapProgram: SWAP_PID,
        tokenProgram: TOKEN_PID,
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
      })
      .rpc();

    officerAccount = await program.account.officer.fetch(officer);
    assert.isTrue(
      officerAccount.authority.equals(program.provider.wallet.publicKey)
    );
    assert.strictEqual(
      JSON.stringify(officerAccount.distribution),
      JSON.stringify(distribution)
    );
  });

  it("Creates a token account for the officer associated with the market", async () => {
    await program.methods
      .createOfficerToken(bBump)
      .accounts({
        officer,
        token: bVault,
        mint: ORDERBOOK_ENV.mintB,
        payer: program.provider.wallet.publicKey,
        systemProgram: SystemProgram.programId,
        tokenProgram: TOKEN_PID,
        rent: SYSVAR_RENT_PUBKEY,
      })
      .rpc();
    const tokenAccount = await B_TOKEN_CLIENT.getAccountInfo(bVault);
    assert.strictEqual(tokenAccount.state, 1);
    assert.isTrue(tokenAccount.isInitialized);
  });

  it("Creates an open orders account for the officer", async () => {
    await program.methods
      .createOfficerOpenOrders(openOrdersBump)
      .accounts({
        officer,
        openOrders,
        payer: program.provider.wallet.publicKey,
        dexProgram: DEX_PID,
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
        market: ORDERBOOK_ENV.marketA.address,
      })
      .rpc();
    await program.rpc.createOfficerOpenOrders(openOrdersBumpB, {
      accounts: {
        officer,
        openOrders: openOrdersB,
        payer: program.provider.wallet.publicKey,
        dexProgram: DEX_PID,
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
        market: ORDERBOOK_ENV.marketB.address,
      },
    });
  });

  it("Sweeps fees", async () => {
    const [sweepVault, bump] = await PublicKey.findProgramAddress(
      [utf8.encode("token"), officer.toBuffer(), ORDERBOOK_ENV.usdc.toBuffer()],
      program.programId
    );
    const beforeTokenAccount = await serumCmn.getTokenAccount(
      program.provider,
      sweepVault
    );
    await program.methods
      .sweepFees()
      .accounts({
        officer,
        sweepVault,
        mint: ORDERBOOK_ENV.usdc,
        dex: {
          market: ORDERBOOK_ENV.marketA._decoded.ownAddress,
          pcVault: ORDERBOOK_ENV.marketA._decoded.quoteVault,
          sweepAuthority,
          vaultSigner: ORDERBOOK_ENV.marketAVaultSigner,
          dexProgram: DEX_PID,
          tokenProgram: TOKEN_PID,
        },
      })
      .rpc();
    const afterTokenAccount = await serumCmn.getTokenAccount(
      program.provider,
      sweepVault
    );
    assert.strictEqual(
      afterTokenAccount.amount.sub(beforeTokenAccount.amount).toString(),
      FEES
    );
  });

  it("Creates a market auth token", async () => {
    await program.methods
      .authorizeMarket(marketAuthBump)
      .accounts({
        officer,
        authority: program.provider.wallet.publicKey,
        marketAuth,
        payer: program.provider.wallet.publicKey,
        market: ORDERBOOK_ENV.marketA.address,
        systemProgram: SystemProgram.programId,
      })
      .rpc();
    await program.methods
      .authorizeMarket(marketAuthBumpB)
      .accounts({
        officer,
        authority: program.provider.wallet.publicKey,
        marketAuth: marketAuthB,
        payer: program.provider.wallet.publicKey,
        market: ORDERBOOK_ENV.marketB.address,
        systemProgram: SystemProgram.programId,
      })
      .rpc();
  });

  it("Transfers into the mintB vault", async () => {
    await B_TOKEN_CLIENT.transfer(
      ORDERBOOK_ENV.godB,
      bVault,
      program.provider.wallet.payer,
      [],
      616035558100
    );
  });

  it("Swaps from B token to USDC", async () => {
    const bVaultBefore = await B_TOKEN_CLIENT.getAccountInfo(bVault);
    const usdcVaultBefore = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);

    const minExchangeRate = {
      rate: new anchor.BN(0),
      fromDecimals: 6,
      quoteDecimals: 6,
      strict: false,
    };
    await program.methods
      .swapToUsdc(minExchangeRate)
      .accounts({
        officer,
        market: {
          market: marketBClient.address,
          openOrders: openOrdersB,
          requestQueue: marketBClient.decoded.requestQueue,
          eventQueue: marketBClient.decoded.eventQueue,
          bids: marketBClient.decoded.bids,
          asks: marketBClient.decoded.asks,
          orderPayerTokenAccount: bVault,
          coinVault: marketBClient.decoded.baseVault,
          pcVault: marketBClient.decoded.quoteVault,
          vaultSigner: ORDERBOOK_ENV.marketBVaultSigner,
        },
        marketAuth: marketAuthB,
        usdcVault,
        fromVault: bVault,
        usdcMint: ORDERBOOK_ENV.usdc,
        swapProgram: SWAP_PID,
        dexProgram: DEX_PID,
        tokenProgram: TOKEN_PID,
        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
        rent: SYSVAR_RENT_PUBKEY,
      })
      .rpc();

    const bVaultAfter = await B_TOKEN_CLIENT.getAccountInfo(bVault);
    const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);

    assert.strictEqual(bVaultBefore.amount.toNumber(), 616035558100);
    assert.strictEqual(usdcVaultBefore.amount.toNumber(), 6160355581);
    assert.strictEqual(bVaultAfter.amount.toNumber(), 615884458100);
    assert.strictEqual(usdcVaultAfter.amount.toNumber(), 7060634298);
  });

  it("Swaps to SRM", async () => {
    const srmVaultBefore = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
    const usdcVaultBefore = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);

    const minExchangeRate = {
      rate: new anchor.BN(0),
      fromDecimals: 6,
      quoteDecimals: 6,
      strict: false,
    };
    await program.methods
      .swapToSrm(minExchangeRate)
      .accounts({
        officer,
        market: {
          market: marketAClient.address,
          openOrders,
          requestQueue: marketAClient.decoded.requestQueue,
          eventQueue: marketAClient.decoded.eventQueue,
          bids: marketAClient.decoded.bids,
          asks: marketAClient.decoded.asks,
          orderPayerTokenAccount: usdcVault,
          coinVault: marketAClient.decoded.baseVault,
          pcVault: marketAClient.decoded.quoteVault,
          vaultSigner: ORDERBOOK_ENV.marketAVaultSigner,
        },
        marketAuth,
        usdcVault,
        srmVault,
        usdcMint: ORDERBOOK_ENV.usdc,
        srmMint: ORDERBOOK_ENV.mintA,
        swapProgram: SWAP_PID,
        dexProgram: DEX_PID,
        tokenProgram: TOKEN_PID,
        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
        rent: SYSVAR_RENT_PUBKEY,
      })
      .rpc();

    const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
    const usdcVaultAfter = await USDC_TOKEN_CLIENT.getAccountInfo(usdcVault);

    assert.strictEqual(srmVaultBefore.amount.toNumber(), 0);
    assert.strictEqual(srmVaultAfter.amount.toNumber(), 1152000000);
    assert.strictEqual(usdcVaultBefore.amount.toNumber(), 7060634298);
    assert.strictEqual(usdcVaultAfter.amount.toNumber(), 530863);
  });

  it("Distributes the tokens to categories", async () => {
    const srmVaultBefore = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
    const treasuryBefore = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);
    const stakeBefore = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
    const mintInfoBefore = await SRM_TOKEN_CLIENT.getMintInfo();

    await program.methods
      .distribute()
      .accounts({
        officer,
        treasury,
        stake,
        srmVault,
        srmMint: ORDERBOOK_ENV.mintA,
        tokenProgram: TOKEN_PID,
        dexProgram: DEX_PID,
      })
      .rpc();

    const srmVaultAfter = await SRM_TOKEN_CLIENT.getAccountInfo(srmVault);
    const treasuryAfter = await SRM_TOKEN_CLIENT.getAccountInfo(treasury);
    const stakeAfter = await SRM_TOKEN_CLIENT.getAccountInfo(stake);
    const mintInfoAfter = await SRM_TOKEN_CLIENT.getMintInfo();

    const beforeAmount = 1152000000;
    assert.strictEqual(srmVaultBefore.amount.toNumber(), beforeAmount);
    assert.strictEqual(srmVaultAfter.amount.toNumber(), 0); // Fully distributed.
    assert.strictEqual(
      stakeAfter.amount.toNumber(),
      beforeAmount * (distribution.stake / 100.0)
    );
    assert.strictEqual(
      treasuryAfter.amount.toNumber(),
      beforeAmount * (distribution.treasury / 100.0)
    );
    // Check burn amount.
    assert.strictEqual(mintInfoBefore.supply.toString(), "1000000000000000000");
    assert.strictEqual(
      mintInfoBefore.supply.sub(mintInfoAfter.supply).toNumber(),
      beforeAmount * (distribution.burn / 100.0)
    );
  });
});
