import { mockRpcPool } from "../../mocks/mockRpcPool";
mockRpcPool();

import { afterAll, afterEach, describe, expect, it, jest } from "@jest/globals";
import { SuiClient } from "@mysten/sui.js/client";
import base58 from "bs58";
import { randomBytes } from "crypto";
import nock from "nock";
import { SuiJsonRPCBlockRepository } from "../../../src/infrastructure/repositories";

const rpc = "http://localhost";
let repo: SuiJsonRPCBlockRepository;

let txs: string[];
let checkpoints: string[];
let getTxsSpy: jest.SpiedFunction<SuiClient["multiGetTransactionBlocks"]>;
let getCheckpointsSpy: jest.SpiedFunction<SuiClient["getCheckpoints"]>;

const TX_BATCH_SIZE = 50;
const CHK_BATCH_SIZE = 100;

describe("SuiJsonRPCBlockRepository", () => {
  afterAll(() => {
    nock.restore();
  });

  afterEach(() => {
    nock.cleanAll();
  });

  it("should be able to get the latest checkpoint sequence", async () => {
    const expectedSeq = 23993824n;
    givenARepo();
    givenLastCheckpointIs(expectedSeq);

    const result = await repo.getLastCheckpointNumber();

    expect(result).toBe(expectedSeq);
  });

  it("should be able to fetch a set of transaction blocks", async () => {
    givenARepo();
    givenTransactions(2);

    const result = await repo.getTransactionBlockReceipts(txs);

    for (let i = 0; i < result.length; i++) {
      expect(result[i].digest).toBe(txs[i]);
      expect(result[i].timestampMs).toBe("1706107525474");
      expect(result[i].checkpoint).toBe("24408383");
      expect(result[i].events).toHaveLength(1);
      expect(result[i].effects?.status?.status).toBe("success");
      expect(result[i].transaction).not.toBeFalsy();
    }
  });

  it("should fetch transactions above the rpc limit", async () => {
    givenARepo();
    givenTransactions(123);

    const result = await repo.getTransactionBlockReceipts(txs);

    expect(result.length).toBe(123);
    expect(getTxsSpy).toHaveBeenCalledTimes(3);
  });

  it("should fetch a range of checkpoints", async () => {
    const range = { from: 200000n, to: 200049n };
    givenARepo();
    givenCheckpoints(range.from, range.to);

    const result = await repo.getCheckpoints(range);

    expect(result).toHaveLength(50);
    expect(getCheckpointsSpy).toHaveBeenCalledTimes(1);
  });

  it("should fetch cehckpoints above the rpc limit", async () => {
    const range = { from: 200000n, to: 200249n };
    givenARepo();
    givenCheckpoints(range.from, range.to);

    const result = await repo.getCheckpoints(range);

    expect(result).toHaveLength(250);
    expect(getCheckpointsSpy).toHaveBeenCalledTimes(3);
  });
});

const givenARepo = () => {
  const client = new SuiClient({ url: rpc });
  const pool = {
    get: () => client,
  };
  repo = new SuiJsonRPCBlockRepository(pool as any);

  getTxsSpy = jest.spyOn(client, "multiGetTransactionBlocks");
  getCheckpointsSpy = jest.spyOn(client, "getCheckpoints");
};

const givenLastCheckpointIs = (sequence: bigint) => {
  nock(rpc)
    .post("/", (body) => {
      // SuiClient inserts a GUID
      return body.method === "sui_getLatestCheckpointSequenceNumber" && body.params.length === 0;
    })
    .reply(200, {
      jsonrpc: "2.0",
      id: 1,
      result: sequence.toString(),
    });
};

const givenTransactions = (count: number) => {
  txs = [...new Array(count).keys()].map((_) => randomDigest());

  for (let i = 0; i < count / TX_BATCH_SIZE; i++) {
    nock(rpc)
      .post("/", (body) => {
        // SuiClient inserts a GUID
        return body.method === "sui_multiGetTransactionBlocks" && body.params.length === 2;
      })
      .reply(200, {
        jsonrpc: "2.0",
        id: 1,
        result: txs.slice(i * TX_BATCH_SIZE, (i + 1) * TX_BATCH_SIZE).map(mapTx),
      });
  }
};

const givenCheckpoints = (from: bigint, to: bigint) => {
  checkpoints = [...new Array(Number(to - from + 1n)).keys()].map((t) =>
    (from + BigInt(t)).toString()
  );

  for (let i = 0; i < checkpoints.length / CHK_BATCH_SIZE; i++) {
    nock(rpc)
      .post("/", (body) => {
        // SuiClient inserts a GUID
        return body.method === "sui_getCheckpoints";
      })
      .reply(200, {
        jsonrpc: "2.0",
        id: 1,
        result: {
          data: checkpoints.slice(i * CHK_BATCH_SIZE, (i + 1) * CHK_BATCH_SIZE).map(mapCheckpoint),
          cursor: "",
          hasNext: false,
        },
      });
  }
};

const mapTx = (digest: string) => ({
  digest: digest,
  transaction: {
    data: {
      messageVersion: "v1",
      transaction: {
        kind: "ProgrammableTransaction",
        inputs: [],
        transactions: [],
      },
      sender: "0xfcda48b391b8a1c6a9e57f30247bc0d5a97595f4a61784078ad17e11c2a8d529",
      gasData: {
        payment: [
          {
            objectId: "0xf0405f2f6e2ef6f86762f973907faeb68e41f8a3fb00326bb58dea73701209f7",
            version: "63608463",
            digest: "CoLRg7oKMJ3T3p3X9yTWGpiNPJL3SvdkQJLbXetfVVYe",
          },
        ],
        owner: "0xfcda48b391b8a1c6a9e57f30247bc0d5a97595f4a61784078ad17e11c2a8d529",
        price: "750",
        budget: "7690416",
      },
    },
    txSignatures: [
      "AFEXiY7OGpEFtlU4YZ/K6IktslWzqRfSqP8e90V+Pqowv8emUBDg875hoDxLU+hRqPPShBDqfTy6IzOZeNSQpQrPyLwQgQtt1dF1BjINPA76mVZwoYzzB1KhblrBFvgl/Q==",
    ],
  },
  effects: { status: { status: "success" } },
  events: [{}],
  timestampMs: "1706107525474",
  checkpoint: "24408383",
});

const randomDigest = () => base58.encode(randomBytes(32));

const mapCheckpoint = (height: string) => ({
  epoch: "285",
  sequenceNumber: height,
  digest: randomDigest(),
  networkTotalTransactions: "1063849022",
  previousDigest: randomDigest(),
  epochRollingGasCostSummary: {
    computationCost: "169602918500",
    storageCost: "1234564033600",
    storageRebate: "1198647002916",
    nonRefundableStorageFee: "12107545484",
  },
  timestampMs: "1705946928610",
  transactions: [],
  checkpointCommitments: [],
  validatorSignature: "i9kUhaIs2SOZFMqYgAOf9rjZJs8lVWGUlxJwsiGcwW8Eg7u4EfWGHjJulg6ZwMWb",
});
