import { FeeNotLoaded } from "@ledgerhq/errors";
import type { TokenAccount } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import { buildOptimisticOperation } from "./buildOptimisticOperation";
import type { AlgorandAccount, AlgorandTransaction } from "./types";

jest.mock("@ledgerhq/ledger-wallet-framework/operation", () => ({
  encodeOperationId: jest.fn((accountId, hash, type) => `${accountId}--${type}`),
}));

describe("buildOptimisticOperation", () => {
  const createMockAccount = (balance: string, subAccounts: TokenAccount[] = []): AlgorandAccount =>
    ({
      id: "algorand-account-1",
      freshAddress: "ALGO_SENDER_ADDRESS",
      spendableBalance: new BigNumber(balance),
      balance: new BigNumber(balance),
      algorandResources: {
        rewards: new BigNumber("0"),
        nbAssets: subAccounts.length,
      },
      subAccounts,
    }) as unknown as AlgorandAccount;

  describe("basic operation creation", () => {
    it("should create operation with correct senders and recipients", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.senders).toEqual(["ALGO_SENDER_ADDRESS"]);
      expect(result.recipients).toEqual(["RECIPIENT_ADDRESS"]);
    });

    it("should set correct fee", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("2500"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.fee.toString()).toBe("2500");
    });

    it("should set accountId from account", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.accountId).toBe("algorand-account-1");
    });

    it("should set date to current time", () => {
      const before = new Date();
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);
      const after = new Date();

      expect(result.date.getTime()).toBeGreaterThanOrEqual(before.getTime());
      expect(result.date.getTime()).toBeLessThanOrEqual(after.getTime());
    });

    it("should have empty hash", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.hash).toBe("");
    });

    it("should have null blockHash and blockHeight", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.blockHash).toBeNull();
      expect(result.blockHeight).toBeNull();
    });
  });

  describe("operation type", () => {
    it("should be OUT for send mode", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.type).toBe("OUT");
    });

    it("should be OPT_IN for optIn mode", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "optIn",
        amount: new BigNumber("0"),
        recipient: "ALGO_SENDER_ADDRESS",
        fees: new BigNumber("1000"),
        assetId: "12345",
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.type).toBe("OPT_IN");
    });

    it("should be FEES for subAccount transaction", () => {
      const tokenAccount = {
        id: "token-account-1",
        type: "TokenAccount",
        balance: new BigNumber("1000"),
        token: { id: "algorand/asa/12345" },
      } as unknown as TokenAccount;

      const account = createMockAccount("10000000", [tokenAccount]);
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("100"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        subAccountId: "token-account-1",
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.type).toBe("FEES");
    });
  });

  describe("operation value", () => {
    it("should be amount + fees for regular send", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.value.toString()).toBe("1001000");
    });

    it("should be spendableBalance for useAllAmount", () => {
      const account = createMockAccount("5000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("0"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: true,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.value.toString()).toBe("5000000");
    });

    it("should be fees only for subAccount transaction", () => {
      const tokenAccount = {
        id: "token-account-1",
        type: "TokenAccount",
        balance: new BigNumber("500"),
        token: { id: "algorand/asa/12345" },
      } as unknown as TokenAccount;

      const account = createMockAccount("10000000", [tokenAccount]);
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("100"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("2000"),
        subAccountId: "token-account-1",
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.value.toString()).toBe("2000"); // Only fees
    });
  });

  describe("subOperations", () => {
    it("should include subOperation for token transfer", () => {
      const tokenAccount = {
        id: "token-account-1",
        type: "TokenAccount",
        balance: new BigNumber("1000"),
        token: { id: "algorand/asa/12345" },
      } as unknown as TokenAccount;

      const account = createMockAccount("10000000", [tokenAccount]);
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("500"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        subAccountId: "token-account-1",
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.subOperations).toHaveLength(1);
      expect(result.subOperations?.[0].type).toBe("OUT");
      expect(result.subOperations?.[0].value.toString()).toBe("500");
      expect(result.subOperations?.[0].accountId).toBe("token-account-1");
    });

    it("should use token balance for useAllAmount", () => {
      const tokenAccount = {
        id: "token-account-2",
        type: "TokenAccount",
        balance: new BigNumber("750"),
        token: { id: "algorand/asa/67890" },
      } as unknown as TokenAccount;

      const account = createMockAccount("10000000", [tokenAccount]);
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("0"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        subAccountId: "token-account-2",
        useAllAmount: true,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.subOperations?.[0].value.toString()).toBe("750");
    });

    it("should have zero fee for subOperation", () => {
      const tokenAccount = {
        id: "token-account-1",
        type: "TokenAccount",
        balance: new BigNumber("1000"),
        token: { id: "algorand/asa/12345" },
      } as unknown as TokenAccount;

      const account = createMockAccount("10000000", [tokenAccount]);
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("500"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        subAccountId: "token-account-1",
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.subOperations?.[0].fee.toString()).toBe("0");
    });

    it("should not include subOperations for regular send", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: new BigNumber("1000"),
        useAllAmount: false,
      };

      const result = buildOptimisticOperation(account, transaction);

      expect(result.subOperations).toBeUndefined();
    });
  });

  describe("error handling", () => {
    it("should throw FeeNotLoaded when fees are null", () => {
      const account = createMockAccount("10000000");
      const transaction: AlgorandTransaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: null,
        useAllAmount: false,
      };

      expect(() => buildOptimisticOperation(account, transaction)).toThrow(FeeNotLoaded);
    });

    it("should throw FeeNotLoaded when fees are undefined", () => {
      const account = createMockAccount("10000000");
      const transaction = {
        family: "algorand",
        mode: "send",
        amount: new BigNumber("1000000"),
        recipient: "RECIPIENT_ADDRESS",
        fees: undefined,
        useAllAmount: false,
      } as unknown as AlgorandTransaction;

      expect(() => buildOptimisticOperation(account, transaction)).toThrow(FeeNotLoaded);
    });
  });
});
