import {
  addAssets,
  Data,
  fromHex,
  LucidEvolution,
  Credential,
  OutRef,
  slotToUnixTime,
  toHex,
  TxBuilder,
  UTxO,
  getInputIndices,
  credentialToAddress,
  validatorToScriptHash,
  credentialToRewardAddress,
  scriptHashToCredential,
} from '@lucid-evolution/lucid';
import {
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../src/types/system-params';
import {
  addrDetails,
  calculateMinCollateralCappedIAssetRedemptionAmt,
  createScriptAddress,
  getInlineDatumOrThrow,
  matchSingle,
  mkPriceOracleValidator,
  mkTreasuryValidatorFromSP,
  ONE_SECOND,
  PriceOracleParams,
  treasuryFeeTx,
} from '../../src';
import {
  parseCdpDatumOrThrow,
  serialiseCdpDatum,
  serialiseCdpRedeemer,
  serialiseRedeemCdpWithdrawalRedeemer,
} from '../../src/contracts/cdp/types-new';
import {
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
} from '../../src/contracts/iasset/types';
import {
  parsePriceOracleDatum,
  serialisePriceOracleDatum,
  serialisePriceOracleRedeemer,
} from '../../src/contracts/price-oracle/types-new';
import { parseInterestOracleDatum } from '../../src/contracts/interest-oracle/types-new';
import { parseGovDatumOrThrow } from '../../src/contracts/gov/types-new';
import { match, P } from 'ts-pattern';
import {
  calculateAccruedInterest,
  calculateUnitaryInterestSinceOracleLastUpdated,
} from '../../src/contracts/interest-oracle/helpers';
import { calculateFeeFromRatio } from '../../src/utils/indigo-helpers';
import {
  AssetClass,
  assetClassValueOf,
  mkAssetsOf,
  mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import { bigintMin } from '../../src/utils/bigint-utils';
import { oracleExpirationAwareValidity } from '../../src/contracts/price-oracle/helpers';
import {
  findAllNecessaryOrefs,
  findCdp,
  findPriceOracleFromCollateralAsset,
} from './cdp-queries';
import { collectInterestTx } from '../../src/contracts/interest-collection/transactions';
import { findCollateralAsset, findIAsset } from '../queries/iasset-queries';
import { findInterestOracle } from '../queries/interest-oracle-queries';
import { AssetInfo } from '../endpoints/initialize';
import { LucidContext } from '../test-helpers';
import { option as O, function as F } from 'fp-ts';
import { findPriceOracle } from '../price-oracle/price-oracle-queries';
import { findRandomNonAdminInterestCollector } from '../interest-collection/interest-collector-queries';
import {
  serialiseCDPCreatorDatum,
  serialiseCDPCreatorRedeemer,
} from '../../src/contracts/cdp-creator/types-new';
import {
  CollectInterestVariation,
  testCollectInterest,
} from '../interest-collection/transactions-mutated';
import { findRandomTreasuryUtxoWithOnlyAda } from '../treasury/treasury-queries';
import {
  Rational,
  rationalFloor,
  rationalFromInt,
  rationalMul,
} from '../../src/types/rational';
import { retrieveAdjustedPrice } from '../../src/utils/oracle-helpers';
import * as Core from '@evolution-sdk/evolution';

export async function mutatedRedeemCdp(
  /**
   * When the goal is to redeem the maximum possible, just pass in the total minted amount of the CDP.
   * The logic will automatically cap the amount to the max.
   */
  attemptedRedemptionIAssetAmt: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  interestCollectorOref: OutRef,
  govOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage?: string,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpRedeemRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpRedeemValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp redeem Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const isDelisted = match(collateralAssetDatum.priceInfo)
    .with({ Delisted: P.any }, () => true)
    .otherwise(() => false);

  if (!isDelisted && priceOracleOref === undefined) {
    throw new Error('Missing price oracle');
  }

  const [adjustedPrice, priceOracleUtxo] = await retrieveAdjustedPrice(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    collateralAssetDatum.extraDecimals,
    priceOracleOref,
    pythMessage,
    sysParams.pythConfig,
    lucid,
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const govUtxo = matchSingle(
    await lucid.utxosByOutRef([govOref]),
    (_) => new Error('Expected a single gov UTXO'),
  );
  const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo));

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const collateralAmt = assetClassValueOf(
    cdpUtxo.assets,
    cdpDatum.collateralAsset,
  );

  const totalCdpDebt = cdpDatum.mintedAmt + interestAmt;

  const [isPartial, redemptionIAssetAmt] = (() => {
    const res = calculateMinCollateralCappedIAssetRedemptionAmt(
      collateralAmt,
      totalCdpDebt,
      adjustedPrice,
      collateralAssetDatum.redemptionRatio,
      iassetDatum.redemptionReimbursementRatio,
      BigInt(collateralAssetDatum.minCollateralAmt),
    );

    const redemptionAmt = bigintMin(
      attemptedRedemptionIAssetAmt,
      res.cappedIAssetRedemptionAmt,
    );

    return [redemptionAmt < res.cappedIAssetRedemptionAmt, redemptionAmt];
  })();

  if (redemptionIAssetAmt <= 0) {
    throw new Error("There's no iAssets available for redemption.");
  }

  const redemptionCollateralAmt = rationalFloor(
    rationalMul(adjustedPrice, rationalFromInt(redemptionIAssetAmt)),
  );

  const isPublicRedemption =
    !govDatum.protocolParams.cdpRedemptionRequiredSignature;

  const partialRedemptionFee =
    isPartial && isPublicRedemption
      ? BigInt(sysParams.cdpRedeemParams.partialRedemptionExtraFeeLovelace)
      : 0n;

  const processingFee = calculateFeeFromRatio(
    iassetDatum.redemptionProcessingFeeRatio,
    redemptionIAssetAmt,
  );

  const reimburstmentFee = calculateFeeFromRatio(
    iassetDatum.redemptionReimbursementRatio,
    redemptionCollateralAmt,
  );

  const referenceScripts = [
    cdpRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
    cdpRedeemRefScriptUtxo,
  ];

  const referenceInputs = [
    iassetUtxo,
    collateralAssetUtxo,
    interestOracleUtxo,
    govUtxo,
  ];

  const tx = lucid
    .newTx()
    // Ref Script
    .readFrom(referenceScripts)
    // Ref inputs
    .readFrom(referenceInputs)
    .collectFrom([cdpUtxo], serialiseCdpRedeemer('RedeemCdp'))
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: iassetDatum.assetName,
        },
        interestAmt - redemptionIAssetAmt,
      ),
      Data.void(),
    )
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: totalCdpDebt - redemptionIAssetAmt,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                interestOracleDatum.unitaryInterest +
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ),
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        mkLovelacesOf(-redemptionCollateralAmt),
        mkLovelacesOf(reimburstmentFee),
      ),
    );

  if (priceOracleUtxo !== undefined) {
    const priceOracleDatum = parsePriceOracleDatum(
      getInlineDatumOrThrow(priceOracleUtxo),
    );
    const txValidity = oracleExpirationAwareValidity(
      currentSlot,
      Number(sysParams.cdpCreatorParams.biasTime),
      Number(priceOracleDatum.expirationTime),
      network,
    );

    referenceInputs.push(priceOracleUtxo);

    tx.validFrom(txValidity.validFrom)
      .validTo(txValidity.validTo)
      .readFrom([priceOracleUtxo]);
  } else {
    const validateFrom = slotToUnixTime(network, currentSlot - 1);
    const validateTo =
      validateFrom + Number(sysParams.cdpCreatorParams.biasTime);

    tx.validFrom(validateFrom).validTo(validateTo);
  }

  // Trigger CDP Redeem Withdrawal validator
  tx.withdraw(
    credentialToRewardAddress(
      lucid.config().network!,
      scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash),
    ),
    0n,
    serialiseRedeemCdpWithdrawalRedeemer({
      cdpOutReference: {
        txHash: fromHex(cdpUtxo.txHash),
        outputIndex: BigInt(cdpUtxo.outputIndex),
      },
      currentTime: currentTime,
      priceOracleIdx: priceOracleUtxo
        ? {
            OracleRefInputIdx: getInputIndices(
              [priceOracleUtxo],
              [...referenceInputs, ...referenceScripts],
            )[0],
          }
        : 'OracleVoid',
    }),
  );

  //TODO: Use a treasury input to save on ADA.
  tx.pay.ToContract(
    credentialToAddress(lucid.config().network!, {
      hash: validatorToScriptHash(
        mkTreasuryValidatorFromSP(sysParams.treasuryParams),
      ),
      type: 'Script',
    }),
    { kind: 'inline', value: Data.void() },
    addAssets(
      mkAssetsOf(cdpDatum.collateralAsset, processingFee),
      mkLovelacesOf(partialRedemptionFee),
    ),
  );
  if (interestAmt > 0n) {
    await collectInterestTx(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: iassetDatum.assetName,
        },
        interestAmt,
      ),
      lucid,
      sysParams,
      tx,
      interestCollectorOref,
    );
  }

  return tx;
}

export async function runOpenCdpDelisted(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  initialCollateral: bigint,
  initialMint: bigint,
): Promise<TxBuilder> {
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const network = context.lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot));

  const [pkh, skh] = await addrDetails(context.lucid);

  const cdpCreatorRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpCreatorValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp creator Ref Script UTXO'),
  );
  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );
  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const iassetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.iasset.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.collateralAsset.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const interestOracleUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.interestOracleUtxo]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const cdpCreatorUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.cdpCreatorUtxo]),
    (_) => new Error('Expected a single CDP creator UTXO'),
  );

  const cdpNftVal = mkAssetsOf(
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    1n,
  );

  const iassetClass = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  const iassetTokensVal = mkAssetsOf(iassetClass, initialMint);

  const refScripts: UTxO[] = [
    cdpCreatorRefScriptUtxo,
    cdpAuthTokenPolicyRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
  ];

  const referenceInputs: UTxO[] = [
    interestOracleUtxo,
    iassetUtxo,
    collateralAssetUtxo,
  ];

  const tx = context.lucid
    .newTx()
    .readFrom(refScripts)
    .readFrom(referenceInputs)
    .mintAssets(cdpNftVal, Data.void())
    .mintAssets(iassetTokensVal, Data.void())
    .pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh),
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          cdpOwner: fromHex(pkh.hash),
          iasset: iassetDatum.assetName,
          collateralAsset: collateralAssetDatum.collateralAsset,
          mintedAmt: initialMint,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpNftVal,
        mkAssetsOf(collateralAssetDatum.collateralAsset, initialCollateral),
      ),
    )
    .pay.ToContract(
      cdpCreatorUtxo.address,
      {
        kind: 'inline',
        value: serialiseCDPCreatorDatum({
          creatorInputOref: {
            outputIndex: BigInt(cdpCreatorUtxo.outputIndex),
            txHash: fromHex(cdpCreatorUtxo.txHash),
          },
        }),
      },
      cdpCreatorUtxo.assets,
    );

  const debtMintingFee = calculateFeeFromRatio(
    iassetDatum.debtMintingFeeRatio,
    initialMint,
  );

  const treasuryRefScriptUtxo =
    debtMintingFee > 0
      ? await treasuryFeeTx(
          iassetClass,
          debtMintingFee,
          0n,
          context.lucid,
          sysParams,
          tx,
          cdpCreatorUtxo,
          orefs.treasuryUtxo,
        )
      : undefined;

  const validFrom = slotToUnixTime(network, context.emulator.slot - 1);
  const validTo = validFrom + Number(sysParams.cdpCreatorParams.biasTime);

  tx.validFrom(validFrom).validTo(validTo);

  // We need to take into account the treasury ref script.
  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...refScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
  ]);

  tx.collectFrom([cdpCreatorUtxo], {
    kind: 'self',
    makeRedeemer: (inputIdx) => {
      return serialiseCDPCreatorRedeemer({
        CreateCDP: {
          cdpOwner: fromHex(pkh.hash),
          minted: initialMint,
          collateralAmt: initialCollateral,
          currentTime: currentTime,
          creatorInputIdx: inputIdx,
          creatorOutputIdx: 1n,
          cdpOutputIdx: 0n,
          iassetRefInputIdx: refInputsIndices[1],
          collateralAssetRefInputIdx: refInputsIndices[2],
          interestOracleRefInputIdx: refInputsIndices[0],
          priceOracleIdx: 'OracleVoid',
        },
      });
    },
  });

  return tx;
}

export async function runOpenCdpAndUpdateOracle(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  initialCollateral: bigint,
  initialMint: bigint,
  oracleParams: PriceOracleParams,
  newPrice: Rational,
): Promise<TxBuilder> {
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const network = context.lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot));

  const [pkh, skh] = await addrDetails(context.lucid);

  const oracleValidator = mkPriceOracleValidator(oracleParams);

  const cdpCreatorRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpCreatorValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp creator Ref Script UTXO'),
  );
  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );
  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const iassetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.iasset.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.collateralAsset.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

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

  if (!priceOracleUtxo) throw new Error('Expected a price oracle');

  const interestOracleUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.interestOracleUtxo]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const cdpCreatorUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.cdpCreatorUtxo]),
    (_) => new Error('Expected a single CDP creator UTXO'),
  );

  const cdpNftVal = mkAssetsOf(
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    1n,
  );

  const iassetClass = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  const iassetTokensVal = mkAssetsOf(iassetClass, initialMint);

  const refScripts: UTxO[] = [
    cdpCreatorRefScriptUtxo,
    cdpAuthTokenPolicyRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
  ];

  const referenceInputs: UTxO[] = [
    interestOracleUtxo,
    iassetUtxo,
    collateralAssetUtxo,
  ];

  const tx = context.lucid
    .newTx()
    .validFrom(Number(currentTime - oracleParams.biasTime) + ONE_SECOND)
    .validTo(Number(currentTime + oracleParams.biasTime) - ONE_SECOND)
    .attach.SpendingValidator(oracleValidator)
    .readFrom(refScripts)
    .readFrom(referenceInputs)
    .mintAssets(cdpNftVal, Data.void())
    .mintAssets(iassetTokensVal, Data.void())
    .collectFrom(
      [priceOracleUtxo],
      serialisePriceOracleRedeemer({
        currentTime: currentTime,
        newPrice: newPrice,
      }),
    )
    .pay.ToContract(
      priceOracleUtxo.address,
      {
        kind: 'inline',
        value: serialisePriceOracleDatum({
          price: newPrice,
          expirationTime: currentTime + oracleParams.expirationPeriod,
          auxiliaryData: Core.Data.fromCBORHex(Data.void()),
        }),
      },
      priceOracleUtxo.assets,
    )
    .pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh),
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          cdpOwner: fromHex(pkh.hash),
          iasset: iassetDatum.assetName,
          collateralAsset: collateralAssetDatum.collateralAsset,
          mintedAmt: initialMint,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpNftVal,
        mkAssetsOf(collateralAssetDatum.collateralAsset, initialCollateral),
      ),
    )
    .pay.ToContract(
      cdpCreatorUtxo.address,
      {
        kind: 'inline',
        value: serialiseCDPCreatorDatum({
          creatorInputOref: {
            outputIndex: BigInt(cdpCreatorUtxo.outputIndex),
            txHash: fromHex(cdpCreatorUtxo.txHash),
          },
        }),
      },
      cdpCreatorUtxo.assets,
    )
    .addSignerKey(pkh.hash);

  const debtMintingFee = calculateFeeFromRatio(
    iassetDatum.debtMintingFeeRatio,
    initialMint,
  );

  const treasuryRefScriptUtxo =
    debtMintingFee > 0
      ? await treasuryFeeTx(
          iassetClass,
          debtMintingFee,
          0n,
          context.lucid,
          sysParams,
          tx,
          cdpCreatorUtxo,
          orefs.treasuryUtxo,
        )
      : undefined;

  // We need to take into account the treasury ref script as well.
  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...refScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
  ]);

  tx.collectFrom([cdpCreatorUtxo], {
    kind: 'self',
    makeRedeemer: (inputIdx) => {
      return serialiseCDPCreatorRedeemer({
        CreateCDP: {
          cdpOwner: fromHex(pkh.hash),
          minted: initialMint,
          collateralAmt: initialCollateral,
          currentTime: currentTime,
          creatorInputIdx: inputIdx,
          creatorOutputIdx: 2n,
          cdpOutputIdx: 1n,
          iassetRefInputIdx: refInputsIndices[1],
          collateralAssetRefInputIdx: refInputsIndices[2],
          interestOracleRefInputIdx: refInputsIndices[0],
          priceOracleIdx: { OracleOutputIdx: 0n },
        },
      });
    },
  });

  return tx;
}

export async function runTestAdjustCdpDelisted(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  collateralAdjustment: bigint,
  debtAdjustment: bigint,
): Promise<TxBuilder> {
  const network = context.lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot));
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );

  const cdpRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await context.lucid.utxosByOutRef([cdp.utxo]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetOutput = await findIAsset(
    context.lucid,
    sysParams.validatorHashes.iassetHash,
    fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken),
    asset,
  );

  const iassetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([iassetOutput.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetOutput = await findCollateralAsset(
    context.lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    asset,
    collateralAsset,
  );

  const collateralAssetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([collateralAssetOutput.utxo]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const interestOracleUtxo = await findInterestOracle(
    context.lucid,
    collateralAssetDatum.interestOracleNft,
  );

  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const interestCollectorUtxo = await findRandomNonAdminInterestCollector(
    context.lucid,
    sysParams.validatorHashes.interestCollectionHash,
    fromSystemParamsAsset(sysParams.interestCollectionParams.multisigUtxoNft),
  );

  const validateFrom = slotToUnixTime(network, context.emulator.slot - 1);
  const validateTo =
    validateFrom + Number(sysParams.cdpParams.biasTime) - 60_000;

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const mintedAmountChange = debtAdjustment + interestAmt;

  const tx = context.lucid
    .newTx()
    .validFrom(validateFrom)
    .validTo(validateTo)
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({
        AdjustCdp: {
          currentTime: currentTime,
          debtAdjustment,
          collateralAdjustment,
          priceOracleIdx: 'OracleVoid',
        },
      }),
    )
    .readFrom([cdpRefScriptUtxo])
    .readFrom([iassetUtxo, collateralAssetUtxo, interestOracleUtxo])
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: cdpDatum.mintedAmt + mintedAmountChange,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        mkAssetsOf(cdpDatum.collateralAsset, collateralAdjustment),
      ),
    );

  if (!cdpDatum.cdpOwner) {
    throw new Error('Expected active CDP');
  }

  tx.addSignerKey(toHex(cdpDatum.cdpOwner));

  if (mintedAmountChange !== 0n) {
    const iAssetTokenPolicyRefScriptUtxo = matchSingle(
      await context.lucid.utxosByOutRef([
        fromSystemParamsScriptRef(
          sysParams.scriptReferences.iAssetTokenPolicyRef,
        ),
      ]),
      (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
    );

    const iassetTokensVal = mkAssetsOf(
      {
        currencySymbol: fromHex(
          sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
        ),
        tokenName: iassetDatum.assetName,
      },
      mintedAmountChange,
    );

    tx.readFrom([iAssetTokenPolicyRefScriptUtxo]).mintAssets(
      iassetTokensVal,
      Data.void(),
    );
  }

  const iAssetAc = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  if (interestAmt > 0n) {
    await collectInterestTx(
      mkAssetsOf(iAssetAc, interestAmt),
      context.lucid,
      sysParams,
      tx,
      interestCollectorUtxo,
    );
  }

  let treasuryFee = 0n;

  if (debtAdjustment > 0n) {
    const treasuryUtxo = await findRandomTreasuryUtxoWithOnlyAda(
      context.lucid,
      sysParams,
    );

    treasuryFee += calculateFeeFromRatio(
      iassetDatum.debtMintingFeeRatio,
      debtAdjustment,
    );

    if (treasuryFee > 0n) {
      await treasuryFeeTx(
        iAssetAc,
        treasuryFee,
        0n,
        context.lucid,
        sysParams,
        tx,
        cdpUtxo,
        treasuryUtxo,
      );
    }
  }

  return tx;
}

export async function runTestDepositCdpWithInterestVar(
  context: LucidContext,
  sysParams: SystemParams,
  iasset: string,
  collateralAsset: AssetClass,
  interestCollectorUtxo: UTxO,
  interestVariation: CollectInterestVariation,

  amount: bigint = 1_000_000n,
): Promise<TxBuilder> {
  const network = context.lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot));
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );

  const cdpRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await context.lucid.utxosByOutRef([cdp.utxo]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetOutput = await findIAsset(
    context.lucid,
    sysParams.validatorHashes.iassetHash,
    fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken),
    iasset,
  );

  const iassetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([iassetOutput.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetOutput = await findCollateralAsset(
    context.lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    iasset,
    collateralAsset,
  );

  const collateralAssetUtxo = matchSingle(
    await context.lucid.utxosByOutRef([collateralAssetOutput.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );

  const interestOracleUtxo = await findInterestOracle(
    context.lucid,
    collateralAssetOutput.datum.interestOracleNft,
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const validateFrom = slotToUnixTime(network, context.emulator.slot - 1);
  const validateTo =
    validateFrom + Number(sysParams.cdpParams.biasTime) - 60_000;

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const mintedAmountChange = interestAmt;

  const tx = context.lucid
    .newTx()
    .validFrom(validateFrom)
    .validTo(validateTo)
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({
        AdjustCdp: {
          currentTime: currentTime,
          debtAdjustment: 0n,
          collateralAdjustment: amount,
          priceOracleIdx: 'OracleVoid',
        },
      }),
    )
    .readFrom([cdpRefScriptUtxo])
    .readFrom([iassetUtxo, collateralAssetUtxo, interestOracleUtxo])
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: cdpDatum.mintedAmt + mintedAmountChange,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(cdpUtxo.assets, mkLovelacesOf(amount)),
    );

  if (!cdpDatum.cdpOwner) {
    throw new Error('Expected active CDP');
  }

  tx.addSignerKey(toHex(cdpDatum.cdpOwner));

  if (mintedAmountChange !== 0n) {
    const iAssetTokenPolicyRefScriptUtxo = matchSingle(
      await context.lucid.utxosByOutRef([
        fromSystemParamsScriptRef(
          sysParams.scriptReferences.iAssetTokenPolicyRef,
        ),
      ]),
      (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
    );

    const iassetTokensVal = mkAssetsOf(
      {
        currencySymbol: fromHex(
          sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
        ),
        tokenName: iassetDatum.assetName,
      },
      mintedAmountChange,
    );

    tx.readFrom([iAssetTokenPolicyRefScriptUtxo]).mintAssets(
      iassetTokensVal,
      Data.void(),
    );
  }

  if (interestAmt > 0n) {
    await testCollectInterest(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: iassetDatum.assetName,
        },
        interestAmt,
      ),
      context.lucid,
      sysParams,
      tx,
      interestCollectorUtxo,
      interestVariation,
    );
  }

  return tx;
}

export async function runCloseCdpWrongOracle(
  lucid: LucidEvolution,
  currentSlot: number,
  sysParams: SystemParams,
  iasset: string,
  wrongAsset: string,
  collateralAsset: AssetClass,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(lucid);

  const cdp = await findCdp(
    lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    lucid,
    sysParams,
    iasset,
    collateralAsset,
  );

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

  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdp.utxo]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([orefs.iasset.utxo]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const priceOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([priceOracleOref!]),
    (_) => new Error('Expected a single price oracle UTXO'),
  );
  const priceOracleDatum = parsePriceOracleDatum(
    getInlineDatumOrThrow(priceOracleUtxo),
  );

  const wrongCollateral = await findCollateralAsset(
    lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    wrongAsset,
    collateralAsset,
  );

  const wrongInterestOracleUtxo = await findInterestOracle(
    lucid,
    wrongCollateral.datum.interestOracleNft,
  );

  const wrongInterestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(wrongInterestOracleUtxo),
  );

  const txValidity = oracleExpirationAwareValidity(
    currentSlot,
    Number(sysParams.cdpCreatorParams.biasTime),
    Number(priceOracleDatum.expirationTime),
    network,
  );

  const tx = lucid
    .newTx()
    .readFrom([
      cdpRefScriptUtxo,
      iAssetTokenPolicyRefScriptUtxo,
      cdpAuthTokenPolicyRefScriptUtxo,
    ])
    .readFrom([wrongCollateral.utxo, wrongInterestOracleUtxo])
    .validFrom(txValidity.validFrom)
    .validTo(txValidity.validTo)
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: iassetDatum.assetName,
        },
        -cdpDatum.mintedAmt,
      ),
      Data.void(),
    )
    .mintAssets(
      mkAssetsOf(fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), -1n),
      Data.void(),
    )
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({ CloseCdp: { currentTime: currentTime } }),
    );

  if (!cdpDatum.cdpOwner) {
    throw new Error('Expected active CDP');
  }

  tx.addSignerKey(toHex(cdpDatum.cdpOwner));

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        wrongInterestOracleDatum,
      );
    })
    .exhaustive();

  if (interestAmt > 0n) {
    await collectInterestTx(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: cdpDatum.iasset,
        },
        interestAmt,
      ),
      lucid,
      sysParams,
      tx,
      orefs.interestCollectorUtxo,
    );
  }

  return tx;
}

export async function runRedeemCdpWrongOracle(
  context: LucidContext,
  sysParams: SystemParams,
  assetInfo: AssetInfo,
  wrongAssetInfo: AssetInfo,
  collateralAsset: AssetClass,
  pkh: string,
  skh: Credential | undefined,
): Promise<TxBuilder> {
  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh,
    skh,
  );

  const cdpRedeemRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpRedeemValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp redeem Ref Script UTXO'),
  );

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

  const wrongIasset = await findIAsset(
    context.lucid,
    sysParams.validatorHashes.iassetHash,
    fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken),
    wrongAssetInfo.iassetTokenNameAscii,
  );

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

  const wrongInterestOracleUtxo = await findInterestOracle(
    context.lucid,
    wrongCollateral.datum.interestOracleNft,
  );

  const wrongInterestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(wrongInterestOracleUtxo),
  );

  const wrongPriceOracleUtxo = await findPriceOracle(
    context.lucid,
    match(wrongCollateral.datum.priceInfo)
      .with({ OracleNft: P.select() }, (oracleNft) => oracleNft)
      .otherwise(() => {
        throw new Error('Expected active oracle');
      }),
  );

  const wrongPriceOracleDatum = parsePriceOracleDatum(
    getInlineDatumOrThrow(wrongPriceOracleUtxo),
  );

  const network = context.lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot));

  const cdpRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await context.lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await context.lucid.utxosByOutRef([cdp.utxo]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const govUtxo = matchSingle(
    await context.lucid.utxosByOutRef([orefs.govUtxo]),
    (_) => new Error('Expected a single gov UTXO'),
  );
  const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo));

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        wrongInterestOracleDatum,
      );
    })
    .exhaustive();

  const collateralAmt = assetClassValueOf(
    cdpUtxo.assets,
    cdpDatum.collateralAsset,
  );

  const totalCdpDebt = cdpDatum.mintedAmt + interestAmt;

  const [isPartial, redemptionIAssetAmt] = (() => {
    const res = calculateMinCollateralCappedIAssetRedemptionAmt(
      collateralAmt,
      totalCdpDebt,
      wrongPriceOracleDatum.price,
      wrongCollateral.datum.redemptionRatio,
      wrongIasset.datum.redemptionReimbursementRatio,
      BigInt(wrongCollateral.datum.minCollateralAmt),
    );

    const redemptionAmt = bigintMin(
      cdp.datum.mintedAmt,
      res.cappedIAssetRedemptionAmt,
    );

    return [redemptionAmt < res.cappedIAssetRedemptionAmt, redemptionAmt];
  })();

  if (redemptionIAssetAmt <= 0) {
    throw new Error("There's no iAssets available for redemption.");
  }

  const redemptionCollateralAmt = rationalFloor(
    rationalMul(
      wrongPriceOracleDatum.price,
      rationalFromInt(redemptionIAssetAmt),
    ),
  );

  const processingFee = calculateFeeFromRatio(
    wrongIasset.datum.redemptionProcessingFeeRatio,
    redemptionCollateralAmt,
  );
  const reimburstmentFee = calculateFeeFromRatio(
    wrongIasset.datum.redemptionReimbursementRatio,
    redemptionCollateralAmt,
  );

  const txValidity = oracleExpirationAwareValidity(
    context.emulator.slot,
    Number(sysParams.cdpCreatorParams.biasTime),
    Number(wrongPriceOracleDatum.expirationTime),
    network,
  );

  const referenceInputs = [
    wrongIasset.utxo,
    wrongCollateral.utxo,
    wrongPriceOracleUtxo,
    wrongInterestOracleUtxo,
    govUtxo,
  ];

  const referenceScripts = [
    cdpRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
    cdpRedeemRefScriptUtxo,
  ];

  const tx = context.lucid.newTx().readFrom(referenceScripts);

  const interestCollectorRefScriptUtxo =
    interestAmt > 0n
      ? await collectInterestTx(
          mkAssetsOf(
            {
              currencySymbol: fromHex(
                sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
              ),
              tokenName: cdpDatum.iasset,
            },
            interestAmt,
          ),
          context.lucid,
          sysParams,
          tx,
          orefs.interestCollectorUtxo,
        )
      : undefined;

  // We need to take into account the interest collector ref script as well.
  const priceOracleIdx = getInputIndices(
    [wrongPriceOracleUtxo],
    [
      ...referenceInputs,
      ...referenceScripts,
      ...(interestCollectorRefScriptUtxo != null
        ? [interestCollectorRefScriptUtxo]
        : []),
    ],
  )[0];

  tx.readFrom(referenceInputs)
    // Trigger CDP Redeem Withdrawal validator
    .withdraw(
      credentialToRewardAddress(
        context.lucid.config().network!,
        scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash),
      ),
      0n,
      serialiseRedeemCdpWithdrawalRedeemer({
        cdpOutReference: {
          txHash: fromHex(cdpUtxo.txHash),
          outputIndex: BigInt(cdpUtxo.outputIndex),
        },
        currentTime: currentTime,
        priceOracleIdx: { OracleRefInputIdx: priceOracleIdx },
      }),
    )
    .validFrom(txValidity.validFrom)
    .validTo(txValidity.validTo)
    .collectFrom([cdpUtxo], serialiseCdpRedeemer('RedeemCdp'))
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: cdpDatum.iasset,
        },
        interestAmt - redemptionIAssetAmt,
      ),
      Data.void(),
    )
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: totalCdpDebt - redemptionIAssetAmt,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                wrongInterestOracleDatum.unitaryInterest +
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  wrongInterestOracleDatum,
                ),
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        mkAssetsOf(
          cdpDatum.collateralAsset,
          -redemptionCollateralAmt + reimburstmentFee,
        ),
      ),
    );

  const partialRedemptionFee = F.pipe(
    govDatum.protocolParams.cdpRedemptionRequiredSignature,
    O.fromNullable,
    O.match(
      // When public redemptions
      () => {
        return isPartial
          ? BigInt(sysParams.cdpRedeemParams.partialRedemptionExtraFeeLovelace)
          : 0n;
      },
      // When private redemptions
      (requiredSignature) => {
        tx.addSignerKey(toHex(requiredSignature));
        return 0n;
      },
    ),
  );

  //TODO: Use a treasury input to save on ADA.
  tx.pay.ToContract(
    credentialToAddress(context.lucid.config().network!, {
      hash: validatorToScriptHash(
        mkTreasuryValidatorFromSP(sysParams.treasuryParams),
      ),
      type: 'Script',
    }),
    { kind: 'inline', value: Data.void() },
    addAssets(
      mkAssetsOf(cdpDatum.collateralAsset, processingFee),
      mkLovelacesOf(partialRedemptionFee),
    ),
  );

  return tx;
}
