import { DeviceModelId } from "@ledgerhq/devices";
import type { Account } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import specs from "./specs";
import type { AlgorandAccount } from "./types";

// Mock dependencies
jest.mock("@ledgerhq/ledger-wallet-framework/account", () => ({
  isAccountEmpty: jest.fn((account: Account) => account.balance?.isZero() ?? true),
}));

jest.mock("@ledgerhq/ledger-wallet-framework/bot/specs", () => ({
  botTest: jest.fn((name, fn) => fn()),
  genericTestDestination: jest.fn(),
  pickSiblings: jest.fn(siblings => siblings[0]),
}));

jest.mock("@ledgerhq/cryptoassets/index", () => ({
  getCryptoCurrencyById: jest.fn(() => ({
    id: "algorand",
    ticker: "ALGO",
    units: [{ code: "ALGO", magnitude: 6 }],
  })),
}));

jest.mock("@ledgerhq/coin-module-framework/currencies", () => ({
  parseCurrencyUnit: jest.fn(() => new BigNumber("100000")),
}));

jest.mock("./speculos-deviceActions", () => ({
  acceptTransaction: jest.fn(),
}));

describe("specs", () => {
  const { algorand } = specs;

  describe("algorand AppSpec", () => {
    it("should have correct name", () => {
      expect(algorand.name).toBe("Algorand");
    });

    it("should have currency defined", () => {
      expect(algorand.currency).not.toBeUndefined();
      expect(algorand.currency.id).toBe("algorand");
    });

    it("should have appQuery for nanoS", () => {
      expect(algorand.appQuery).toEqual({
        model: DeviceModelId.nanoS,
        appName: "Algorand",
      });
    });

    it("should have genericDeviceAction", () => {
      expect(algorand.genericDeviceAction).not.toBeUndefined();
    });

    it("should have mutations array", () => {
      expect(algorand.mutations).not.toBeUndefined();
      expect(Array.isArray(algorand.mutations)).toBe(true);
    });
  });

  describe("mutations", () => {
    const findMutation = (name: string) => algorand.mutations.find(m => m.name === name);

    describe("move ~50%", () => {
      const mutation = findMutation("move ~50%");

      it("should exist", () => {
        expect(mutation).not.toBeUndefined();
      });

      it("should have feature 'send'", () => {
        expect(mutation?.feature).toBe("send");
      });

      it("should have testDestination", () => {
        expect(mutation?.testDestination).not.toBeUndefined();
      });

      it("should create transaction with amount ~50% of maxSpendable", () => {
        const account = { freshAddress: "SENDER" } as Account;
        const sibling = { freshAddress: "RECIPIENT", balance: new BigNumber("1000000") } as Account;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("2000000");

        const result = mutation?.transaction?.({
          account,
          siblings: [sibling],
          bridge,
          maxSpendable,
        } as never);

        expect(result?.transaction).not.toBeUndefined();
        expect(result?.updates).toHaveLength(2);
        expect(result?.updates[0].amount).not.toBeUndefined();
        expect(result?.updates[1].recipient).toBe("RECIPIENT");
      });

      it("should have test function", () => {
        expect(mutation?.test).not.toBeUndefined();
      });
    });

    describe("send max", () => {
      const mutation = findMutation("send max");

      it("should exist", () => {
        expect(mutation).not.toBeUndefined();
      });

      it("should have feature 'sendMax'", () => {
        expect(mutation?.feature).toBe("sendMax");
      });

      it("should create transaction with useAllAmount true", () => {
        const account = { freshAddress: "SENDER" } as Account;
        const sibling = { freshAddress: "RECIPIENT", balance: new BigNumber("1000000") } as Account;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("2000000");

        const result = mutation?.transaction?.({
          account,
          siblings: [sibling],
          bridge,
          maxSpendable,
        } as never);

        expect(result?.updates).toContainEqual({ useAllAmount: true });
      });

      it("should have test function that checks spendable balance is low", () => {
        expect(mutation?.test).not.toBeUndefined();
      });
    });

    describe("send ASA ~50%", () => {
      const mutation = findMutation("send ASA ~50%");

      it("should exist", () => {
        expect(mutation).not.toBeUndefined();
      });

      it("should have feature 'tokens'", () => {
        expect(mutation?.feature).toBe("tokens");
      });

      it("should have test function", () => {
        expect(mutation?.test).not.toBeUndefined();
      });
    });

    describe("opt-In ASA available", () => {
      const mutation = findMutation("opt-In ASA available");

      it("should exist", () => {
        expect(mutation).not.toBeUndefined();
      });

      it("should have feature 'tokens'", () => {
        expect(mutation?.feature).toBe("tokens");
      });

      it("should have test function", () => {
        expect(mutation?.test).not.toBeUndefined();
      });
    });

    describe("claim rewards", () => {
      const mutation = findMutation("claim rewards");

      it("should exist", () => {
        expect(mutation).not.toBeUndefined();
      });

      it("should have feature 'staking'", () => {
        expect(mutation?.feature).toBe("staking");
      });

      it("should create transaction with claimReward mode", () => {
        const account = {
          freshAddress: "SENDER",
          algorandResources: {
            rewards: new BigNumber("1000"),
            nbAssets: 0,
          },
        } as AlgorandAccount;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("1000000");

        const result = mutation?.transaction?.({
          account,
          bridge,
          maxSpendable,
        } as never);

        expect(result?.updates).toContainEqual({ mode: "claimReward" });
      });

      it("should have test function that checks rewards are zero", () => {
        expect(mutation?.test).not.toBeUndefined();
      });
    });
  });

  describe("mutation count", () => {
    it("should have 5 mutations", () => {
      expect(algorand.mutations).toHaveLength(5);
    });

    it("should have all expected mutations", () => {
      const mutationNames = algorand.mutations.map(m => m.name);
      expect(mutationNames).toContain("move ~50%");
      expect(mutationNames).toContain("send max");
      expect(mutationNames).toContain("send ASA ~50%");
      expect(mutationNames).toContain("opt-In ASA available");
      expect(mutationNames).toContain("claim rewards");
    });
  });

  describe("helper functions behavior", () => {
    describe("move ~50% mutation transaction", () => {
      const mutation = algorand.mutations.find(m => m.name === "move ~50%");

      it("should throw when maxSpendable is 0", () => {
        const account = { freshAddress: "SENDER" } as Account;
        const sibling = { freshAddress: "RECIPIENT", balance: new BigNumber("1000000") } as Account;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("0");

        expect(() =>
          mutation?.transaction?.({
            account,
            siblings: [sibling],
            bridge,
            maxSpendable,
          } as never),
        ).toThrow("Spendable balance is too low");
      });
    });

    describe("send max mutation transaction", () => {
      const mutation = algorand.mutations.find(m => m.name === "send max");

      it("should throw when maxSpendable is 0", () => {
        const account = { freshAddress: "SENDER" } as Account;
        const sibling = { freshAddress: "RECIPIENT", balance: new BigNumber("1000000") } as Account;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("0");

        expect(() =>
          mutation?.transaction?.({
            account,
            siblings: [sibling],
            bridge,
            maxSpendable,
          } as never),
        ).toThrow("Spendable balance is too low");
      });
    });

    describe("claim rewards mutation transaction", () => {
      const mutation = algorand.mutations.find(m => m.name === "claim rewards");

      it("should throw when no pending rewards", () => {
        const account = {
          freshAddress: "SENDER",
          algorandResources: {
            rewards: new BigNumber("0"),
            nbAssets: 0,
          },
        } as AlgorandAccount;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("1000000");

        expect(() =>
          mutation?.transaction?.({
            account,
            bridge,
            maxSpendable,
          } as never),
        ).toThrow("No pending rewards");
      });

      it("should throw when maxSpendable is 0", () => {
        const account = {
          freshAddress: "SENDER",
          algorandResources: {
            rewards: new BigNumber("1000"),
            nbAssets: 0,
          },
        } as AlgorandAccount;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("0");

        expect(() =>
          mutation?.transaction?.({
            account,
            bridge,
            maxSpendable,
          } as never),
        ).toThrow("Spendable balance is too low");
      });
    });

    describe("opt-In ASA mutation transaction", () => {
      const mutation = algorand.mutations.find(m => m.name === "opt-In ASA available");

      it("should throw when maxSpendable is too low for opt-in", () => {
        const account = {
          freshAddress: "SENDER",
          subAccounts: [],
        } as unknown as Account;
        const bridge = {
          createTransaction: jest.fn(() => ({ family: "algorand" })),
        };
        const maxSpendable = new BigNumber("50000"); // Less than 100000 required

        expect(() =>
          mutation?.transaction?.({
            account,
            bridge,
            maxSpendable,
          } as never),
        ).toThrow("Spendable balance is too low");
      });
    });
  });

  describe("test functions", () => {
    describe("move ~50% test", () => {
      const mutation = algorand.mutations.find(m => m.name === "move ~50%");

      it("should validate balance change", () => {
        const account = { balance: new BigNumber("4000000") } as Account;
        const accountBeforeTransaction = {
          balance: new BigNumber("5000000"),
          algorandResources: { rewards: new BigNumber("0") },
        } as AlgorandAccount;
        const operation = { value: new BigNumber("1000000") };

        expect(() =>
          mutation?.test?.({
            account,
            accountBeforeTransaction,
            operation,
          } as never),
        ).not.toThrow();
      });
    });

    describe("send max test", () => {
      const mutation = algorand.mutations.find(m => m.name === "send max");

      it("should validate spendable balance is low", () => {
        const account = { spendableBalance: new BigNumber("10") } as Account;

        expect(() =>
          mutation?.test?.({
            account,
          } as never),
        ).not.toThrow();
      });
    });

    describe("claim rewards test", () => {
      const mutation = algorand.mutations.find(m => m.name === "claim rewards");

      it("should validate rewards are zero after claim", () => {
        const account = {
          algorandResources: { rewards: new BigNumber("0") },
        } as AlgorandAccount;

        expect(() =>
          mutation?.test?.({
            account,
          } as never),
        ).not.toThrow();
      });
    });
  });
});
