// Tests for the token filtering in the Send view's token selector
// (src/popup/views/send.js).
//
// The selector decides which of the user's tokens can be spent at all, so
// over-filtering here is worse than in the history list: the asset is not
// merely hidden, it becomes unspendable through the UI.  Issue #230: an
// explorer that omits holders_count was read as "zero holders" and the token
// disappeared from this list.
//
// renderSendTokenSelect only ever touches getElementById, createElement,
// innerHTML, value, textContent and appendChild, so a small stub document is
// enough to drive it; the real DOM behaviour of the view is covered by
// tests/e2e/run.js.

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

const { state } = require("../src/shared/state");
const { renderSendTokenSelect } = require("../src/popup/views/send");

const USDC_CONTRACT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const NOVEL_TOKEN = "0x1111111111111111111111111111111111111111";

let select;

function installStubDocument() {
    select = { innerHTML: "", children: [] };
    select.appendChild = (child) => select.children.push(child);
    globalThis.document = {
        getElementById: (id) => (id === "send-token" ? select : null),
        createElement: () => ({ value: "", textContent: "" }),
    };
}

// The symbols offered for sending, excluding the hardcoded ETH option that
// renderSendTokenSelect writes straight into innerHTML.
function offeredTokens() {
    return select.children.map((opt) => opt.value.toLowerCase());
}

function tokenBalance(overrides) {
    return {
        address: NOVEL_TOKEN,
        symbol: "SPAMTKN",
        decimals: 18,
        balance: "12.5",
        holders: 50000,
        ...overrides,
    };
}

function render(tokenBalances) {
    installStubDocument();
    renderSendTokenSelect({ address: "0x" + "a".repeat(40), tokenBalances });
}

beforeEach(() => {
    state.fraudContracts = [];
    state.hideLowHolderTokens = true;
});

describe("the low-holder rule in the send token selector", () => {
    test("ETH is always offered", () => {
        render([]);
        expect(select.innerHTML).toBe('<option value="ETH">ETH</option>');
        expect(offeredTokens()).toEqual([]);
    });

    test("a token with plenty of holders is offered", () => {
        render([tokenBalance()]);
        expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
    });

    test("a token reporting zero holders is withheld", () => {
        render([tokenBalance({ holders: 0 })]);
        expect(offeredTokens()).toEqual([]);
    });

    test("boundary: 999 holders is withheld, 1000 is offered", () => {
        render([tokenBalance({ holders: 999 })]);
        expect(offeredTokens()).toEqual([]);
        render([tokenBalance({ holders: 1000 })]);
        expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
    });

    // Issue #230: an unknown holder count must not read as zero.  A token the
    // user demonstrably holds — it has a balance — cannot be made unspendable
    // by a field the block explorer failed to report.
    test("a token whose holder count is unknown is still offered", () => {
        render([tokenBalance({ holders: null })]);
        expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
    });

    test("a token balance carrying no holders field at all is offered", () => {
        const t = tokenBalance();
        delete t.holders;
        render([t]);
        expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
    });

    test("the rule is bypassed entirely when the setting is off", () => {
        state.hideLowHolderTokens = false;
        render([tokenBalance({ holders: 0 })]);
        expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
    });
});

describe("the other send-selector rules are unaffected", () => {
    test("a token spoofing a known symbol from a wrong address is withheld", () => {
        render([
            tokenBalance({ symbol: "USDC", holders: null }),
            tokenBalance({ address: USDC_CONTRACT, symbol: "USDC" }),
        ]);
        expect(offeredTokens()).toEqual([USDC_CONTRACT.toLowerCase()]);
    });

    test("a blocklisted fraud contract is withheld even with an unknown count", () => {
        state.fraudContracts = [NOVEL_TOKEN.toUpperCase()];
        render([tokenBalance({ holders: null })]);
        expect(offeredTokens()).toEqual([]);
    });
});
