import { FeeNotLoaded } from "@ledgerhq/errors";
import type { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
import { BigNumber } from "bignumber.js";
import * as buildOptimisticOperationModule from "./buildOptimisticOperation";
import * as buildTransactionModule from "./buildTransaction";
import { buildSignOperation } from "./signOperation";
import type { AlgorandSigner } from "./signer";
import type { AlgorandAccount, AlgorandTransaction, AlgorandOperation } from "./types";

jest.mock("./buildTransaction");
jest.mock("./buildOptimisticOperation");

const mockBuildTransactionPayload =
  buildTransactionModule.buildTransactionPayload as jest.MockedFunction<
    typeof buildTransactionModule.buildTransactionPayload
  >;

const mockEncodeToSign = buildTransactionModule.encodeToSign as jest.MockedFunction<
  typeof buildTransactionModule.encodeToSign
>;

const mockEncodeToBroadcast = buildTransactionModule.encodeToBroadcast as jest.MockedFunction<
  typeof buildTransactionModule.encodeToBroadcast
>;

const mockBuildOptimisticOperation =
  buildOptimisticOperationModule.buildOptimisticOperation as jest.MockedFunction<
    typeof buildOptimisticOperationModule.buildOptimisticOperation
  >;

describe("signOperation", () => {
  const mockAccount: AlgorandAccount = {
    id: "algorand-account-1",
    freshAddress: "ALGO_ADDRESS",
    freshAddressPath: "44'/283'/0'/0/0",
    algorandResources: {
      rewards: new BigNumber("0"),
      nbAssets: 0,
    },
  } as unknown as AlgorandAccount;

  const mockTransaction: AlgorandTransaction = {
    family: "algorand",
    mode: "send",
    amount: new BigNumber("1000000"),
    recipient: "RECIPIENT_ADDRESS",
    fees: new BigNumber("1000"),
  };

  const mockOperation: AlgorandOperation = {
    id: "op-1",
    hash: "",
    type: "OUT",
    value: new BigNumber("1001000"),
    fee: new BigNumber("1000"),
    senders: ["ALGO_ADDRESS"],
    recipients: ["RECIPIENT_ADDRESS"],
    accountId: "algorand-account-1",
    date: new Date(),
    blockHash: null,
    blockHeight: null,
    extra: {},
  };

  const mockSignature = Buffer.from("mock_signature");

  const mockSignerContext: SignerContext<AlgorandSigner> = jest.fn().mockResolvedValue({
    signature: mockSignature,
  });

  beforeEach(() => {
    jest.clearAllMocks();
    mockBuildTransactionPayload.mockResolvedValue({} as never);
    mockEncodeToSign.mockReturnValue("encoded_tx_hex");
    mockEncodeToBroadcast.mockReturnValue(Buffer.from("broadcast_payload"));
    mockBuildOptimisticOperation.mockReturnValue(mockOperation);
  });

  describe("buildSignOperation", () => {
    it("should return a function that creates an Observable", () => {
      const signOperation = buildSignOperation(mockSignerContext);

      expect(typeof signOperation).toBe("function");

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      expect(observable).not.toBeUndefined();
      expect(typeof observable.subscribe).toBe("function");
    });

    it("should throw FeeNotLoaded when fees are not set", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const transactionWithoutFees: AlgorandTransaction = {
        ...mockTransaction,
        fees: null,
      };

      const observable = signOperation({
        account: mockAccount,
        transaction: transactionWithoutFees,
        deviceId: "device-1",
      });

      observable.subscribe({
        error: err => {
          expect(err).toBeInstanceOf(FeeNotLoaded);
          done();
        },
        complete: () => {
          done.fail("Should have thrown FeeNotLoaded");
        },
      });
    });

    it("should emit device-signature-requested event", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const events: string[] = [];

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        next: event => {
          events.push(event.type);
        },
        complete: () => {
          expect(events).toContain("device-signature-requested");
          done();
        },
        error: done.fail,
      });
    });

    it("should emit device-signature-granted event after signing", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const events: string[] = [];

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        next: event => {
          events.push(event.type);
        },
        complete: () => {
          expect(events).toContain("device-signature-granted");
          done();
        },
        error: done.fail,
      });
    });

    it("should emit signed event with signedOperation", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      let signedEvent: { type: string; signedOperation?: unknown } | undefined;

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        next: event => {
          if (event.type === "signed") {
            signedEvent = event;
          }
        },
        complete: () => {
          expect(signedEvent).not.toBeUndefined();
          expect(signedEvent?.signedOperation).toHaveProperty("operation");
          expect(signedEvent?.signedOperation).toHaveProperty("signature");
          done();
        },
        error: done.fail,
      });
    });

    it("should call signerContext with correct parameters", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-123",
      });

      observable.subscribe({
        complete: () => {
          expect(mockSignerContext).toHaveBeenCalledWith("device-123", expect.any(Function));
          done();
        },
        error: done.fail,
      });
    });

    it("should build transaction payload and encode for signing", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        complete: () => {
          expect(mockBuildTransactionPayload).toHaveBeenCalledWith(mockAccount, mockTransaction);
          expect(mockEncodeToSign).toHaveBeenCalled();
          done();
        },
        error: done.fail,
      });
    });

    it("should throw error when signature is missing", done => {
      const signerWithoutSignature: SignerContext<AlgorandSigner> = jest.fn().mockResolvedValue({
        signature: null,
      });

      const signOperation = buildSignOperation(signerWithoutSignature);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        error: err => {
          expect(err.message).toBe("No signature");
          done();
        },
        complete: () => {
          done.fail("Should have thrown error");
        },
      });
    });

    it("should encode for broadcast after signing", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        complete: () => {
          expect(mockEncodeToBroadcast).toHaveBeenCalledWith(expect.anything(), mockSignature);
          done();
        },
        error: done.fail,
      });
    });

    it("should build optimistic operation", done => {
      const signOperation = buildSignOperation(mockSignerContext);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      observable.subscribe({
        complete: () => {
          expect(mockBuildOptimisticOperation).toHaveBeenCalledWith(mockAccount, mockTransaction);
          done();
        },
        error: done.fail,
      });
    });

    it("should support cancellation", () => {
      const signOperation = buildSignOperation(mockSignerContext);

      const observable = signOperation({
        account: mockAccount,
        transaction: mockTransaction,
        deviceId: "device-1",
      });

      const subscription = observable.subscribe({
        next: () => {},
      });

      // Should not throw
      expect(() => subscription.unsubscribe()).not.toThrow();
    });
  });
});
