// The floor of the approval screen's amount line.
//
// Amounts are truncated to four decimal places for scannability (README.md,
// Display Consistency). With the token's true scale resolved, that truncation
// can still take a real amount below the floor and print it as `0.0000`: one
// base unit of an 18-decimal token, or a few hundred of an 8-decimal one. On
// the one screen whose job is to state what is being authorized, a nonzero
// transfer or allowance then reads as nothing.
//
// The invariant asserted here is narrow: a nonzero amount never renders as
// zero. The four-decimal rule itself is unchanged, and the string the
// confirmation screens carry as `txInfo.amount` is the same one, so it is
// asserted on `rawValue` alongside the displayed line.
//
// Both amount paths of that screen are covered: the ERC-20 line decoded by
// `src/popup/views/approval.js`, and the swap's `Amount` and `Min. received`
// lines decoded by `src/shared/uniswap.js`.

globalThis.chrome = {
    storage: { local: { get: async () => ({}), set: async () => {} } },
};

const { AbiCoder, Interface } = require("ethers");
const { ERC20_ABI } = require("../src/shared/constants");
const { state } = require("../src/shared/state");
const { decodeCalldata } = require("../src/popup/views/approval");
const uniswap = require("../src/shared/uniswap");
const {
    truncateAmount,
    truncateAmountNeverZero,
} = require("../src/shared/amountDisplay");

const iface = new Interface(ERC20_ABI);

// Bundled tokens, so the scale and the symbol both come from the list.
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // 6 decimals
const WBT = "0x925206b8a707096Ed26ae47C84747fE0bb734F59"; // 8 decimals
const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; // 18 decimals
// Outside the list, so the scale comes from what the user tracks and the line
// carries no symbol.
const NOVEL = "0xE2E0000000000000000000000000000000000E2e";

const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
const SPENDER = "0x1111111111111111111111111111111111111111";

// The Uniswap swap lines land on this same approval screen.
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; // 6 decimals
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; // 18 decimals

const coder = AbiCoder.defaultAbiCoder();
const routerIface = new Interface([
    "function execute(bytes commands, bytes[] inputs, uint256 deadline)",
]);

// A V2_SWAP_EXACT_IN (command 0x08) execute() call: `amountIn` of USDT for at
// least `amountOutMin` of WETH.
function swapData(amountIn, amountOutMin) {
    const input = coder.encode(
        ["address", "uint256", "uint256", "address[]", "bool"],
        [RECIPIENT, amountIn, amountOutMin, [USDT, WETH], true],
    );
    return routerIface.encodeFunctionData("execute", [
        "0x08",
        [input],
        9999999999n,
    ]);
}

function swapDetail(amountIn, amountOutMin, label) {
    const decoded = uniswap.decode(swapData(amountIn, amountOutMin), ROUTER);
    return decoded.details.find((d) => d.label === label);
}

function transferData(amount) {
    return iface.encodeFunctionData("transfer", [RECIPIENT, amount]);
}

function approveData(amount) {
    return iface.encodeFunctionData("approve", [SPENDER, amount]);
}

// The Amount detail as the approval screen renders it: `value` is the line on
// the screen, `rawValue` is what is carried to the wait/success/error screens.
function amount(data, token) {
    const decoded = decodeCalldata(data, token);
    return decoded.details.find((d) => d.label === "Amount");
}

beforeEach(() => {
    state.trackedTokens = [];
    state.wallets = [];
});

describe("a nonzero amount never renders as zero", () => {
    test("500 base units of a 6-decimal token", () => {
        const detail = amount(transferData(500n), USDC);
        expect(detail.value).toBe("0.0005 USDC");
        expect(detail.rawValue).toBe("0.0005");
    });

    test("1 base unit of an 18-decimal token", () => {
        const detail = amount(transferData(1n), DAI);
        expect(detail.value).toBe("0.000000000000000001 DAI");
        expect(detail.rawValue).toBe("0.000000000000000001");
    });

    test("500 base units of an 8-decimal token", () => {
        expect(amount(transferData(500n), WBT).rawValue).toBe("0.000005");
    });

    test("an allowance below the floor is not rendered as zero either", () => {
        expect(amount(approveData(1n), DAI).value).toBe(
            "0.000000000000000001 DAI",
        );
    });

    // The floor holds at any scale, not only the three above: for every
    // decimals a token can declare, one base unit has to show a digit.
    test("one base unit shows a significant digit at every scale", () => {
        for (let decimals = 0; decimals <= 30; decimals++) {
            state.trackedTokens = [{ address: NOVEL, decimals }];
            expect(amount(transferData(1n), NOVEL).rawValue).toMatch(/[1-9]/);
        }
    });
});

// The swap decoder formats its own amounts, so the same floor has to hold on
// the swap lines of the same screen. `Min. received` is the sharper of the
// two: the slippage floor rendered as `0.0000` states that the swap may return
// nothing.
describe("a swap's amounts never render as zero either", () => {
    test("a swap input below the floor keeps a significant digit", () => {
        // 50 base units of a 6-decimal token is 0.00005.
        const detail = swapDetail(50n, 10n ** 15n, "Amount");
        expect(detail.value).toBe("0.00005 USDT");
        expect(detail.rawValue).toBe("0.00005");
    });

    test("a min-received below the floor keeps a significant digit", () => {
        // 1 wei of an 18-decimal token.
        expect(swapDetail(10n ** 6n, 1n, "Min. received").value).toBe(
            "0.000000000000000001 WETH",
        );
    });

    test("swap amounts at or above the floor are still truncated", () => {
        expect(swapDetail(1000000n, 10n ** 15n, "Amount").rawValue).toBe(
            "1.0000",
        );
        expect(
            swapDetail(1000000n, 999999999999999999n, "Min. received").value,
        ).toBe("0.9999 WETH");
    });
});

describe("the four-decimal rule is otherwise unchanged", () => {
    test("a whole amount keeps exactly four decimals", () => {
        expect(amount(transferData(5000000000n), USDC).rawValue).toBe(
            "5000.0000",
        );
    });

    test("precision beyond four decimals is still truncated", () => {
        expect(amount(transferData(1234567890123456789n), DAI).rawValue).toBe(
            "1.2345",
        );
    });

    test("an amount at the floor is not extended", () => {
        expect(amount(transferData(100000000000000n), DAI).rawValue).toBe(
            "0.0001",
        );
    });

    test("a genuine zero still renders as zero", () => {
        expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000");
    });

    // The three truncators now share one module. The floor is a policy of the
    // approval and confirmation screens only: the history and balance lists
    // keep plain truncation, because the transaction detail view is the
    // authoritative record and already shows exact precision.
    test("the list rule stays unfloored", () => {
        expect(truncateAmount("0.000000000000000001")).toBe("0.0000");
        expect(truncateAmountNeverZero("0.000000000000000001")).toBe(
            "0.000000000000000001",
        );
    });
});
