import type { BigintIsh } from "@saberhq/token-utils";
import { Percent, Token as SToken, TokenAmount } from "@saberhq/token-utils";
import { PublicKey } from "@solana/web3.js";
import { BN } from "bn.js";
import { mapValues } from "lodash";

import { SWAP_PROGRAM_ID } from "../constants.js";
import type { IExchangeInfo } from "../entities/exchange.js";
import { RECOMMENDED_FEES, ZERO_FEES } from "../state/fees.js";
import {
  calculateEstimatedMintAmount,
  calculateEstimatedSwapOutputAmount,
  calculateEstimatedWithdrawAmount,
  calculateEstimatedWithdrawOneAmount,
  calculateVirtualPrice,
} from "./amounts.js";

const exchange = {
  swapAccount: new PublicKey("YAkoNb6HKmSxQN9L8hiBE5tPJRsniSSMzND1boHmZxe"),
  programID: SWAP_PROGRAM_ID,
  lpToken: new SToken({
    symbol: "LP",
    name: "StableSwap LP",
    address: "2poo1w1DL6yd2WNTCnNTzDqkC6MBXq7axo77P16yrBuf",
    decimals: 6,
    chainId: 100,
  }),
  tokens: [
    new SToken({
      symbol: "TOKA",
      name: "Token A",
      address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      decimals: 6,
      chainId: 100,
    }),
    new SToken({
      symbol: "TOKB",
      name: "Token B",
      address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
      decimals: 6,
      chainId: 100,
    }),
  ],
} as const;

const makeExchangeInfo = (
  {
    lpTotalSupply = BigInt(200_000_000),
    tokenAAmount = BigInt(100_000_000),
    tokenBAmount = BigInt(100_000_000),
  }: {
    lpTotalSupply?: bigint;
    tokenAAmount?: bigint;
    tokenBAmount?: bigint;
  } = {
    lpTotalSupply: BigInt(200_000_000),
    tokenAAmount: BigInt(100_000_000),
    tokenBAmount: BigInt(100_000_000),
  },
): IExchangeInfo => ({
  ampFactor: BigInt(100),
  fees: ZERO_FEES,
  lpTotalSupply: new TokenAmount(exchange.lpToken, lpTotalSupply),
  reserves: [
    {
      reserveAccount: new PublicKey(
        "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
      ),
      adminFeeAccount: new PublicKey(
        "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
      ),
      amount: new TokenAmount(exchange.tokens[0], tokenAAmount),
    },
    {
      reserveAccount: new PublicKey(
        "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
      ),
      adminFeeAccount: new PublicKey(
        "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
      ),
      amount: new TokenAmount(exchange.tokens[1], tokenBAmount),
    },
  ],
});

const exchangeInfo = makeExchangeInfo();

const exchangeInfoWithFees = {
  ...exchangeInfo,
  fees: RECOMMENDED_FEES,
} as const;

const assertTokenAmounts = (actual: TokenAmount, expected: TokenAmount) => {
  expect(actual.equalTo(expected) && actual.token.equals(expected.token)).toBe(
    true,
  );
};

const assertTokenAmount = (actual: TokenAmount, expected: BigintIsh) => {
  expect(actual.raw.toString()).toEqual(expected.toString());
};

describe("Calculated amounts", () => {
  describe("#calculateVirtualPrice", () => {
    it("works", () => {
      const result = calculateVirtualPrice(exchangeInfo);
      expect(result?.toFixed(4)).toBe("1.0000");
    });

    it("is symmetric", () => {
      const result = calculateVirtualPrice(
        makeExchangeInfo({
          lpTotalSupply: BigInt("200000000"),
          tokenAAmount: BigInt("10000000"),
          tokenBAmount: BigInt("190000000"),
        }),
      );
      expect(result?.toFixed(4)).toBe("0.9801");

      const result2 = calculateVirtualPrice(
        makeExchangeInfo({
          lpTotalSupply: BigInt(200_000_000),
          tokenAAmount: BigInt(190_000_000),
          tokenBAmount: BigInt(10_000_000),
        }),
      );
      expect(result2?.toFixed(4)).toBe("0.9801");
    });

    it("can quote both prices", () => {
      const exchange = makeExchangeInfo({
        lpTotalSupply: BigInt(200_000_000),
        tokenAAmount: BigInt(10_000_000),
        tokenBAmount: BigInt(190_000_000),
      });

      const result = calculateVirtualPrice(exchange);
      expect(result?.toFixed(4)).toBe("0.9801");
    });
  });

  describe("#calculateEstimatedSwapOutputAmount", () => {
    it("no fees", () => {
      const result = calculateEstimatedSwapOutputAmount(
        exchangeInfo,
        new TokenAmount(exchange.tokens[0], BigInt(10_000_000)),
      );

      assertTokenAmounts(result.outputAmount, result.outputAmountBeforeFees);
    });

    it("fees are different", () => {
      const result = calculateEstimatedSwapOutputAmount(
        {
          ...exchangeInfoWithFees,
          fees: {
            ...exchangeInfoWithFees.fees,
            trade: new Percent(50, 100),
          },
        },
        new TokenAmount(exchange.tokens[0], BigInt(100)),
      );

      // 50 percent fee
      assertTokenAmount(result.outputAmountBeforeFees, BigInt(100));
      assertTokenAmount(result.outputAmount, BigInt(50));
    });
  });

  describe("#calculateEstimatedMintAmount", () => {
    it("no fees if equal liquidity provision", () => {
      const result = calculateEstimatedMintAmount(
        {
          ...exchangeInfo,
          fees: {
            ...ZERO_FEES,
            trade: new Percent(50, 100),
          },
        },
        BigInt(100),
        BigInt(100),
      );

      assertTokenAmounts(result.mintAmount, result.mintAmountBeforeFees);
    });

    it("fees if unequal liquidity provision", () => {
      const result = calculateEstimatedMintAmount(
        {
          ...exchangeInfo,
          fees: {
            ...ZERO_FEES,
            trade: new Percent(50, 100),
          },
        },
        BigInt(100_000),
        BigInt(0),
      );

      assertTokenAmount(result.mintAmountBeforeFees, new BN(99_999));
      // 3/4 because only half of the swapped amount (100 tokens) should have fees on it (so 1/4)
      const expectedMintAmount =
        (result.mintAmountBeforeFees.raw * BigInt(3)) / BigInt(4);
      assertTokenAmount(result.mintAmount, expectedMintAmount);

      assertTokenAmount(
        result.fees,
        result.mintAmountBeforeFees.raw - expectedMintAmount,
      );
    });
  });

  describe("#calculateEstimatedWithdrawAmount", () => {
    it("works", () => {
      calculateEstimatedWithdrawAmount({
        ...exchangeInfo,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
      });
    });

    it("works with fees", () => {
      calculateEstimatedWithdrawAmount({
        ...exchangeInfoWithFees,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
      });
    });

    it("works zero with fees", () => {
      calculateEstimatedWithdrawAmount({
        ...exchangeInfoWithFees,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 0),
      });
    });
  });

  describe("#calculateEstimatedWithdrawOneAmount", () => {
    it("works", () => {
      calculateEstimatedWithdrawOneAmount({
        exchange: exchangeInfo,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
        withdrawToken: exchange.tokens[0],
      });
    });

    it("works with fees", () => {
      const result = calculateEstimatedWithdrawOneAmount({
        exchange: exchangeInfoWithFees,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 100_000),
        withdrawToken: exchange.tokens[0],
      });

      const resultMapped = mapValues(result, (q) => q.raw.toString());
      expect(resultMapped).toEqual({
        withdrawAmount: "99301",
        withdrawAmountBeforeFees: "99900",
        swapFee: "100",
        withdrawFee: "500",
        lpSwapFee: "50",
        lpWithdrawFee: "250",
        adminSwapFee: "50",
        adminWithdrawFee: "250",
      });
    });

    it("works zero with fees", () => {
      const result = calculateEstimatedWithdrawOneAmount({
        exchange: exchangeInfoWithFees,
        poolTokenAmount: new TokenAmount(exchange.lpToken, 0),
        withdrawToken: exchange.tokens[0],
      });

      const resultMapped = mapValues(result, (q) => q.raw.toString());
      expect(resultMapped).toEqual({
        withdrawAmount: "0",
        withdrawAmountBeforeFees: "0",
        swapFee: "0",
        withdrawFee: "0",
        lpSwapFee: "0",
        lpWithdrawFee: "0",
        adminSwapFee: "0",
        adminWithdrawFee: "0",
      });
    });
  });
});
