// What the screens that READ a stored token balance do with a holding whose
// scale nothing knows.
//
// https://git.eeqj.de/sneak/AutistMask/issues/349 stopped `fetchTokenBalances()`
// fabricating a scale of 18, so a row it cannot state a quantity for is now
// stored with `balance: null`. Every reader of that field therefore has two
// distinct inputs where it used to have one, and the property that has to hold
// at each of them is the same one this codebase keeps losing:
//
//     null (unknown) and 0 (genuinely zero) must produce DIFFERENT output.
//
// Losing it is what https://git.eeqj.de/sneak/AutistMask/issues/246,
// https://git.eeqj.de/sneak/AutistMask/issues/306,
// https://git.eeqj.de/sneak/AutistMask/issues/322,
// https://git.eeqj.de/sneak/AutistMask/issues/359 and
// https://git.eeqj.de/sneak/AutistMask/issues/364 each were. So every case
// below asserts the pair, not just that the null branch does something
// reasonable: an assertion on the null alone still passes on a build that
// renders both as zero, which is precisely the build being guarded against.
//
// The writer half — that the fetcher stores null rather than 18 — is in
// tests/fabricatedDecimals.test.js, and the Send and confirmation screens are
// in tests/unknownScaleSend.test.js.

"use strict";

// helpers.js reaches for both at module scope through the modules it pulls in.
globalThis.chrome = {
    storage: {
        local: {
            get: () => Promise.resolve({}),
            set: () => Promise.resolve(),
        },
    },
    runtime: { sendMessage: () => {} },
};
globalThis.document = {
    getElementById: () => null,
    createElement: () => ({ style: {}, classList: { toggle() {} } }),
    body: { prepend: () => {} },
    addEventListener: () => {},
};

const {
    balanceLine,
    balanceLinesForAddress,
    addressHoldsFunds,
} = require("../src/popup/views/helpers");
const {
    prices,
    clearPrices,
    getAddressValue,
} = require("../src/shared/prices");
const { state } = require("../src/shared/state");

const NOVEL = "0x1111111111111111111111111111111111111111";

// One stored tokenBalances row. `balance: null` is what balances.js writes for
// a holding whose scale nothing knows; "0.0" is a quantity that was actually
// established and is zero.
function holding(balance) {
    return {
        address: NOVEL,
        symbol: "NOVEL",
        decimals: balance === null ? null : 18,
        balance,
        holders: 50000,
    };
}

function address(balance) {
    return {
        address: "0x" + "a".repeat(40),
        balance: "0",
        tokenBalances: [holding(balance)],
    };
}

// The quantity cell of a rendered row, which is the second of the two spans
// inside the fixed-width span.
function quantities(html) {
    return [...html.matchAll(/<span>([^<]*)<\/span>/g)].map((m) => m[1]);
}

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

afterEach(() => {
    clearPrices();
});

describe("balanceLine", () => {
    test("an unknown quantity and a zero one render differently", () => {
        const unknown = balanceLine("NOVEL", null, null, NOVEL);
        const zero = balanceLine("NOVEL", 0, null, NOVEL);
        expect(unknown).not.toBe(zero);
        expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
        expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]);
    });

    test("an unknown quantity produces no fiat figure, a zero one does", () => {
        prices.NOVEL = 3;
        const unknown = balanceLine("NOVEL", null, 3, NOVEL);
        const zero = balanceLine("NOVEL", 0, 3, NOVEL);
        // A price times an unknown quantity is not $0.00: that is the same
        // claim of "nothing here" the quantity cell just refused to make.
        expect(unknown).toContain(
            '<span class="text-right text-muted flex-1">&nbsp;</span>',
        );
        expect(zero).toContain(
            '<span class="text-right text-muted flex-1">$0.00</span>',
        );
    });
});

describe("balanceLinesForAddress", () => {
    // The show-zero setting is a statement about zeroes. An unknown quantity
    // is not one, so hiding the row would assert the zero nobody established
    // and the holding would vanish from the list entirely.
    test("hiding zero balances hides the zero row and keeps the unknown one", () => {
        const unknown = balanceLinesForAddress(address(null), [], false);
        const zero = balanceLinesForAddress(address("0.0"), [], false);
        expect(unknown).not.toBe(zero);
        expect(unknown).toContain("quantity unknown");
        expect(unknown).toContain("NOVEL");
        expect(zero).not.toContain("NOVEL");
    });

    test("showing zero balances still tells the two apart", () => {
        const unknown = balanceLinesForAddress(address(null), [], true);
        const zero = balanceLinesForAddress(address("0.0"), [], true);
        expect(unknown).not.toBe(zero);
        expect(quantities(unknown)).toEqual([
            "ETH",
            "0.0000",
            "NOVEL",
            "quantity unknown",
        ]);
        expect(quantities(zero)).toEqual(["ETH", "0.0000", "NOVEL", "0.0000"]);
    });
});

describe("addressHoldsFunds", () => {
    // Read by deleteAddress.js to decide whether removing the address is
    // warned about. balances.js drops a row of zero base units before any
    // scale is consulted, so a row that survived with no quantity is holding
    // something, and the warning must err towards warning.
    test("an unknown balance holds funds, a zero balance does not", () => {
        expect(addressHoldsFunds(address(null))).toBe(true);
        expect(addressHoldsFunds(address("0.0"))).toBe(false);
    });
});

describe("getAddressValue", () => {
    // `usd` is the value of what could be priced and `partial` says it is a
    // floor rather than the total. An unpriceable holding is exactly what
    // `partial` exists for; a holding of zero can neither add to the total nor
    // make it incomplete.
    test("an unknown balance makes the total partial, a zero balance does not", () => {
        prices.ETH = 2000;
        prices.NOVEL = 3;
        const unknown = getAddressValue(address(null));
        const zero = getAddressValue(address("0.0"));
        expect(unknown).not.toEqual(zero);
        expect(unknown).toEqual({ usd: 0, partial: true });
        expect(zero).toEqual({ usd: 0, partial: false });
    });

    test("an unknown balance is not priced as zero of the token", () => {
        prices.ETH = 2000;
        prices.NOVEL = 3;
        // The same row with a real quantity of 10 is worth $30. Neither that
        // figure nor a confident $0.00 may be stated for the unknown one.
        expect(getAddressValue(address("10.0"))).toEqual({
            usd: 30,
            partial: false,
        });
        expect(getAddressValue(address(null)).usd).toBe(0);
        expect(getAddressValue(address(null)).partial).toBe(true);
    });
});
