import { describe, it, expect, afterEach, afterAll, jest } from "@jest/globals";
import { ArbitrumEvmJsonRPCBlockRepository } from "../../../src/infrastructure/repositories";
import { MetadataRepository } from "../../../src/domain/repositories";
import { HttpClient } from "../../../src/infrastructure/rpc/http/HttpClient";
import { EvmTag } from "../../../src/domain/entities/evm";
import axios from "axios";
import nock from "nock";
import fs from "fs";

const dirPath = "./metadata-repo";
axios.defaults.adapter = "http"; // needed by nock
const ethereum = "ethereum";
const arbitrum = "arbitrum";
const rpc = "http://localhost";

let repo: ArbitrumEvmJsonRPCBlockRepository;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PersistedBlock[]>["save"]>;
let metadataGetSpy: jest.SpiedFunction<MetadataRepository<PersistedBlock[]>["get"]>;
let metadataRepo: MetadataRepository<PersistedBlock[]>;

describe("ArbitrumEvmJsonRPCBlockRepository", () => {
  afterAll(() => {
    nock.restore();
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath);
    }
  });

  afterEach(() => {
    nock.cleanAll();
    fs.rm(dirPath, () => {});
  });

  it("should be able to get block height with arbitrum latest commitment and eth finalized commitment", async () => {
    // Given
    const originalBlock = 19808090n;
    const expectedBlock = 157542621n;

    givenARepo();
    givenL2Block("latest");
    givenBlockHeightIs(originalBlock, "finalized");

    // When
    const result = await repo.getBlockHeight(arbitrum, "latest");

    // Then
    expect(result).toBe(expectedBlock);
  });

  it("should be throw error because unable to parse empty result for latest block", async () => {
    // Given
    givenARepo();
    nock(rpc)
      .post("/", {
        jsonrpc: "2.0",
        method: "eth_getBlockByNumber",
        params: ["latest", false],
        id: 1,
      })
      .reply(200, {
        jsonrpc: "2.0",
        id: 1,
        result: {},
      });

    try {
      // When
      await repo.getBlockHeight(arbitrum, "latest");
    } catch (e: Error | any) {
      // Then
      expect(e).toBeInstanceOf(Error);
    }
  });
});

const givenARepo = () => {
  repo = new ArbitrumEvmJsonRPCBlockRepository(
    {
      chains: {
        ethereum: { rpcs: [rpc], timeout: 100, name: ethereum, network: "mainnet", chainId: 2 },
        arbitrum: {
          rpcs: [rpc],
          timeout: 100,
          name: arbitrum,
          network: "mainnet",
          chainId: 23,
        },
      },
    },
    new HttpClient(),
    givenMetadataRepository([{ associatedL1Block: 18764852, l2BlockNumber: 157542621 }])
  );
};

const givenL2Block = (commitment: EvmTag) => {
  nock(rpc)
    .post("/", {
      jsonrpc: "2.0",
      method: "eth_getBlockByNumber",
      params: [commitment, false],
      id: 1,
    })
    .reply(200, {
      jsonrpc: "2.0",
      id: 1,
      result: {
        baseFeePerGas: "0x5f5e100",
        difficulty: "0x1",
        extraData: "0xe24ff00c699874b42c1eb6a325ae6c672c502c529de9dbb24ed9ff51563ab5ec",
        gasLimit: "0x4000000000000",
        gasUsed: "0x44a679",
        hash: "0xff1461564bad17aa8b047fc819f2d167b48e172f9143725815b2ec57b5aef429",
        l1BlockNumber: "0x11dcb25",
        logsBloom:
          "0x00000002000000100000000000020000800000000002000040000000004100000000000000000000000000000000000000005000020020000000000080200000000000010000004980404008000000000202000000000000000000000000004000000020000400000000100400000000040000100000000240000010000800000000000000080000000000000000000400000000000000080000000000000110030000000202000000000400800000000000000000800001000200000080400000000002001000000080001020040000000000000000000080000000000000008010000000000000000000440000000000004000000000000000000000000000",
        miner: "0xa4b000000000000000000073657175656e636572",
        mixHash: "0x00000000000184c900000000011dcb25000000000000000a0000000000000000",
        nonce: "0x000000000012b586",
        number: "0x963e8dd",
        parentHash: "0x648eff1f5f81b60f3bd9031114d54af71a1787f3f73e62fdbcf90cc228e976e9",
        receiptsRoot: "0x25a3340b2150c6db9bab6f82b8885d7f08a0ddbf06b4a5ab765c882a869fc594",
        sendCount: "0x184c9",
        sendRoot: "0xe24ff00c699874b42c1eb6a325ae6c672c502c529de9dbb24ed9ff51563ab5ec",
        sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
        size: "0x51f",
        stateRoot: "0x3ab24e412a3954b82927e92294e42ad73051ecf95e561a57c5bf6fcbb8114cc2",
        timestamp: "0x6570deb5",
        totalDifficulty: "0x8110b95",
        transactions: [
          "0xbfd339af3216f5a029e3cb96b75f0a09ef81ad49458af64c6f621d94476de2da",
          "0x3fb701d004eae54917c73e2008ca69e96faa46e8bc9631923170a3c88b19150f",
          "0x538622dae61bdfd20ddad66a3c06e49c94cba6bdca848a9320b9486b5dc1c8ad",
        ],
        transactionsRoot: "0xc9f0d07e2b5d4f0d33e28917fc9e8af0ad5e0558c7d885fd6f48034bccafff06",
      },
    });
};

const givenMetadataRepository = (data: PersistedBlock[]) => {
  metadataRepo = {
    get: () => Promise.resolve(data),
    save: () => Promise.resolve(),
  };
  metadataGetSpy = jest.spyOn(metadataRepo, "get");
  metadataSaveSpy = jest.spyOn(metadataRepo, "save");
  return metadataRepo;
};

const givenBlockHeightIs = (height: bigint, commitment: EvmTag) => {
  nock(rpc)
    .post("/", {
      jsonrpc: "2.0",
      method: "eth_getBlockByNumber",
      params: [commitment, false],
      id: 1,
    })
    .reply(200, {
      jsonrpc: "2.0",
      id: 1,
      result: {
        number: `0x${height.toString(16)}`,
        hash: blockHash(height),
        timestamp: "0x654a892f",
      },
    });
};

type PersistedBlock = {
  associatedL1Block: number;
  l2BlockNumber: number;
};

const blockHash = (blockNumber: bigint) => `0x${blockNumber.toString(16)}`;
