import { describe, expect, test } from 'vitest';
import {
  decodePriceUpdate,
  decodePythMessage,
  encodePriceUpdate,
  encodePythMessage,
  encodeSignedPythMessage,
  PRICE_UPDATE_MAGIC,
  SOLANA_FORMAT_MAGIC,
  type PriceUpdate,
  type PythMessageParts,
} from '../../src/utils/pyth';

/** Generate a 32-byte Ed25519 secret key for tests. */
function randomSecretKey(): Uint8Array {
  return crypto.getRandomValues(new Uint8Array(32));
}

describe('Pyth > Encoding', () => {
  test('encodePriceUpdate uses Aiken price_update format (magic + timestamp + channel u8 + feeds_len u8 + property-list feeds)', () => {
    const timestampUs = 1000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId: 1,
          exponent: -8,
          price: '50000000',
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 2,
          exponent: 0,
          price: '-100',
          feedUpdateTimestamp: Number(timestampUs + 1n),
        },
      ],
    };
    const payload = encodePriceUpdate(update);
    expect(Array.from(payload.subarray(0, 4))).toEqual(
      Array.from(PRICE_UPDATE_MAGIC),
    );
    // magic(4) + timestamp(8) + channel(1) + feeds_len(1) + 2 feeds
    // For this particular update, each feed encodes 13 properties:
    // Price, BestBidPrice, BestAskPrice, PublisherCount, Exponent, Confidence,
    // FundingRate, FundingTimestamp, FundingRateInterval, MarketSession,
    // EmaPrice, EmaConfidence, FeedUpdateTimestamp.
    // One feed layout:
    //   - feed_id u32 (4)
    //   - properties_len u8 (1)
    //   - properties bytes:
    //       Price:              1 (id) + 8 (i64)
    //       BestBidPrice:       1 (id) + 8 (i64)
    //       BestAskPrice:       1 (id) + 8 (i64)
    //       PublisherCount:     1 (id) + 2 (u16)
    //       Exponent:           1 (id) + 2 (i16)
    //       Confidence:         1 (id) + 8 (i64)
    //       FundingRate:        1 (id) + 1 (none tag)
    //       FundingTimestamp:   1 (id) + 1 (none tag)
    //       FundingRateInterval:1 (id) + 1 (none tag)
    //       MarketSession:      1 (id) + 2 (u16)
    //       EmaPrice:           1 (id) + 8 (i64)
    //       EmaConfidence:      1 (id) + 8 (i64)
    //       FeedUpdateTimestamp:1 (id) + 1 (some tag) + 8 (u64)
    const feedBytes =
      4 + // feed_id
      1 + // properties_len
      (1 + 8) + // Price
      (1 + 8) + // BestBidPrice
      (1 + 8) + // BestAskPrice
      (1 + 2) + // PublisherCount
      (1 + 2) + // Exponent
      (1 + 8) + // Confidence
      (1 + 1) + // FundingRate (None)
      (1 + 1) + // FundingTimestamp (None)
      (1 + 1) + // FundingRateInterval (None)
      (1 + 2) + // MarketSession
      (1 + 8) + // EmaPrice
      (1 + 8) + // EmaConfidence
      (1 + 1 + 8); // FeedUpdateTimestamp (Some + u64)
    expect(payload.length).toBe(4 + 8 + 1 + 1 + 2 * feedBytes);
  });

  test('encodeSignedPythMessage returns Pyth wire format (magic + signature + key + size + payload)', async () => {
    const secretKey = randomSecretKey();
    const timestampUs = 2000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId: 42,
          exponent: -9,
          price: '12345',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const message = await encodeSignedPythMessage(update, secretKey);
    const payload = encodePriceUpdate(update);
    expect(message.length).toBe(4 + 64 + 32 + 2 + payload.length);
    expect(Array.from(message.subarray(0, 4))).toEqual(
      Array.from(SOLANA_FORMAT_MAGIC),
    );
  });

  test('encodePythMessage produces correct layout (magic + sig + key + u16 len + payload)', async () => {
    const secretKey = randomSecretKey();
    const noble = await import('@noble/ed25519');
    const publicKey = await noble.getPublicKeyAsync(secretKey);
    const timestampUs = 1n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId: 0,
          exponent: 0,
          price: '0',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const payload = encodePriceUpdate(update);
    const signature = await noble.signAsync(payload, secretKey);
    const parts: PythMessageParts = { signature, publicKey, payload };
    const message = encodePythMessage(parts);
    expect(message.length).toBe(4 + 64 + 32 + 2 + payload.length);
    expect(Array.from(message.subarray(0, 4))).toEqual(
      Array.from(SOLANA_FORMAT_MAGIC),
    );
  });

  test('encodePriceUpdate supports full Feed properties', () => {
    const timestampUs = 1772613309000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 3,
      priceFeeds: [
        {
          priceFeedId: 1,
          price: '6950870030930',
          bestBidPrice: '6950841224625',
          bestAskPrice: '6950918797952',
          exponent: -8,
          marketSession: 'Regular',
          emaPrice: '6891705600000',
          emaConfidence: 6891641300000,
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const payload = encodePriceUpdate(update);
    expect(Array.from(payload.subarray(0, 4))).toEqual(
      Array.from(PRICE_UPDATE_MAGIC),
    );
    expect(payload.length).toBeGreaterThan(4 + 8 + 1 + 1);
  });

  // Encoding counterpart of Aiken parses_update() test.
  // Same subscription: priceFeedIds [1, 2, 112], properties [price, bestBidPrice, bestAskPrice,
  // exponent, fundingRate, fundingTimestamp, fundingRateInterval, marketSession, emaPrice,
  // emaConfidence, feedUpdateTimestamp], channel fixed_rate@200ms → channel_id 3.
  // Parsed values from streamUpdated.timestampUs and priceFeeds.
  test('encodePriceUpdate matches parses_update subscription (three feeds, full properties)', () => {
    const timestampUs = 1772613309000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 3,
      priceFeeds: [
        {
          priceFeedId: 1,
          price: '6950870030930',
          bestBidPrice: '6950841224625',
          bestAskPrice: '6950918797952',
          exponent: -8,
          marketSession: 'Regular',
          emaPrice: '6891705600000',
          emaConfidence: 6891641300000,
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 2,
          price: '201300331620',
          bestBidPrice: '201299371656',
          bestAskPrice: '201307527050',
          exponent: -8,
          marketSession: 'Regular',
          emaPrice: '199516364000',
          emaConfidence: 199513381000,
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 112,
          price: '6946927951449',
          exponent: -12,
          fundingRate: 2520,
          fundingTimestamp: 1772613308916265,
          fundingRateInterval: 28800000000,
          marketSession: 'Regular',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };

    const payload = encodePriceUpdate(update);

    expect(Array.from(payload.subarray(0, 4))).toEqual(
      Array.from(PRICE_UPDATE_MAGIC),
    );

    const view = new DataView(
      payload.buffer,
      payload.byteOffset,
      payload.byteLength,
    );
    const encodedTimestamp = view.getBigUint64(4, true);
    expect(encodedTimestamp).toBe(timestampUs);

    expect(payload[12]).toBe(3); // channel_id
    expect(payload[13]).toBe(3); // feeds_len

    // Feed IDs (u32 LE) appear in payload: 1 = 01 00 00 00, 2 = 02 00 00 00, 112 = 70 00 00 00
    const payloadBytes = Array.from(payload);
    const feed1Le = [1, 0, 0, 0];
    const feed2Le = [2, 0, 0, 0];
    const feed112Le = [112, 0, 0, 0];
    const findU32Le = (arr: number[], val: number[]) => {
      for (let i = 0; i <= arr.length - 4; i++) {
        if (
          arr[i] === val[0] &&
          arr[i + 1] === val[1] &&
          arr[i + 2] === val[2] &&
          arr[i + 3] === val[3]
        )
          return i;
      }
      return -1;
    };
    const idx1 = findU32Le(payloadBytes, feed1Le);
    const idx2 = findU32Le(payloadBytes, feed2Le);
    const idx112 = findU32Le(payloadBytes, feed112Le);
    expect(idx1).toBeGreaterThanOrEqual(14);
    expect(idx2).toBeGreaterThan(idx1);
    expect(idx112).toBeGreaterThan(idx2);
  });
});

describe('Pyth > Decoding', () => {
  test('decodePythMessage recovers parts from encodePythMessage', async () => {
    const secretKey = randomSecretKey();
    const noble = await import('@noble/ed25519');
    const publicKey = await noble.getPublicKeyAsync(secretKey);
    const timestampUs = 3000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 1,
      priceFeeds: [
        {
          priceFeedId: 99,
          exponent: -6,
          price: '42',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const payload = encodePriceUpdate(update);
    const signature = await noble.signAsync(payload, secretKey);
    const parts: PythMessageParts = { signature, publicKey, payload };
    const message = encodePythMessage(parts);
    const decoded = decodePythMessage(message);
    expect(decoded.payload.length).toBe(payload.length);
    expect(Array.from(decoded.signature)).toEqual(Array.from(signature));
    expect(Array.from(decoded.publicKey)).toEqual(Array.from(publicKey));
    expect(Array.from(decoded.payload)).toEqual(Array.from(payload));
  });

  test('decodePriceUpdate round-trips encodePriceUpdate (binary identity)', () => {
    const timestampUs = 1000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId: 1,
          exponent: -8,
          price: '50000000',
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 2,
          exponent: 0,
          price: '-100',
          feedUpdateTimestamp: Number(timestampUs + 1n),
        },
      ],
    };
    const encoded = encodePriceUpdate(update);
    const decoded = decodePriceUpdate(encoded);
    const again = encodePriceUpdate(decoded);
    expect(Array.from(again)).toEqual(Array.from(encoded));
  });

  test('decodePriceUpdate round-trips parses_update-style subscription payload', () => {
    const timestampUs = 1772613309000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 3,
      priceFeeds: [
        {
          priceFeedId: 1,
          price: '6950870030930',
          bestBidPrice: '6950841224625',
          bestAskPrice: '6950918797952',
          exponent: -8,
          marketSession: 'Regular',
          emaPrice: '6891705600000',
          emaConfidence: 6891641300000,
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 2,
          price: '201300331620',
          bestBidPrice: '201299371656',
          bestAskPrice: '201307527050',
          exponent: -8,
          marketSession: 'Regular',
          emaPrice: '199516364000',
          emaConfidence: 199513381000,
          feedUpdateTimestamp: Number(timestampUs),
        },
        {
          priceFeedId: 112,
          price: '6946927951449',
          exponent: -12,
          fundingRate: 2520,
          fundingTimestamp: 1772613308916265,
          fundingRateInterval: 28800000000,
          marketSession: 'Regular',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const encoded = encodePriceUpdate(update);
    const decoded = decodePriceUpdate(encoded);
    expect(decoded.timestampUs).toBe(update.timestampUs);
    expect(decoded.channelId).toBe(update.channelId);
    expect(decoded.priceFeeds.length).toBe(3);
    const again = encodePriceUpdate(decoded);
    expect(Array.from(again)).toEqual(Array.from(encoded));
  });

  test('encodeSignedPythMessage wire: decodePythMessage payload round-trips through decodePriceUpdate', async () => {
    const secretKey = randomSecretKey();
    const timestampUs = 4000000n;
    const update: PriceUpdate = {
      timestampUs: timestampUs.toString(),
      channelId: 0,
      priceFeeds: [
        {
          priceFeedId: 7,
          exponent: -4,
          price: '1',
          feedUpdateTimestamp: Number(timestampUs),
        },
      ],
    };
    const wire = await encodeSignedPythMessage(update, secretKey);
    const parts = decodePythMessage(wire);
    const payloadRoundTrip = encodePriceUpdate(
      decodePriceUpdate(parts.payload),
    );
    expect(Array.from(payloadRoundTrip)).toEqual(Array.from(parts.payload));
  });
});
