import { BigNumber } from "bignumber.js";
import * as network from "../network";
import { AlgoTransactionType } from "../network";
import { listOperations } from "./listOperations";

jest.mock("../network");

const mockGetAccountTransactions = network.getAccountTransactions as jest.MockedFunction<
  typeof network.getAccountTransactions
>;

describe("listOperations", () => {
  const address = "ALGO_ADDRESS_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("should return empty array when no transactions", async () => {
    mockGetAccountTransactions.mockResolvedValue({ transactions: [], nextToken: undefined });

    const { items, next } = await listOperations(address, { order: "desc" });

    expect(items).toEqual([]);
    expect(next).toBe("");
  });

  it("should convert payment transaction to OUT operation for sender", async () => {
    const tx = {
      id: "TX_123",
      type: AlgoTransactionType.PAYMENT,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000000,
      timestamp: "1700000000",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        amount: new BigNumber("1000000"),
        recipientAddress: "RECIPIENT_ADDRESS",
        closeToAddress: undefined,
        closeAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items).toHaveLength(1);
    expect(items[0].type).toBe("OUT");
    expect(items[0].value).toBe(1000000n);
    expect(items[0].senders).toEqual([address]);
    expect(items[0].recipients).toEqual(["RECIPIENT_ADDRESS"]);
    expect(items[0].asset).toEqual({ type: "native" });
  });

  it("should convert payment transaction to IN operation for recipient", async () => {
    const tx = {
      id: "TX_456",
      type: AlgoTransactionType.PAYMENT,
      senderAddress: "SENDER_ADDRESS",
      fee: new BigNumber("1000"),
      round: 1000001,
      timestamp: "1700000100",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        amount: new BigNumber("500000"),
        recipientAddress: address,
        closeToAddress: undefined,
        closeAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].type).toBe("IN");
    expect(items[0].value).toBe(500000n);
    expect(items[0].senders).toEqual(["SENDER_ADDRESS"]);
    expect(items[0].recipients).toEqual([address]);
    expect(items[0].asset).toEqual({ type: "native" });
  });

  it("should convert asset transfer to operation with asa type", async () => {
    const tx = {
      id: "TX_789",
      type: AlgoTransactionType.ASSET_TRANSFER,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000002,
      timestamp: "1700000200",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        assetAmount: new BigNumber("100"),
        assetRecipientAddress: "RECIPIENT_ADDRESS",
        assetId: "12345",
        assetCloseToAddress: undefined,
        assetCloseAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].type).toBe("OUT");
    expect(items[0].value).toBe(100n);
    expect(items[0].asset).toEqual({ type: "asa", assetReference: "12345" });
  });

  it("should detect OPT_IN operation", async () => {
    const tx = {
      id: "TX_OPTIN",
      type: AlgoTransactionType.ASSET_TRANSFER,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000003,
      timestamp: "1700000300",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        assetAmount: new BigNumber("0"),
        assetRecipientAddress: address, // Same as sender
        assetId: "67890",
        assetCloseToAddress: undefined,
        assetCloseAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].type).toBe("OPT_IN");
  });

  it("should detect OPT_OUT operation", async () => {
    const tx = {
      id: "TX_OPTOUT",
      type: AlgoTransactionType.ASSET_TRANSFER,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000004,
      timestamp: "1700000400",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        assetAmount: new BigNumber("50"),
        assetRecipientAddress: "RECIPIENT_ADDRESS",
        assetId: "11111",
        assetCloseToAddress: "CLOSE_ADDRESS",
        assetCloseAmount: new BigNumber("100"),
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].type).toBe("OPT_OUT");
  });

  it("should include memo when note is present", async () => {
    const tx = {
      id: "TX_MEMO",
      type: AlgoTransactionType.PAYMENT,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000005,
      timestamp: "1700000500",
      note: "Test memo",
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        amount: new BigNumber("1000000"),
        recipientAddress: "RECIPIENT_ADDRESS",
        closeToAddress: undefined,
        closeAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].details).toEqual({
      memo: {
        type: "string",
        kind: "note",
        value: "Test memo",
      },
    });
  });

  it("should include rewards in details when present", async () => {
    const tx = {
      id: "TX_REWARDS",
      type: AlgoTransactionType.PAYMENT,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000006,
      timestamp: "1700000600",
      note: undefined,
      senderRewards: new BigNumber("500"),
      recipientRewards: new BigNumber("300"),
      details: {
        amount: new BigNumber("1000000"),
        recipientAddress: "RECIPIENT_ADDRESS",
        closeToAddress: undefined,
        closeAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({ transactions: [tx], nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items[0].details).toEqual({ rewards: 800n });
  });

  it("should sort operations in descending order", async () => {
    const txs = [
      {
        id: "TX_1",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 1000,
        timestamp: "1700000000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("100"),
          recipientAddress: "R1",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
      {
        id: "TX_2",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 3000,
        timestamp: "1700002000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("200"),
          recipientAddress: "R2",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
      {
        id: "TX_3",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 2000,
        timestamp: "1700001000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("300"),
          recipientAddress: "R3",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
    ];

    mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: "NEXT_TOKEN" });

    const { items, next } = await listOperations(address, { order: "desc" });

    expect(items[0].tx.block.height).toBe(3000);
    expect(items[1].tx.block.height).toBe(2000);
    expect(items[2].tx.block.height).toBe(1000);
    expect(next).toBe("NEXT_TOKEN");
  });

  it("should sort operations in ascending order", async () => {
    const txs = [
      {
        id: "TX_1",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 3000,
        timestamp: "1700002000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("100"),
          recipientAddress: "R1",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
      {
        id: "TX_2",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 1000,
        timestamp: "1700000000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("200"),
          recipientAddress: "R2",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
    ];

    mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: undefined });

    const { items } = await listOperations(address, { order: "asc" });

    expect(items[0].tx.block.height).toBe(1000);
    expect(items[1].tx.block.height).toBe(3000);
  });

  it("should filter out non-payment/transfer transactions", async () => {
    const txs = [
      {
        id: "TX_PAY",
        type: AlgoTransactionType.PAYMENT,
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 1000,
        timestamp: "1700000000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {
          amount: new BigNumber("100"),
          recipientAddress: "R1",
          closeToAddress: undefined,
          closeAmount: undefined,
        },
      },
      {
        id: "TX_OTHER",
        type: "keyreg" as AlgoTransactionType, // Key registration
        senderAddress: address,
        fee: new BigNumber("1000"),
        round: 2000,
        timestamp: "1700001000",
        note: undefined,
        senderRewards: new BigNumber("0"),
        recipientRewards: new BigNumber("0"),
        details: {},
      },
    ];

    mockGetAccountTransactions.mockResolvedValue({ transactions: txs, nextToken: undefined });

    const { items } = await listOperations(address, { order: "desc" });

    expect(items).toHaveLength(1);
    expect(items[0].id).toBe("TX_PAY");
  });

  it("should forward pagination options to getAccountTransactions", async () => {
    mockGetAccountTransactions.mockResolvedValue({ transactions: [], nextToken: undefined });

    await listOperations(address, {
      order: "asc",
      minHeight: 5000,
      limit: 50,
      cursor: "PAGE_TOKEN",
    });

    expect(mockGetAccountTransactions).toHaveBeenCalledWith(address, {
      minRound: 5000,
      limit: 50,
      nextToken: "PAGE_TOKEN",
    });
  });

  it("should return next token from paginated response", async () => {
    const tx = {
      id: "TX_PAGE",
      type: AlgoTransactionType.PAYMENT,
      senderAddress: address,
      fee: new BigNumber("1000"),
      round: 1000,
      timestamp: "1700000000",
      note: undefined,
      senderRewards: new BigNumber("0"),
      recipientRewards: new BigNumber("0"),
      details: {
        amount: new BigNumber("100"),
        recipientAddress: "R1",
        closeToAddress: undefined,
        closeAmount: undefined,
      },
    };

    mockGetAccountTransactions.mockResolvedValue({
      transactions: [tx],
      nextToken: "CURSOR_ABC",
    });

    const { items, next } = await listOperations(address, { order: "asc" });

    expect(items).toHaveLength(1);
    expect(next).toBe("CURSOR_ABC");
  });

  it("should propagate network errors", async () => {
    mockGetAccountTransactions.mockRejectedValue(new Error("Network error"));

    await expect(listOperations(address, { order: "desc" })).rejects.toThrow("Network error");
  });
});
