const { parseEther } = require("ethers");
const {
    CODES,
    FEE_PENDING,
    FEE_KNOWN,
    FEE_UNAVAILABLE,
    feeReserveWei,
    feeEstimateWei,
    toFixedPoint,
    validateTransfer,
} = require("../src/shared/txValidation");

// A plausible mainnet fee: 21000 gas at 20 gwei.
const FEE = 21000n * 20000000000n; // 0.00042 ETH

const GWEI = 1000000000n;
const GAS_LIMIT = 21000n;

describe("toFixedPoint", () => {
    test("scales human decimals to 18 places", () => {
        expect(toFixedPoint("1.5")).toBe(parseEther("1.5"));
        expect(toFixedPoint("0")).toBe(0n);
    });

    test("rejects values it cannot represent exactly", () => {
        expect(toFixedPoint("not a number")).toBe(null);
        expect(toFixedPoint("")).toBe(null);
        expect(toFixedPoint(null)).toBe(null);
        // More precision than 18 decimals can hold.
        expect(toFixedPoint("0.0000000000000000001")).toBe(null);
    });
});

describe("validateTransfer, native ETH", () => {
    const eth = (over) => ({
        isErc20: false,
        amount: "0.5",
        ethBalance: "1.0",
        feeStatus: FEE_KNOWN,
        feeWei: FEE,
        ...over,
    });

    test("allows a send comfortably within balance", () => {
        const r = validateTransfer(eth());
        expect(r).toEqual({ canSend: true, codes: [] });
    });

    test("blocks a send whose amount plus fee exceeds the balance", () => {
        // The whole balance: passes an amount-only check, fails once the fee
        // is counted. This is the bug this module exists to prevent.
        const r = validateTransfer(eth({ amount: "1.0", ethBalance: "1.0" }));
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_WITH_FEE]);
    });

    test("blocks a send left short by less than one fee", () => {
        const balance = "1.0";
        // One wei less headroom than the fee needs.
        const amount = "0.99958000000000001"; // 1.0 - 0.00042 + 1e-17
        const r = validateTransfer(eth({ amount, ethBalance: balance }));
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_WITH_FEE]);
    });

    test("allows a send that leaves exactly the fee behind", () => {
        const r = validateTransfer(
            eth({ amount: "0.99958", ethBalance: "1.0" }),
        );
        expect(r).toEqual({ canSend: true, codes: [] });
    });

    test("reports plain insufficient balance when the amount alone is too big", () => {
        const r = validateTransfer(eth({ amount: "2.0", ethBalance: "1.0" }));
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH]);
    });

    test("blocks while the fee estimate is still pending", () => {
        const r = validateTransfer(
            eth({ feeStatus: FEE_PENDING, feeWei: null }),
        );
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.FEE_PENDING]);
    });

    test("blocks when the fee estimate failed, without assuming zero", () => {
        const r = validateTransfer(
            eth({
                amount: "1.0",
                ethBalance: "1.0",
                feeStatus: FEE_UNAVAILABLE,
                feeWei: null,
            }),
        );
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.FEE_UNAVAILABLE]);
        // A zero fee would have let this exact transfer through.
        expect(
            validateTransfer(
                eth({ amount: "1.0", ethBalance: "1.0", feeWei: 0n }),
            ).canSend,
        ).toBe(true);
    });

    test("still reports an over-balance amount before the estimate lands", () => {
        const r = validateTransfer(
            eth({
                amount: "2.0",
                ethBalance: "1.0",
                feeStatus: FEE_PENDING,
                feeWei: null,
            }),
        );
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH, CODES.FEE_PENDING]);
    });

    test("rejects an amount it cannot do exact arithmetic on", () => {
        const r = validateTransfer(eth({ amount: "abc" }));
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.AMOUNT_INVALID]);
    });

    test("rejects a negative amount", () => {
        // A negative amount parses to a perfectly good bigint, so neither
        // balance comparison can fire: both are trivially false against it.
        // Left unblocked it clears the screen and then dies at encode time.
        const r = validateTransfer(
            eth({ amount: "-1", ethBalance: "1.0", feeWei: 861000000000000n }),
        );
        expect(r).toEqual({ canSend: false, codes: [CODES.AMOUNT_INVALID] });
        expect(
            validateTransfer(eth({ amount: "-0.000000000000000001" })),
        ).toEqual({ canSend: false, codes: [CODES.AMOUNT_INVALID] });
    });

    test("treats a missing balance as zero, not as unlimited", () => {
        const r = validateTransfer(eth({ ethBalance: undefined }));
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH]);
    });
});

describe("validateTransfer, ERC-20", () => {
    const erc20 = (over) => ({
        isErc20: true,
        amount: "100.0",
        tokenBalance: "250.0",
        ethBalance: "1.0",
        feeStatus: FEE_KNOWN,
        feeWei: FEE,
        ...over,
    });

    test("allows a transfer with tokens to spend and ETH for the fee", () => {
        expect(validateTransfer(erc20())).toEqual({ canSend: true, codes: [] });
    });

    test("checks the token amount against the token balance", () => {
        const r = validateTransfer(erc20({ amount: "250.000001" }));
        expect(r.codes).toEqual([CODES.INSUFFICIENT_TOKEN]);
    });

    test("does not charge the fee against the token balance", () => {
        // The full token balance is sendable: the fee is paid in ETH.
        expect(validateTransfer(erc20({ amount: "250.0" })).canSend).toBe(true);
    });

    test("blocks when the ETH balance does not cover the fee", () => {
        const r = validateTransfer(erc20({ ethBalance: "0.0001" }));
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_FOR_FEE]);
    });

    test("allows a fee exactly equal to the ETH balance", () => {
        const r = validateTransfer(erc20({ ethBalance: "0.00042" }));
        expect(r).toEqual({ canSend: true, codes: [] });
    });

    test("reports both shortfalls when tokens and ETH are both short", () => {
        const r = validateTransfer(
            erc20({ amount: "300.0", ethBalance: "0.0" }),
        );
        expect(r.codes).toEqual([
            CODES.INSUFFICIENT_TOKEN,
            CODES.INSUFFICIENT_ETH_FOR_FEE,
        ]);
    });

    test("blocks while the fee estimate is pending or failed", () => {
        expect(
            validateTransfer(erc20({ feeStatus: FEE_PENDING, feeWei: null }))
                .codes,
        ).toEqual([CODES.FEE_PENDING]);
        expect(
            validateTransfer(
                erc20({ feeStatus: FEE_UNAVAILABLE, feeWei: null }),
            ).codes,
        ).toEqual([CODES.FEE_UNAVAILABLE]);
    });

    test("rejects a negative token amount", () => {
        const r = validateTransfer(
            erc20({ amount: "-0.5", feeWei: 861000000000000n }),
        );
        expect(r).toEqual({ canSend: false, codes: [CODES.AMOUNT_INVALID] });
    });

    test("treats a missing token balance as zero", () => {
        const r = validateTransfer(erc20({ tokenBalance: undefined }));
        expect(r.codes).toEqual([CODES.INSUFFICIENT_TOKEN]);
    });
});

// The reserve a node requires, not the fee the transaction is expected to
// actually cost. An unpinned send goes out as type-2, and the node checks it
// against maxFeePerGas; reserving gasPrice lets a transaction the node will
// reject pass the gate.
describe("feeReserveWei", () => {
    // baseFee 20 gwei, tip 1 gwei: eth_gasPrice reports ~21 gwei, while
    // ethers populates maxFeePerGas as baseFee * 2 + tip = 41 gwei.
    const type2 = {
        gasPrice: 21n * GWEI,
        maxFeePerGas: 41n * GWEI,
        maxPriorityFeePerGas: 1n * GWEI,
    };

    test("reserves gasLimit * maxFeePerGas, not gasLimit * gasPrice", () => {
        expect(feeReserveWei(GAS_LIMIT, type2)).toBe(GAS_LIMIT * 41n * GWEI);
        expect(feeReserveWei(GAS_LIMIT, type2)).toBe(861000000000000n);
        // The number the node would not have accepted.
        expect(feeReserveWei(GAS_LIMIT, type2)).not.toBe(441000000000000n);
    });

    test("gates out a send the type-2 reserve cannot fund", () => {
        // Exactly fundable against a gasPrice reserve (0.999559 + 0.000441 is
        // the whole balance to the wei), and short against the reserve the
        // node will actually require.
        const send = {
            isErc20: false,
            amount: "0.999559",
            ethBalance: "1.0",
            feeStatus: FEE_KNOWN,
        };
        expect(
            validateTransfer({
                ...send,
                feeWei: GAS_LIMIT * type2.gasPrice,
            }).canSend,
        ).toBe(true);
        const r = validateTransfer({
            ...send,
            feeWei: feeReserveWei(GAS_LIMIT, type2),
        });
        expect(r.canSend).toBe(false);
        expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_WITH_FEE]);
    });

    test("falls back to gasPrice on a network with no type-2 pricing", () => {
        const legacy = { gasPrice: 21n * GWEI, maxFeePerGas: null };
        expect(feeReserveWei(GAS_LIMIT, legacy)).toBe(GAS_LIMIT * 21n * GWEI);
    });

    test("returns null when no usable price or gas limit is available", () => {
        expect(feeReserveWei(GAS_LIMIT, { gasPrice: null })).toBe(null);
        expect(feeReserveWei(GAS_LIMIT, {})).toBe(null);
        expect(feeReserveWei(GAS_LIMIT, null)).toBe(null);
        expect(feeReserveWei(21000, type2)).toBe(null);
    });
});

// The display counterpart of the reserve: what the transaction is expected to
// cost. Shown alongside the reserve so the screen neither contradicts the gate
// nor quotes the user roughly double what they will pay.
describe("feeEstimateWei", () => {
    const type2 = {
        gasPrice: 21n * GWEI,
        maxFeePerGas: 41n * GWEI,
        maxPriorityFeePerGas: 1n * GWEI,
    };

    test("estimates gasLimit * gasPrice, below the reserve", () => {
        expect(feeEstimateWei(GAS_LIMIT, type2)).toBe(441000000000000n);
        expect(feeReserveWei(GAS_LIMIT, type2)).toBe(861000000000000n);
        expect(feeEstimateWei(GAS_LIMIT, type2)).toBeLessThan(
            feeReserveWei(GAS_LIMIT, type2),
        );
    });

    test("equals the reserve when the network has no type-2 pricing", () => {
        const legacy = { gasPrice: 21n * GWEI, maxFeePerGas: null };
        expect(feeEstimateWei(GAS_LIMIT, legacy)).toBe(
            feeReserveWei(GAS_LIMIT, legacy),
        );
    });

    test("falls back to maxFeePerGas when there is no gasPrice", () => {
        const noLegacy = { gasPrice: null, maxFeePerGas: 41n * GWEI };
        expect(feeEstimateWei(GAS_LIMIT, noLegacy)).toBe(
            feeReserveWei(GAS_LIMIT, noLegacy),
        );
    });

    test("returns null on the same unusable inputs as the reserve", () => {
        expect(feeEstimateWei(GAS_LIMIT, {})).toBe(null);
        expect(feeEstimateWei(GAS_LIMIT, null)).toBe(null);
        expect(feeEstimateWei(GAS_LIMIT, { gasPrice: -1n })).toBe(null);
        expect(feeEstimateWei(21000, type2)).toBe(null);
    });
});

// Everything that is not a usable fee blocks exactly as FEE_UNAVAILABLE does.
// Each of these previously returned { canSend: true, codes: [] } — counting no
// fee at all, on a full-balance send, in the direction that lets money out.
describe("validateTransfer, unusable fee input fails closed", () => {
    const fullBalanceSend = (over) => ({
        isErc20: false,
        amount: "1.0",
        ethBalance: "1.0",
        ...over,
    });

    test("blocks a null fee claiming to be known", () => {
        const r = validateTransfer(
            fullBalanceSend({ feeStatus: FEE_KNOWN, feeWei: null }),
        );
        expect(r).toEqual({ canSend: false, codes: [CODES.FEE_UNAVAILABLE] });
    });

    test("blocks a known fee that is a number rather than a bigint", () => {
        const r = validateTransfer(
            fullBalanceSend({ feeStatus: FEE_KNOWN, feeWei: 420000000000000 }),
        );
        expect(r).toEqual({ canSend: false, codes: [CODES.FEE_UNAVAILABLE] });
    });

    test("blocks an unrecognised fee status", () => {
        const r = validateTransfer(fullBalanceSend({ feeStatus: "bogus" }));
        expect(r).toEqual({ canSend: false, codes: [CODES.FEE_UNAVAILABLE] });
    });

    test("blocks a negative fee", () => {
        const r = validateTransfer(
            fullBalanceSend({ feeStatus: FEE_KNOWN, feeWei: -1n }),
        );
        expect(r).toEqual({ canSend: false, codes: [CODES.FEE_UNAVAILABLE] });
    });

    test("blocks an ERC-20 transfer on an unusable fee too", () => {
        const r = validateTransfer({
            isErc20: true,
            amount: "100.0",
            tokenBalance: "250.0",
            ethBalance: "1.0",
            feeStatus: FEE_KNOWN,
            feeWei: null,
        });
        expect(r).toEqual({ canSend: false, codes: [CODES.FEE_UNAVAILABLE] });
    });
});
