import { lastValueFrom } from "rxjs";
import { toArray } from "rxjs/operators";
import { genericSignOperation } from "../signOperation";
import { FeeNotLoaded } from "@ledgerhq/errors";
import { getAlpacaApi } from "../alpaca";
import { buildOptimisticOperation, transactionToIntent } from "../utils";

jest.mock("../alpaca", () => ({
  getAlpacaApi: jest.fn(),
}));

jest.mock("../utils", () => ({
  ...jest.requireActual("../utils"),
  buildOptimisticOperation: jest.fn(),
  transactionToIntent: jest.fn(),
}));

describe("genericSignOperation", () => {
  const mockSignerContext = jest.fn();
  const mockSigner = {
    getAddress: jest.fn(),
    signTransaction: jest.fn(),
  };

  const transaction = {
    amount: 100_000n,
    fees: 500n,
    tag: 1234,
    recipientDomain: {
      domain: "recipient.gen",
      address: "recipient-address",
    },
  } as any;

  const txIntent = {
    memo: {
      type: "map",
      memos: new Map(),
    },
  };

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

    (getAlpacaApi as jest.Mock).mockReturnValue({
      craftTransaction: jest.fn().mockResolvedValue({ transaction: "unsignedTx" }),
      getAccountInfo: jest.fn().mockResolvedValue("pubKey"),
      combine: jest.fn().mockResolvedValue("signedTx"),
      getNextSequence: jest.fn().mockResolvedValue(1n),
    });

    (transactionToIntent as jest.Mock).mockReturnValue(txIntent);
    (buildOptimisticOperation as jest.Mock).mockReturnValue({ id: "mock-op" });

    mockSigner.getAddress.mockResolvedValue({ publicKey: "pubKey" });
    mockSigner.signTransaction.mockResolvedValue("sig");
    mockSignerContext.mockImplementation(async (_deviceId, cb) => cb(mockSigner));
  });

  const account = {
    freshAddressPath: "44'/144'/0'/0/0",
    address: "rTestAddress",
    currency: { id: "testnet" },
  } as any;

  it("emits full sign operation flow", async () => {
    const signOperation = genericSignOperation("testnet", "local")(mockSignerContext);
    const observable = signOperation({ account, transaction, deviceId: "" });

    const events = await lastValueFrom(observable.pipe(toArray()));

    expect(events[0]).toEqual({ type: "device-signature-requested" });
    expect(events[1]).toEqual({ type: "device-signature-granted" });
    expect(events[2]).toEqual({
      type: "signed",
      signedOperation: {
        operation: { id: "mock-op" },
        signature: "signedTx",
      },
    });

    expect(transactionToIntent).toHaveBeenCalledWith(account, transaction, undefined);
    expect(mockSigner.signTransaction).toHaveBeenCalledWith("44'/144'/0'/0/0", "unsignedTx", {
      domain: "recipient.gen",
      address: "recipient-address",
    });
    expect(txIntent.memo.memos.get("destinationTag")).toBe("1234");
  });

  it("throws FeeNotLoaded if fees are missing", async () => {
    const txWithoutFees = { ...transaction };
    delete txWithoutFees.fees;

    const signOperation = genericSignOperation("testnet", "local")(mockSignerContext);
    const observable = signOperation({ account, transaction: txWithoutFees, deviceId: "" });

    await expect(lastValueFrom(observable)).rejects.toThrow(FeeNotLoaded);
  });
});
