// What the dApp approval screen says the input token of a swap is when the
// calldata did not name one.
//
// Issue #357, the twin on the input side of
// https://git.eeqj.de/sneak/AutistMask/issues/353: `tokenInfo(null)` answered
// `{symbol: "ETH", decimals: 18}`, so a null `inputToken` rendered as
// `Token In: ETH (native)` and titled the swap `Swap ETH -> X`. An
// undetermined input was therefore asserted to the user as native ETH — the
// same class as https://git.eeqj.de/sneak/AutistMask/issues/340 and
// https://git.eeqj.de/sneak/AutistMask/issues/306, naming the wrong asset
// rather than merely mis-scaling it.
//
// The determination this file pins is the one #353 established, checked here
// on the input side: null is NOT how native ETH arrives. v4-core declares
// `type Currency is address` and wraps `address(0)` for native ETH, and a
// user-defined value type over `address` carries the plain `address` ABI
// encoding, so a native-ETH currency reaches the decoder as the truthy string
// "0x0000000000000000000000000000000000000000". WRAP_ETH sets that same
// explicit zero address. Both halves are asserted below: the zero address
// stays ETH, and null refuses.

const { AbiCoder, Interface, solidityPacked } = require("ethers");
const uniswap = require("../src/shared/uniswap");

const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const USER = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const ZERO = "0x0000000000000000000000000000000000000000";
// Both in the bundled list: USDT at 6 decimals, USDC at 6.
const USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

// The same wording the output side refuses with — one screen, one vocabulary.
const REFUSAL = "Unknown (not named in the calldata)";

const ONE_ETH = 1000000000000000000n;

const V4_SWAP_EXACT_IN = 0x07;
const V4_SETTLE = 0x0b;
const V4_TAKE = 0x0e;

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

function execute(commands, inputs) {
    return routerIface.encodeFunctionData("execute", [
        commands,
        inputs,
        9999999999n,
    ]);
}

function v4Input(actions, params) {
    return coder.encode(
        ["bytes", "bytes[]"],
        [new Uint8Array(actions), params],
    );
}

// IV4Router.ExactInputParams as the decoder reads it:
// (Currency currencyIn, PathKey[] path, uint128 amountIn, uint128 minOut).
function exactInParams(currencyIn, path, amountIn, amountOutMin) {
    return coder.encode(
        [
            "tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
        ],
        [[currencyIn, path, amountIn, amountOutMin]],
    );
}

// BALANCE_CHECK_ERC20 (command 0x0e): (address owner, address token,
// uint256 minBalance). It names the output token and nothing about the input.
function balanceCheck(token, minBalance) {
    return coder.encode(
        ["address", "address", "uint256"],
        [USER, token, minBalance],
    );
}

function detail(data, label) {
    const decoded = uniswap.decode(data, ROUTER, {});
    expect(decoded).not.toBeNull();
    return decoded.details.find((d) => d.label === label);
}

describe("an execute() whose input currency never decoded", () => {
    // A V4_SWAP whose sub-action params do not decode — an action encoding
    // this decoder does not know — leaves every V4 token null. The
    // BALANCE_CHECK still names the output, so the screen has a Min. received
    // figure and a Token Out, and previously claimed the user was paying ETH
    // for them.
    const data = () =>
        execute(solidityPacked(["uint8", "uint8"], [0x10, 0x0e]), [
            coder.encode(["bytes", "bytes[]"], ["0x07", ["0x"]]),
            balanceCheck(USDC, 2000000n),
        ]);

    test("says the input token is unknown instead of naming ETH", () => {
        expect(detail(data(), "Token In").value).toBe(REFUSAL);
    });

    test("keeps a Token In line, so the screen never omits what is paid", () => {
        const tokenIn = detail(data(), "Token In");
        expect(tokenIn).toBeDefined();
        // A refusal is not a token: nothing to link to an explorer.
        expect(tokenIn.address).toBeUndefined();
        expect(tokenIn.isToken).toBeUndefined();
    });

    test("does not name ETH in the swap title either", () => {
        expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
    });
});

describe("an execute() that names only an output token", () => {
    // A lone BALANCE_CHECK_ERC20: nothing in it says what is being paid.
    const data = () => execute("0x0e", [balanceCheck(USDT, 2000000n)]);

    test("refuses the input rather than defaulting it to ETH", () => {
        expect(detail(data(), "Token In").value).toBe(REFUSAL);
        expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
    });

    test("still states the output it did establish", () => {
        expect(detail(data(), "Token Out").value).toContain("USDT");
        expect(detail(data(), "Min. received").value).toBe("2.0000 USDT");
    });
});

describe("native ETH in, which V4 spells as the zero address", () => {
    test("a Currency of address(0) decodes to a truthy address string", () => {
        // The fact the whole determination rests on: an absent input currency
        // and a native-ETH one are distinguishable here, because the ABI
        // decoder never yields null for an address word.
        const [currency] = coder.decode(
            ["address", "uint256", "bool"],
            coder.encode(["address", "uint256", "bool"], [ZERO, ONE_ETH, true]),
        );
        expect(currency).toBe(ZERO);
        expect(Boolean(currency)).toBe(true);
    });

    test("a V4 SETTLE of the zero address is still ETH at 18 decimals", () => {
        const data = execute("0x10", [
            v4Input(
                [V4_SETTLE, V4_SWAP_EXACT_IN, V4_TAKE],
                [
                    coder.encode(
                        ["address", "uint256", "bool"],
                        [ZERO, ONE_ETH, true],
                    ),
                    exactInParams(
                        ZERO,
                        [[USDT, 500, 10, ZERO, "0x"]],
                        ONE_ETH,
                        2000000n,
                    ),
                    coder.encode(
                        ["address", "address", "uint256"],
                        [USDT, USER, 0n],
                    ),
                ],
            ),
        ]);

        expect(detail(data, "Token In").value).toBe("ETH (native)");
        expect(detail(data, "Amount").value).toBe("1.0000 ETH");
        expect(uniswap.decode(data, ROUTER, {}).name).toBe("Swap ETH → USDT");
    });

    test("WRAP_ETH is still ETH at 18 decimals", () => {
        // WRAP_ETH names no currency of its own; the decoder supplies the
        // explicit zero address for it, which is why it survives this change.
        const data = execute("0x0b", [
            coder.encode(["address", "uint256"], [ROUTER, ONE_ETH]),
        ]);

        expect(detail(data, "Token In").value).toBe("ETH (native)");
        expect(detail(data, "Amount").value).toBe("1.0000 ETH");
    });
});
