import BigNumber from "bignumber.js";
import { fetchTronAccountTxsPage, getBlock } from "../network";
import { fromTrongridTxInfoToOperation } from "../network/trongrid/trongrid-adapters";
import { TrongridTxInfo } from "../types";
import { listOperations, ListOperationsOptions } from "./listOperations";
import { TronEmptyPage } from "../types/errors";

jest.mock("../network", () => ({
  fetchTronAccountTxsPage: jest.fn(),
  getBlock: jest.fn(),
}));

jest.mock("../network/trongrid/trongrid-adapters", () => ({
  fromTrongridTxInfoToOperation: jest.fn(),
}));

describe("listOperations", () => {
  const mockAddress = "tronExampleAddress";

  const defaultOptions: ListOperationsOptions = {
    limit: 200,
    minTimestamp: 0,
    order: "asc",
  };

  beforeEach(() => {
    jest.clearAllMocks();
    (getBlock as jest.Mock).mockResolvedValue({
      height: 0,
      hash: "hash0",
      time: new Date("2023-01-01T00:00:00Z"),
    });
  });

  it("should fetch transactions and return operations with pagination", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx1",
        value: new BigNumber(0),
        date: new Date("2023-01-01T00:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx2",
        value: new BigNumber(42),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, defaultOptions);

    expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, {
      limit: 200,
      minTimestamp: 0,
      order: "asc",
    });
    expect(result.items).toHaveLength(2);
    expect(result.next).toBeUndefined();
  });

  it("should handle empty transactions on first page (no cursor)", async () => {
    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: [], hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });

    const result = await listOperations(mockAddress, defaultOptions);

    expect(result.items).toHaveLength(0);
    expect(result.next).toBeUndefined();
  });

  it("should throw TronEmptyPage when cursor is provided but TronGrid returns empty page", async () => {
    // TronGrid occasionally returns 0 results for a valid cursor (transient failure).
    // A cursor is only issued when hasNextPage=true, so this is never a legitimate end-of-stream.
    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: [], hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });

    const cursor = `${new Date("2023-01-01T01:00:00Z").getTime()}:tx1`;

    await expect(listOperations(mockAddress, { ...defaultOptions, cursor })).rejects.toThrow(
      TronEmptyPage,
    );
  });

  it("should return cursor when hasNextPage is true", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx1",
        value: new BigNumber(0),
        date: new Date("2023-01-01T00:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx2",
        value: new BigNumber(42),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 200,
      },
      {
        txID: "tx3",
        value: new BigNumber(50),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 300,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: true },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, defaultOptions);

    expect(result.items).toHaveLength(3);
    expect(result.next).toContain("tx3");
  });

  it("should merge and dedupe native and trc20 transactions", async () => {
    const nativeTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx1",
        value: new BigNumber(0),
        date: new Date("2023-01-01T00:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx3",
        value: new BigNumber(30),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 300,
      },
    ];
    const trc20Txs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 200,
      },
      {
        txID: "tx1",
        value: new BigNumber(0),
        date: new Date("2023-01-01T00:00:00Z"),
        blockHeight: 100,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: nativeTxs, hasNextPage: false },
      trc20Txs: { txs: trc20Txs, hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, defaultOptions);

    expect(result.items).toHaveLength(3);
    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["tx1", "tx2", "tx3"]);
  });

  it("should sort transactions by timestamp in ascending order", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx3",
        value: new BigNumber(30),
        date: new Date("2023-01-01T03:00:00Z"),
        blockHeight: 300,
      },
      {
        txID: "tx1",
        value: new BigNumber(10),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID, date: tx.date },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, { ...defaultOptions, order: "asc" });

    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["tx1", "tx2", "tx3"]);
  });

  it("should sort transactions by timestamp in descending order", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx1",
        value: new BigNumber(10),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx3",
        value: new BigNumber(30),
        date: new Date("2023-01-01T03:00:00Z"),
        blockHeight: 300,
      },
      {
        txID: "tx2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID, date: tx.date },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, { ...defaultOptions, order: "desc" });

    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["tx3", "tx2", "tx1"]);
  });

  it("should filter out transactions before cursor", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx1",
        value: new BigNumber(10),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "tx2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
      {
        txID: "tx3",
        value: new BigNumber(30),
        date: new Date("2023-01-01T03:00:00Z"),
        blockHeight: 300,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const cursorTimestamp = new Date("2023-01-01T01:00:00Z").getTime();
    const cursor = `${cursorTimestamp}:tx1`;

    const result = await listOperations(mockAddress, { ...defaultOptions, cursor });

    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["tx2", "tx3"]);
  });

  it("should filter by cursor position when multiple txs share same timestamp", async () => {
    const sameTimestamp = new Date("2023-01-01T01:00:00Z");
    const mockTxs: Partial<TrongridTxInfo>[] = [
      { txID: "txA", value: new BigNumber(10), date: sameTimestamp, blockHeight: 100 },
      { txID: "txB", value: new BigNumber(20), date: sameTimestamp, blockHeight: 100 },
      { txID: "txC", value: new BigNumber(30), date: sameTimestamp, blockHeight: 100 },
      {
        txID: "txD",
        value: new BigNumber(40),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const cursor = `${sameTimestamp.getTime()}:txB`;
    const result = await listOperations(mockAddress, { ...defaultOptions, cursor });

    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["txC", "txD"]);
  });

  it("should handle errors from fetchTronAccountTxsPage", async () => {
    const exampleError = new Error("Network error!");
    (fetchTronAccountTxsPage as jest.Mock).mockRejectedValue(exampleError);

    await expect(listOperations(mockAddress, defaultOptions)).rejects.toThrow("Network error!");
  });

  it("should throw on invalid cursor format", async () => {
    await expect(
      listOperations(mockAddress, { ...defaultOptions, cursor: "invalid" }),
    ).rejects.toThrow("Invalid cursor format");
  });

  it("should use boundary operation for next cursor when endpoints have different last ops", async () => {
    const nativeTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "native1",
        value: new BigNumber(10),
        date: new Date("2023-01-01T01:00:00Z"),
        blockHeight: 100,
      },
      {
        txID: "native2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T03:00:00Z"),
        blockHeight: 300,
      },
    ];
    const trc20Txs: Partial<TrongridTxInfo>[] = [
      {
        txID: "trc20-1",
        value: new BigNumber(15),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: nativeTxs, hasNextPage: true },
      trc20Txs: { txs: trc20Txs, hasNextPage: true },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const result = await listOperations(mockAddress, defaultOptions);

    expect(result.next).toContain("trc20-1");
    const hashes = result.items.map(op => op.tx.hash);
    expect(hashes).toEqual(["native1", "trc20-1"]);
  });

  it("should pass maxTimestamp from cursor for desc order pagination", async () => {
    const mockTxs: Partial<TrongridTxInfo>[] = [
      {
        txID: "tx3",
        value: new BigNumber(30),
        date: new Date("2023-01-01T03:00:00Z"),
        blockHeight: 300,
      },
      {
        txID: "tx2",
        value: new BigNumber(20),
        date: new Date("2023-01-01T02:00:00Z"),
        blockHeight: 200,
      },
    ];

    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: mockTxs, hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));

    const cursorTimestamp = new Date("2023-01-01T04:00:00Z").getTime();
    const cursor = `${cursorTimestamp}:tx4`;
    const minTimestamp = 1000;

    await listOperations(mockAddress, {
      ...defaultOptions,
      order: "desc",
      cursor,
      minTimestamp,
    });

    expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, {
      limit: 200,
      minTimestamp,
      maxTimestamp: cursorTimestamp,
      order: "desc",
    });
  });

  it("should not pass maxTimestamp for desc order without cursor", async () => {
    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: { txs: [], hasNextPage: false },
      trc20Txs: { txs: [], hasNextPage: false },
    });

    const minTimestamp = 1000;

    await listOperations(mockAddress, {
      ...defaultOptions,
      order: "desc",
      minTimestamp,
    });

    expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, {
      limit: 200,
      minTimestamp,
      maxTimestamp: undefined,
      order: "desc",
    });
  });

  it("should pass cursor timestamp as minTimestamp for asc order pagination", async () => {
    const cursorTimestamp = new Date("2023-01-01T02:00:00Z").getTime();
    const cursor = `${cursorTimestamp}:tx2`;
    (fetchTronAccountTxsPage as jest.Mock).mockResolvedValue({
      nativeTxs: {
        txs: [
          {
            txID: "tx3",
            value: new BigNumber(30),
            date: new Date("2023-01-01T03:00:00Z"),
            blockHeight: 300,
          },
        ],
        hasNextPage: false,
      },
      trc20Txs: { txs: [], hasNextPage: false },
    });
    (fromTrongridTxInfoToOperation as jest.Mock).mockImplementation(tx => ({
      tx: { hash: tx.txID },
      value: BigInt(tx.value.toString()),
    }));
    const minTimestamp = 1000;

    await listOperations(mockAddress, {
      ...defaultOptions,
      order: "asc",
      cursor,
      minTimestamp,
    });

    expect(fetchTronAccountTxsPage).toHaveBeenCalledWith(mockAddress, {
      limit: 200,
      minTimestamp: cursorTimestamp,
      maxTimestamp: undefined,
      order: "asc",
    });
  });
});
