import {
  AssetClass,
  getInlineDatumOrThrow,
  isSameAssetClass,
} from '@3rd-eye-labs/cardano-offchain-common';
import { AssetInfo, fromSystemParamsAsset, SystemParams } from '../../src';
import {
  LucidContext,
  runAndAwaitTx,
  selectWalletByAddress,
} from '../test-helpers';
import {
  findAllNecessaryOrefs,
  findPrice,
  findPriceOracleFromCollateralAsset,
} from '../cdp/cdp-queries';
import { feedPriceOracleTx } from '../../src/contracts/price-oracle/transactions';
import { findCollateralAsset } from '../queries/iasset-queries';
import { unixTimeToSlot } from '@lucid-evolution/lucid';
import { findPriceOracle } from './price-oracle-queries';
import { match, P } from 'ts-pattern';
import { parsePriceOracleDatum } from '../../src/contracts/price-oracle/types-new';
import { expect } from 'vitest';
import { Rational } from '../../src/types/rational';

/**
 * Selects admin wallet and feeds the price oracle with the given price
 */
export async function runFeedPriceToOracle(
  context: LucidContext,
  sysParams: SystemParams,
  assetInfo: AssetInfo,
  collateralAsset: AssetClass,
  price: Rational,
): Promise<void> {
  const prevAddr = await context.lucid.wallet().address();

  context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);

  const collateralOut = await findCollateralAsset(
    context.lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    assetInfo.iassetTokenNameAscii,
    collateralAsset,
  );

  const collateralInfo = assetInfo.collateralAssets.find((a) =>
    isSameAssetClass(a.collateralAsset, collateralAsset),
  );

  if (!collateralInfo) {
    throw new Error('No such collateral asset for the given iasset.');
  }

  const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
    context.lucid,
    collateralOut,
  );

  await runAndAwaitTx(
    context.lucid,
    feedPriceOracleTx(
      context.lucid,
      priceOracleUtxo!,
      price,
      collateralInfo.oracleParams!,
      context.emulator.slot,
    ),
  );

  selectWalletByAddress(context, prevAddr);
}

/**
 * When oracle expired, refreshing updates the expiration leaving the same price.
 */
export async function refreshPriceOracle(
  context: LucidContext,
  sysParams: SystemParams,
  assetInfo: AssetInfo,
  collateralAsset: AssetClass,
): Promise<void> {
  const prevAddr = await context.lucid.wallet().address();

  context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);

  // Feed price oracle
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    assetInfo.iassetTokenNameAscii,
    collateralAsset,
  );

  const collateralInfo = assetInfo.collateralAssets.find((a) =>
    isSameAssetClass(a.collateralAsset, collateralAsset),
  );

  if (!collateralInfo) {
    throw new Error('No such collateral asset for the given iasset.');
  }

  const price = await findPrice(
    context.lucid,
    sysParams,
    assetInfo.iassetTokenNameAscii,
    collateralAsset,
  );

  const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
    context.lucid,
    orefs.collateralAsset,
  );

  await runAndAwaitTx(
    context.lucid,
    feedPriceOracleTx(
      context.lucid,
      priceOracleUtxo!,
      price,
      collateralInfo.oracleParams!,
      context.emulator.slot,
    ),
  );

  selectWalletByAddress(context, prevAddr);
}

export async function waitForOracleExpiration(
  context: LucidContext,
  sysParams: SystemParams,
  iasset: string,
  collateralAsset: AssetClass,
): Promise<void> {
  const collateral = await findCollateralAsset(
    context.lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpCreatorParams.collateralAssetAuthTk),
    iasset,
    collateralAsset,
  );

  await match(collateral.datum.priceInfo)
    .with({ OracleNft: P.select() }, async (nft) => {
      const priceOracle = await findPriceOracle(context.lucid, nft);

      const oracle = parsePriceOracleDatum(getInlineDatumOrThrow(priceOracle));

      const targetSlot = unixTimeToSlot(
        context.lucid.config().network!,
        Number(oracle.expirationTime),
      );

      if (targetSlot > context.emulator.slot) {
        expect(targetSlot).toBeGreaterThan(context.emulator.slot);

        context.emulator.awaitSlot(targetSlot - context.emulator.slot + 5);
      }
    })
    // otherwise no need to await
    .otherwise(() => {});
}
