import {
  addAssets,
  Emulator,
  EmulatorAccount,
  generateEmulatorAccount,
  Lucid,
  paymentCredentialOf,
} from '@lucid-evolution/lucid';
import { beforeEach, describe, test } from 'vitest';
import {
  LucidContext,
  runAndAwaitTx,
  runAndAwaitTxBuilder,
} from '../test-helpers';
import { mkLovelacesOf } from '@3rd-eye-labs/cardano-offchain-common';
import {
  feedPriceOracleTx,
  PriceOracleParams,
  startPriceOracleTx,
} from '../../src';
import { expectScriptFailure } from '../utils/asserts';
import { feedPriceOracleTxStealNft } from './transactions-mutated';
import * as Core from '@evolution-sdk/evolution';
import { rationalFromInt } from '../../src/types/rational';

type MyContext = LucidContext<{
  admin: EmulatorAccount;
}>;

describe('Rational Price Oracle', () => {
  beforeEach<MyContext>(async (context: MyContext) => {
    context.users = {
      admin: generateEmulatorAccount(
        addAssets(mkLovelacesOf(100_000_000_000_000n)),
      ),
    };

    context.emulator = new Emulator(Object.values(context.users));
    context.lucid = await Lucid(context.emulator, 'Custom');
    context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
  });

  test<MyContext>('Start Rational Price Oracle', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [tx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    await runAndAwaitTxBuilder(context.lucid, tx);
  });

  test<MyContext>('Feed Rational Price Oracle', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    const feedTx = await feedPriceOracleTx(
      context.lucid,
      { txHash: startTxHash, outputIndex: 0 },
      rationalFromInt(2n),
      params,
      context.emulator.slot,
    );

    await runAndAwaitTxBuilder(context.lucid, feedTx);
  });

  test<MyContext>('Feed Rational Price Oracle cannot be negative', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    await expectScriptFailure(
      'Price must be positive',
      feedPriceOracleTx(
        context.lucid,
        { txHash: startTxHash, outputIndex: 0 },
        rationalFromInt(-2n),
        params,
        context.emulator.slot,
      ),
    );
  });

  test<MyContext>('Feed Rational Price Oracle cannot be steal NFT', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    await expectScriptFailure(
      'Wrong continuing output',
      feedPriceOracleTxStealNft(
        context.lucid,
        { txHash: startTxHash, outputIndex: 0 },
        rationalFromInt(2n),
        params,
        context.emulator.slot,
      ),
    );
  });

  test<MyContext>('Feed Rational Price Oracle cannot have zero denominator', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    await expectScriptFailure(
      'Denominator cannot be zero',
      feedPriceOracleTx(
        context.lucid,
        { txHash: startTxHash, outputIndex: 0 },
        { numerator: 2n, denominator: 0n },
        params,
        context.emulator.slot,
      ),
    );
  });

  test<MyContext>('Feed Rational Price Oracle with large number', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    await runAndAwaitTx(
      context.lucid,
      feedPriceOracleTx(
        context.lucid,
        { txHash: startTxHash, outputIndex: 0 },
        {
          numerator:
            BigInt(Number.MAX_SAFE_INTEGER) * BigInt(Number.MAX_SAFE_INTEGER),
          denominator: BigInt(Number.MAX_SAFE_INTEGER),
        },
        params,
        context.emulator.slot,
      ),
    );
  });

  test<MyContext>('Feed Rational Price Oracle can change auxiliary data', async (context: MyContext) => {
    const pkh = paymentCredentialOf(context.users.admin.address).hash;
    const params: PriceOracleParams = {
      owner: pkh,
      biasTime: 20_000n,
      expirationPeriod: 1_800_000n,
    };
    const [startTx, _] = await startPriceOracleTx(
      context.lucid,
      'ORACLE_NFT',
      rationalFromInt(1n),
      params,
      context.emulator.slot,
    );

    const startTxHash = await runAndAwaitTxBuilder(context.lucid, startTx);

    await runAndAwaitTx(
      context.lucid,
      feedPriceOracleTx(
        context.lucid,
        { txHash: startTxHash, outputIndex: 0 },
        rationalFromInt(1n),
        params,
        context.emulator.slot,
        Core.Data.fromCBORHex(
          'd8799f1b0180e51d1ae19514d8799f1a00030d40ff1b00000194ce33c598ff',
        ),
      ),
    );
  });
});
