import {
  credentialToRewardAddress,
  Data,
  Emulator,
  EmulatorAccount,
  fromHex,
  generateEmulatorAccount,
  Lucid,
  scriptHashToCredential,
  slotToUnixTime,
  toHex,
  validatorToScriptHash,
} from '@lucid-evolution/lucid';
import { beforeEach, describe, expect, test } from 'vitest';
import { LucidContext, runAndAwaitTxBuilder } from '../test-helpers';
import {
  initPyth,
  TEST_TRUSTED_SIGNER_PRIV_KEY,
  TEST_TRUSTED_SIGNER_PUB_KEY,
} from './endpoints';
import {
  PythFeedParams,
  PythStateDatum,
  serialisePythFeedRedeemer,
  serialisePythStateDatum,
  serialisePythUpdatesRedeemer,
  toDataDerivedPythPrice,
} from '../../src/contracts/pyth-feed/types';
import { assetClassToUnit } from '@3rd-eye-labs/cardano-offchain-common';
import { mkPythFeedValidator } from '../../src/contracts/pyth-feed/scripts';
import { alwaysSucceedValidator } from '../always-succeed/script';
import {
  encodePriceUpdate,
  encodePythMessage,
  type PriceUpdate,
  type PythMessageParts,
} from '../../src/utils/pyth';
import { MAINNET_PROTOCOL_PARAMETERS } from '../indigo-test-helpers';
import { createPythMessage } from './helpers';
import { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk';
import { derivePythPrice } from '../../src/contracts/pyth-feed/helpers';
import * as Core from '@evolution-sdk/evolution';

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

describe('Pyth > Initialization', () => {
  beforeEach<MyContext>(async (context: MyContext) => {
    context.users = {
      admin: generateEmulatorAccount({
        lovelace: BigInt(100_000_000_000_000),
      }),
    };

    context.emulator = new Emulator(
      [context.users.admin],
      MAINNET_PROTOCOL_PARAMETERS,
    );

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

  test('initPyth initializes the Pyth State NFT', async (context: MyContext) => {
    const pythStateAsset = await initPyth(context.lucid);

    expect(pythStateAsset).toBeDefined();
  });

  test('Properly serializes a Pyth State datum', (_context: MyContext) => {
    const pythStateDatum: PythStateDatum = {
      governance: {
        wormhole: fromHex(''),
        emitterChain: 0n,
        emitterAddress: fromHex(''),
        seenSequence: 0n,
      },
      trustedSigners: new Map([
        [
          fromHex(
            '4a50d7c3d16b2be5c16ba996109a455a34cce08a81b3e15b86ef407e2f72e71f',
          ),
          {
            lower_bound: {
              bound_type: {
                _tag: 'NegativeInfinity',
              },
              is_inclusive: true,
            },
            upper_bound: {
              bound_type: {
                _tag: 'Finite',
                finite: 1n,
              },
              is_inclusive: true,
            },
          },
        ],
      ]),
      deprecatedWithdrawScripts: new Map(),
      withdraw_script: fromHex(
        '7688145a4fa7aa18c16ef0daafeec18091ab99ea4268cfe2d863c79fda131938',
      ),
    };

    const serialisedPythStateDatum = serialisePythStateDatum(pythStateDatum);
    expect(serialisedPythStateDatum).toBe(
      'd8799fd8799f40004000ffa158204a50d7c3d16b2be5c16ba996109a455a34cce08a81b3e15b86ef407e2f72e71fd8799fd8799fd87980d87a80ffd8799fd87a9f01ffd87a80ffffa058207688145a4fa7aa18c16ef0daafeec18091ab99ea4268cfe2d863c79fda131938ff',
    );
  });

  test('Pyth Feed validator can confirm a Pyth Message', async (context: MyContext) => {
    const pythStateAsset = await initPyth(context.lucid);

    const noble = await import('@noble/ed25519');
    const priceFeedId = 1;
    const price = 1000n;
    const exponent = -8;

    const params: PythFeedParams = {
      pythStatePolicyId: pythStateAsset.currencySymbol,
      config: {
        Value: { configuration: { priceFeedId: BigInt(priceFeedId) } },
      },
    };

    const pfvValidator = mkPythFeedValidator(params);
    const pfvValHash = validatorToScriptHash(pfvValidator);

    // Register the stake key for pyth feed validator.
    await runAndAwaitTxBuilder(
      context.lucid,
      context.lucid.newTx().register.Stake(
        credentialToRewardAddress(context.lucid.config().network!, {
          hash: pfvValHash,
          type: 'Script',
        }),
      ),
    );

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

    // This is to test the transaction validity when using a timestamp different than the
    // slot boundary (which is the usual and general case).
    const timestamp = currentTime - 10n;

    const update: PriceUpdate = {
      timestampUs: (timestamp * 1_000n).toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId,
          price: price.toString(),
          exponent,
        },
      ],
    };
    const payload = encodePriceUpdate(update);
    const signature = await noble.signAsync(
      payload,
      fromHex(TEST_TRUSTED_SIGNER_PRIV_KEY),
    );
    const parts: PythMessageParts = {
      signature,
      publicKey: fromHex(TEST_TRUSTED_SIGNER_PUB_KEY),
      payload,
    };
    const message = encodePythMessage(parts);

    const pythStateUtxo = await context.lucid.utxoByUnit(
      assetClassToUnit(pythStateAsset),
    );

    await runAndAwaitTxBuilder(
      context.lucid,
      context.lucid
        .newTx()
        .readFrom([pythStateUtxo])
        .attach.Script(pfvValidator)
        .validFrom(Number(timestamp))
        .validTo(Number(timestamp) + 60 * 1000)
        .withdraw(
          credentialToRewardAddress(
            context.lucid.config().network!,
            scriptHashToCredential(pfvValHash),
          ),
          0n,
          serialisePythFeedRedeemer({
            price: {
              numerator: 1_000n,
              denominator: 100_000_000n,
            },
            auxiliaryData: Core.Data.fromCBORHex(Data.void()),
          }),
        )
        .withdraw(
          credentialToRewardAddress(
            context.lucid.config().network!,
            scriptHashToCredential(
              validatorToScriptHash(alwaysSucceedValidator),
            ),
          ),
          0n,
          serialisePythUpdatesRedeemer([message]),
        ),
    );
  });
});

describe('Pyth > Helper functions', () => {
  test('derivePythPrice works', async () => {
    const feed1: ParsedFeedPayload = {
      priceFeedId: 1,
      price: '1000',
      exponent: -8,
    };
    const feed2: ParsedFeedPayload = {
      priceFeedId: 2,
      price: '99',
      exponent: 0,
    };
    const feed3: ParsedFeedPayload = {
      priceFeedId: 3,
      price: '45',
      exponent: 0,
    };
    const message = await createPythMessage(
      [feed1, feed2, feed3],
      BigInt(Date.now()),
    );

    // Single direct value
    // 1000 * 10^-8 = 0.00001
    {
      const price = derivePythPrice(
        {
          Value: {
            configuration: { priceFeedId: 1n },
          },
        },
        toHex(message),
      );

      expect(price.numerator).toBe(1_000n);
      expect(price.denominator).toBe(100_000_000n);
    }

    // Single inverse value: 1 / (1000 * 10^-8) = 1 / 0.00001 = 100000
    {
      const price = derivePythPrice(
        {
          Inverse: {
            value: toDataDerivedPythPrice({
              Value: {
                configuration: {
                  priceFeedId: 1n,
                },
              },
            }),
          },
        },
        toHex(message),
      );

      expect(price.numerator).toBe(100_000_000n);
      expect(price.denominator).toBe(1_000n);
    }

    // Single divide value: 99 / 45 = 2.2
    {
      const price = derivePythPrice(
        {
          Divide: {
            x: toDataDerivedPythPrice({
              Value: {
                configuration: {
                  priceFeedId: 2n,
                },
              },
            }),
            y: toDataDerivedPythPrice({
              Value: {
                configuration: {
                  priceFeedId: 3n,
                },
              },
            }),
          },
        },
        toHex(message),
      );

      expect(price.numerator).toBe(99n);
      expect(price.denominator).toBe(45n);
    }
  });
});
