// The scale the swap lines of the dApp approval screen are displayed with.
//
// Issue #340: `tokenInfo()` in `src/shared/uniswap.js` returned `decimals: 18`
// for any token absent from the bundled list — the same guess
// https://git.eeqj.de/sneak/AutistMask/issues/306 removed from the ERC-20
// amount line, still live on the swap path. A 1,000-token swap of a 6-decimal
// token then rendered as `0.000000001` on the one screen whose job is to state
// what is being authorized, and every newly listed token reaches it.
//
// What is asserted here is the rule #306 established: resolve the real scale
// wherever the wallet already has it, and where nothing has it refuse to
// format — base units with the scale stated, never a quantity.

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

const { AbiCoder, Interface } = require("ethers");
const { state } = require("../src/shared/state");
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
const { decodeCalldata } = require("../src/popup/views/approval");

const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
// Outside the bundled list, as every newly listed token is.
const NOVEL = "0xE2E0000000000000000000000000000000000E2e";
// Also outside it, standing in for the swap's output side.
const NOVEL_OUT = "0xd0d0000000000000000000000000000000000d0d";
// In the bundled list, at 18 decimals.
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

// 1,000.00 of a 6-decimal token — the amount from the issue, which the 18
// guess rendered as 0.000000001.
const THOUSAND_AT_SIX = 1000000000n;
// 0.5 WETH out, so the Min. received line has a real number of its own.
const HALF_WETH = 500000000000000000n;

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 `tokenIn`
// for at least `amountOutMin` of `tokenOut`.
function swapData(tokenIn, amountIn, tokenOut, amountOutMin) {
    const input = coder.encode(
        ["address", "uint256", "uint256", "address[]", "bool"],
        [RECIPIENT, amountIn, amountOutMin, [tokenIn, tokenOut], true],
    );
    return routerIface.encodeFunctionData("execute", [
        "0x08",
        [input],
        9999999999n,
    ]);
}

// A wallet holding `token` with the decimals the block explorer reported,
// shaped as balances.js writes it onto state.
function walletsHolding(token, decimals) {
    return [
        {
            name: "Wallet 1",
            addresses: [
                {
                    address: "0x" + "a".repeat(40),
                    balance: "1.0",
                    tokenBalances: [
                        {
                            address: token,
                            symbol: "NOVEL",
                            decimals,
                            balance: "1000.0",
                        },
                    ],
                },
            ],
        },
    ];
}

// The swap detail line as the approval screen renders it. It goes through
// decodeCalldata() rather than uniswap.decode() directly, because the scale
// sources the screen supplies are part of what is under test.
function swapDetail(data, label) {
    const decoded = decodeCalldata(data, ROUTER);
    return decoded.details.find((d) => d.label === label);
}

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

describe("a swap of a token outside the bundled list", () => {
    const data = () => swapData(NOVEL, THOUSAND_AT_SIX, WETH, HALF_WETH);

    test("shows the true quantity when the user tracks the token", () => {
        state.trackedTokens = [
            { address: NOVEL, symbol: "NOVEL", decimals: 6 },
        ];
        expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
    });

    test("shows the true quantity from the explorer's decimals", () => {
        state.wallets = walletsHolding(NOVEL, "6");
        expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
    });

    test("refuses to format when nothing knows the scale", () => {
        const detail = swapDetail(data(), "Amount");
        expect(detail.value).toBe("1000000000 base units (decimals unknown)");
        expect(detail.value).toBe(unknownDecimalsAmount(THOUSAND_AT_SIX));
        // The defect: an 18-decimal guess renders this swap as 0.000000001, a
        // quantity, and a wrong one.
        expect(detail.value).not.toMatch(/^0\./);
        expect(detail.value).not.toMatch(/[0-9]\.[0-9]/);
    });

    test("the amount carried to the status screens is the same refusal", () => {
        expect(swapDetail(data(), "Amount").rawValue).toBe(
            "1000000000 base units (decimals unknown)",
        );
    });

    test("a bundled token on the other side still formats", () => {
        expect(swapDetail(data(), "Min. received").value).toBe("0.5000 WETH");
    });
});

describe("the Min. received line takes the same rule", () => {
    test("refuses to format an output token of unknown scale", () => {
        const data = swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
        const detail = swapDetail(data, "Min. received");
        expect(detail.value).toBe("1000000000 base units (decimals unknown)");
        expect(detail.value).not.toMatch(/[0-9]\.[0-9]/);
    });

    test("shows the true quantity when the user tracks the output token", () => {
        state.trackedTokens = [
            { address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
        ];
        const data = swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
        expect(swapDetail(data, "Min. received").value).toBe("1000.0000");
    });
});

describe("the permit amount takes the same rule", () => {
    // PERMIT2_PERMIT (command 0x0a): the input token and amount come from the
    // permit rather than from a swap step.
    function permitData(token, amount) {
        const input = coder.encode(
            [
                "tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
                "bytes",
            ],
            [[[token, amount, 0, 0], ROUTER, 9999999999], "0x1234"],
        );
        return routerIface.encodeFunctionData("execute", [
            "0x0a",
            [input],
            9999999999n,
        ]);
    }

    test("refuses to format a permit on a token of unknown scale", () => {
        const detail = swapDetail(permitData(NOVEL, THOUSAND_AT_SIX), "Amount");
        expect(detail.value).toBe("1000000000 base units (decimals unknown)");
    });

    test("an unbounded permit is still named, with or without a scale", () => {
        const maxUint160 = (1n << 160n) - 1n;
        expect(swapDetail(permitData(NOVEL, maxUint160), "Amount").value).toBe(
            "Unlimited",
        );
    });
});
