import { fetchEstimatedFees } from "../../network/api";
import { craftTransaction } from "./craftTransaction";
import { combine } from "./combine";

jest.mock("../../network/api");
jest.mock("@ledgerhq/logs");
// Mock the iso-filecoin-backed validator so synthetic test addresses pass.
jest.mock("../../network/addresses", () => ({
  validateAddress: (input: string) => ({
    isValid: input !== "INVALID",
    parsedAddress: { toString: () => input },
  }),
}));
jest.mock("../../erc20/tokenAccounts", () => ({
  abiEncodeTransferParams: (_recipient: string, _amount: string) => "0xencoded",
  encodeTxnParams: (_data: string) => "encoded-params",
}));
// Mock iso-filecoin/message Message so we don't hit real address checksum
// validation inside serialize() — that is covered by integration tests, not units.
jest.mock("iso-filecoin/message", () => ({
  Message: class {
    version: number;
    to: string;
    from: string;
    nonce: number;
    value: string;
    gasLimit: number;
    gasFeeCap: string;
    gasPremium: string;
    method: number;
    params: string | undefined;
    constructor(msg: Record<string, unknown>) {
      this.version = (msg.version as number) ?? 0;
      this.to = msg.to as string;
      this.from = msg.from as string;
      this.nonce = msg.nonce as number;
      this.value = msg.value as string;
      this.gasLimit = msg.gasLimit as number;
      this.gasFeeCap = msg.gasFeeCap as string;
      this.gasPremium = msg.gasPremium as string;
      this.method = msg.method as number;
      this.params = msg.params as string | undefined;
    }
    serialize(): Uint8Array {
      return new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
    }
  },
}));

const mockedFetch = fetchEstimatedFees as jest.MockedFunction<typeof fetchEstimatedFees>;

function mockFees() {
  mockedFetch.mockResolvedValue({
    gas_limit: 1_000_000,
    gas_fee_cap: "150000",
    gas_premium: "125000",
    nonce: 5,
  });
}

describe("craftTransaction", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockFees();
  });

  it("native intent produces a base64 string that combine can parse", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za",
      recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y",
      amount: 1_000_000_000_000_000_000n,
      asset: { type: "native" as const },
      useAllAmount: false,
    };

    const crafted = await craftTransaction(intent);
    expect(typeof crafted.transaction).toBe("string");

    // Round-trip: combine should produce a parseable BroadcastTransactionRequest
    const combined = combine(crafted.transaction, ["c2ln"]);
    const parsed = JSON.parse(combined);
    expect(parsed.message.nonce).toBe(5);
    expect(parsed.message.value).toBe("1000000000000000000");
    expect(parsed.message.method).toBe(0);
  });

  it("throws on unknown asset type (no silent native fallback)", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za",
      recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y",
      amount: 100n,
      asset: { type: "spl-token" as unknown as "native" },
      useAllAmount: false,
    };

    await expect(craftTransaction(intent)).rejects.toThrow(/Unsupported asset type/);
  });

  it("fetches nonce at craft time (via fetchEstimatedFees)", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "f1abjxfbp274xpdqcpuaykwkfb43omjotacm2p3za",
      recipient: "f1z4nykg7q6q5qnxs7h4zknhlqbqhq5jxcqm5qw4y",
      amount: 1_000n,
      asset: { type: "native" as const },
      useAllAmount: false,
    };

    await craftTransaction(intent);
    // fetchEstimatedFees is called at least once: once for getNextSequence, once for gas
    expect(mockedFetch).toHaveBeenCalledTimes(2);
  });

  it("uses custom fees when provided (skips fetchEstimatedFees for gas)", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "f1sender",
      recipient: "f1recipient",
      amount: 1_000n,
      asset: { type: "native" as const },
      useAllAmount: false,
    };
    const customFees = {
      value: 100n,
      parameters: { gasFeeCap: "200", gasLimit: "500000", gasPremium: "100" },
    };

    const crafted = await craftTransaction(intent, customFees);
    const parsed = JSON.parse(crafted.transaction);
    expect(parsed.message.gasFeeCap).toBe("200");
    expect(parsed.message.gasLimit).toBe(500000);
    expect(parsed.message.gasPremium).toBe("100");
    // Only 1 call for nonce, none for gas
    expect(mockedFetch).toHaveBeenCalledTimes(1);
  });

  it("crafts ERC-20 token transfer with InvokeEVM method", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "f1sender",
      recipient: "0xrecipient",
      amount: 5_000n,
      asset: { type: "erc20" as const, assetReference: "0xcontract" },
      useAllAmount: false,
    };

    const crafted = await craftTransaction(intent);
    const parsed = JSON.parse(crafted.transaction);
    expect(parsed.message.method).toBe(3844450837);
    expect(parsed.message.value).toBe("0");
    expect(parsed.message.params).toBe("encoded-params");
  });

  it("throws on invalid sender address", async () => {
    const intent = {
      intentType: "transaction" as const,
      type: "send" as const,
      sender: "INVALID",
      recipient: "f1recipient",
      amount: 1_000n,
      asset: { type: "native" as const },
      useAllAmount: false,
    };

    await expect(craftTransaction(intent)).rejects.toThrow(/Invalid sender address/);
  });
});
