import { beforeEach, describe, test, expect } from 'vitest';
import { IndigoTestContext, runAndAwaitTx } from '../test-helpers';
import {
  EXAMPLE_TOKEN_1,
  EXAMPLE_TOKEN_2,
  EXAMPLE_TOKEN_3,
  MAINNET_PROTOCOL_PARAMETERS,
} from '../indigo-test-helpers';
import {
  addAssets,
  Emulator,
  fromHex,
  fromText,
  generateEmulatorAccount,
  generateEmulatorAccountFromPrivateKey,
  Lucid,
  slotToUnixTime,
  toHex,
} from '@lucid-evolution/lucid';
import {
  adaAssetClass,
  AssetClass,
  mkAssetsOf,
  mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  DEFAULT_INTEREST,
  DEFAULT_PRICE,
  ibtcInitialAssetCfgWithPyth,
  iethInitialAssetCfgWithPyth,
  iusdInitialAssetCfgWithPyth,
  mkBaseCollateralAsset,
} from '../mock/assets-mock';
import { runFreezeCdp, runOpenCdp, runWithdrawCdp } from '../cdp/actions';
import { benchmarkAndAwaitTx } from '../utils/benchmark-utils';
import { addrDetails, MIN_ROB_COLLATERAL_AMT, openRob } from '../../src';
import { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk';
import { createPythMessage } from './helpers';
import { findSingleRob } from '../rob/rob-queries';
import { expectValue } from '../utils/asserts';
import { runRedeemRob } from '../rob/actions';
import { rationalFromInt } from '../../src/types/rational';
import { init } from '../endpoints/initialize';

const collateralAssetA: AssetClass = {
  currencySymbol: fromHex(
    // random generated
    'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dea',
  ),
  tokenName: fromHex(fromText('A')),
};

describe('Pyth > Indigo', () => {
  beforeEach<IndigoTestContext>(async (context: IndigoTestContext) => {
    context.users = {
      admin: generateEmulatorAccount(
        addAssets(
          mkLovelacesOf(100_000_000_000_000n),
          mkAssetsOf(EXAMPLE_TOKEN_1, 1_000_000_000_000_000n),
          mkAssetsOf(collateralAssetA, 100_000_000n),
        ),
      ),
      user: generateEmulatorAccount(
        addAssets(
          mkLovelacesOf(100_000_000_000_000n),
          mkAssetsOf(EXAMPLE_TOKEN_1, 1_000_000_000_000_000n),
          mkAssetsOf(EXAMPLE_TOKEN_2, 1_000_000_000_000_000n),
          mkAssetsOf(EXAMPLE_TOKEN_3, 1_000_000_000_000_000n),
        ),
      ),
      user2: generateEmulatorAccount({
        lovelace: BigInt(100_000_000_000_000),
      }),
      withdrawalAccount: generateEmulatorAccountFromPrivateKey({}),
    };

    context.emulator = new Emulator(
      [
        context.users.admin,
        context.users.user,
        context.users.user2,
        context.users.withdrawalAccount,
      ],
      MAINNET_PROTOCOL_PARAMETERS,
    );
    context.lucid = await Lucid(context.emulator, 'Custom');
    context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);

    const [systemParams, assetConfigs] = await init(
      context.lucid,
      [],
      context.emulator.slot,
      (pythStateNft: AssetClass) => [
        iusdInitialAssetCfgWithPyth(pythStateNft, (pythStatePolicyId) => [
          mkBaseCollateralAsset(
            adaAssetClass,
            DEFAULT_INTEREST,
            DEFAULT_PRICE,
            0n,
            pythStatePolicyId,
            {
              tag: 'value',
              val: { priceFeedId: 1 },
            },
          ),
        ]),
        iethInitialAssetCfgWithPyth(pythStateNft, (pythStatePolicyId) => [
          mkBaseCollateralAsset(
            collateralAssetA,
            DEFAULT_INTEREST,
            DEFAULT_PRICE,
            0n,
            pythStatePolicyId,
            {
              tag: 'value',
              val: { priceFeedId: 2 },
            },
          ),
        ]),
        ibtcInitialAssetCfgWithPyth(pythStateNft, (pythStatePolicyId) => [
          mkBaseCollateralAsset(
            adaAssetClass,
            DEFAULT_INTEREST,
            DEFAULT_PRICE,
            0n,
            pythStatePolicyId,
            {
              tag: 'divide',
              val: [
                { tag: 'value', val: { priceFeedId: 1 } },
                { tag: 'value', val: { priceFeedId: 2 } },
              ],
            },
          ),
        ]),
      ],
    );

    context.systemParams = systemParams;
    context.assetConfigs = assetConfigs;
  });

  test('Open CDP - Direct Price Value (ada collateral)', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );

    const iUsdFeed: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000000',
      exponent: -6,
    };
    const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);

    await benchmarkAndAwaitTx(
      'Pyth - CDP - Direct Price Value (ADA collateral)',
      await runOpenCdp(
        context,
        context.systemParams,
        'iUSD',
        adaAssetClass,
        10_000_000n,
        500_000n,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });

  test('Open CDP - Direct Price Value (non-ADA collateral)', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );

    const iEthFeed: ParsedFeedPayload = {
      priceFeedId: 2,
      price: '1000000',
      exponent: -6,
      feedUpdateTimestamp: Number(currentTime) * 1000,
    };
    const message = await createPythMessage([iEthFeed], currentTime * 1_000n);

    await benchmarkAndAwaitTx(
      'Pyth - CDP - Direct Price Value (non-ADA collateral)',
      await runOpenCdp(
        context,
        context.systemParams,
        'iETH',
        collateralAssetA,
        10_000_000n,
        500_000n,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });

  test('Open CDP - Divide Price Value', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );

    const feed1: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000000',
      exponent: -6,
    };
    const feed2: ParsedFeedPayload = {
      priceFeedId: 2,
      price: '10',
      exponent: -1,
    };
    const message = await createPythMessage(
      [feed1, feed2],
      currentTime * 1_000n,
    );

    await benchmarkAndAwaitTx(
      'Pyth - CDP - Divide Price Value',
      await runOpenCdp(
        context,
        context.systemParams,
        'iBTC',
        adaAssetClass,
        10_000_000n,
        500_000n,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });

  test('Open CDP - Fails if missing feed price', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );

    // This feed doesn't include a price for feedId 2, which is used by the iBTC asset
    const feed1: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000000',
      exponent: -6,
    };
    const message = await createPythMessage([feed1], currentTime * 1_000n);

    await expect(
      runOpenCdp(
        context,
        context.systemParams,
        'iBTC',
        adaAssetClass,
        10_000_000n,
        500_000n,
        toHex(message),
      ),
    ).rejects.toThrowError('Pyth payload not found for feed ID: 2');
  });

  test('Withdraw CDP - Direct Price Value', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );

    const iUsdFeed: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000000',
      exponent: -6,
      feedUpdateTimestamp: Number(currentTime) * 1000,
    };
    const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);

    await runAndAwaitTx(
      context.lucid,
      runOpenCdp(
        context,
        context.systemParams,
        'iUSD',
        adaAssetClass,
        11_000_000n,
        500_000n,
        toHex(message),
      ),
    );

    await benchmarkAndAwaitTx(
      'Pyth - CDP - Withdraw CDP - Direct Price Value',
      await runWithdrawCdp(
        context,
        context.systemParams,
        'iUSD',
        adaAssetClass,
        1_000_000n,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });

  test('Freeze CDP - Direct Price Value', async (context: IndigoTestContext) => {
    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );
    {
      const iUsdFeed: ParsedFeedPayload = {
        priceFeedId: 1,
        price: '1000000',
        exponent: -6,
      };
      const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);

      await runAndAwaitTx(
        context.lucid,
        runOpenCdp(
          context,
          context.systemParams,
          'iUSD',
          adaAssetClass,
          11_000_000n,
          500_000n,
          toHex(message),
        ),
      );
    }
    {
      const iUsdFeed: ParsedFeedPayload = {
        priceFeedId: 1,
        price: '100000000',
        exponent: -6,
      };
      const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);
      const [pkh, skh] = await addrDetails(context.lucid);

      await benchmarkAndAwaitTx(
        'Pyth - CDP - Freeze CDP - Direct Price Value',
        await runFreezeCdp(
          context,
          context.systemParams,
          'iUSD',
          adaAssetClass,
          pkh.hash,
          skh,
          toHex(message),
        ),
        context.lucid,
        context.emulator,
      );
    }
  });

  test<IndigoTestContext>('ROB redemption on buy order', async (context: IndigoTestContext) => {
    const [ownPkh, _] = await addrDetails(context.lucid);

    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );
    const iUsdFeed: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000000',
      exponent: -6,
      feedUpdateTimestamp: Number(currentTime) * 1000,
    };
    const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);

    await runAndAwaitTx(
      context.lucid,
      runOpenCdp(
        context,
        context.systemParams,
        'iUSD',
        adaAssetClass,
        100_000_000n,
        30_000_000n,
        toHex(message),
      ),
    );

    const initialDeposit = 20_000_000n;

    await runAndAwaitTx(
      context.lucid,
      openRob(
        'iUSD',
        initialDeposit,
        {
          BuyIAssetOrder: {
            collateralAsset: adaAssetClass,
            maxPrice: rationalFromInt(1n),
          },
        },
        context.lucid,
        context.systemParams,
      ),
    );

    const robUtxo = await findSingleRob(
      context,
      context.systemParams,
      'iUSD',
      ownPkh,
    );

    const redemptionIAssetAmt = 7_500_000n;

    await benchmarkAndAwaitTx(
      'Pyth - ROB - Redemption on buy order',
      await runRedeemRob(
        context,
        context.systemParams,
        [[robUtxo, redemptionIAssetAmt]],
        'iUSD',
        adaAssetClass,
        context.emulator.slot,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });

  test<IndigoTestContext>('ROB redemption on sell order', async (context: IndigoTestContext) => {
    const [ownPkh, _] = await addrDetails(context.lucid);

    const currentTime = BigInt(
      slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
    );
    const iUsdFeed: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1250000',
      exponent: -6,
      feedUpdateTimestamp: Number(currentTime) * 1000,
    };
    const message = await createPythMessage([iUsdFeed], currentTime * 1_000n);

    await runAndAwaitTx(
      context.lucid,
      runOpenCdp(
        context,
        context.systemParams,
        'iUSD',
        adaAssetClass,
        100_000_000n,
        30_000_000n,
        toHex(message),
      ),
    );

    const initialDeposit = 20_000_000n;

    await runAndAwaitTx(
      context.lucid,
      openRob(
        'iUSD',
        initialDeposit,
        {
          SellIAssetOrder: {
            allowedCollateralAssets: [[adaAssetClass, rationalFromInt(1n)]],
          },
        },
        context.lucid,
        context.systemParams,
      ),
    );

    const robUtxo = await findSingleRob(
      context,
      context.systemParams,
      'iUSD',
      ownPkh,
    );

    const iassetAc: AssetClass = {
      currencySymbol: fromHex(
        context.systemParams.robParams.iassetPolicyId.unCurrencySymbol,
      ),
      tokenName: fromHex(fromText('iUSD')),
    };

    expectValue(
      robUtxo.utxo.assets,
      'Wrong ROB value before redemption',
    ).toEqual(
      addAssets(
        mkLovelacesOf(MIN_ROB_COLLATERAL_AMT),
        mkAssetsOf(iassetAc, initialDeposit),
      ),
    );

    const payoutCollateralAmt = 7_500_000n;

    await benchmarkAndAwaitTx(
      'Pyth - ROB - Redemption on sell order',
      await runRedeemRob(
        context,
        context.systemParams,
        [[robUtxo, payoutCollateralAmt]],
        'iUSD',
        adaAssetClass,
        context.emulator.slot,
        toHex(message),
      ),
      context.lucid,
      context.emulator,
    );
  });
});
