// Tests for src/shared/holders.js and the balance-list spam gate that reads
// it (issue #230).
//
// The rule these pin down: an explorer that reports no holders_count has told
// us nothing, and "nothing" must not be recorded as "zero holders".  Zero is
// the strongest spam signal the wallet has, so handing it out for free turns
// a missing field into a hidden asset.

jest.mock("../src/shared/log", () => ({
    log: {
        debugf: () => {},
        infof: () => {},
        warnf: () => {},
        errorf: () => {},
    },
    debugFetch: jest.fn(),
    setRuntimeDebug: () => {},
    isDebug: () => false,
}));

global.fetch = jest.fn(() => {
    throw new Error("tests must not perform network requests");
});
global.chrome = { storage: { local: {} } };

const {
    LOW_HOLDER_THRESHOLD,
    parseHoldersCount,
    isLowHolderCount,
} = require("../src/shared/holders");
const { fetchTokenBalances } = require("../src/shared/balances");
const { debugFetch } = require("../src/shared/log");

const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
const HOLDER = "0x66133e8ea0f5d1d612d2502a968757d1048c214a";
const USDC_CONTRACT = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const NOVEL_TOKEN = "0x1111111111111111111111111111111111111111";

describe("parseHoldersCount", () => {
    test("a reported count parses to that number", () => {
        expect(parseHoldersCount("3500000")).toBe(3500000);
        expect(parseHoldersCount(3500000)).toBe(3500000);
    });

    test('a reported "0" parses to 0, which is not null', () => {
        expect(parseHoldersCount("0")).toBe(0);
        expect(parseHoldersCount(0)).toBe(0);
    });

    test("an omitted, null or empty count is unknown", () => {
        expect(parseHoldersCount(undefined)).toBeNull();
        expect(parseHoldersCount(null)).toBeNull();
        expect(parseHoldersCount("")).toBeNull();
    });

    test("an unparseable count is unknown rather than zero", () => {
        expect(parseHoldersCount("many")).toBeNull();
        expect(parseHoldersCount(NaN)).toBeNull();
    });
});

describe("isLowHolderCount", () => {
    test("the threshold is the documented 1,000 holders", () => {
        expect(LOW_HOLDER_THRESHOLD).toBe(1000);
    });

    test("a reported count below the threshold is low", () => {
        expect(isLowHolderCount(0)).toBe(true);
        expect(isLowHolderCount(999)).toBe(true);
    });

    test("a reported count at or above the threshold is not low", () => {
        expect(isLowHolderCount(1000)).toBe(false);
        expect(isLowHolderCount(1001)).toBe(false);
    });

    test("an unknown count is not low", () => {
        expect(isLowHolderCount(null)).toBe(false);
        expect(isLowHolderCount(undefined)).toBe(false);
    });
});

// fetchTokenBalances applies its own spam gate, which is not the low-holder
// display filter: it has no setting behind it and decides what the balance
// list contains at all.  It stays strict on an unknown count — see the
// comment at the gate — but must stop recording that unknown as zero.
describe("the balance-list spam gate", () => {
    function respondWith(items) {
        debugFetch.mockImplementation(async () => ({
            ok: true,
            status: 200,
            statusText: "OK",
            json: async () => items,
        }));
    }

    function item(overrides = {}) {
        const { token, ...rest } = overrides;
        return {
            value: "12500000",
            ...rest,
            token: {
                type: "ERC-20",
                address_hash: NOVEL_TOKEN,
                symbol: "SPAMTKN",
                name: "Spam Token",
                decimals: "6",
                holders_count: "50000",
                ...token,
            },
        };
    }

    beforeEach(() => {
        debugFetch.mockReset();
    });

    test("a token with plenty of reported holders is listed", async () => {
        respondWith([item()]);
        const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
        expect(balances).toHaveLength(1);
        expect(balances[0].holders).toBe(50000);
    });

    test("a token reporting zero holders is still excluded", async () => {
        respondWith([item({ token: { holders_count: "0" } })]);
        expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
    });

    test("an unknown holder count does not admit an unvouched token", async () => {
        respondWith([item({ token: { holders_count: null } })]);
        expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
    });

    // The path that reaches the send selector and the history filter: a token
    // the user vouched for by tracking it is listed whatever the explorer
    // says, and it must carry the unknown count through as null, not as the
    // zero that would then hide it downstream.
    test("a tracked token with an unknown count is listed with holders null", async () => {
        respondWith([item({ token: { holders_count: undefined } })]);
        const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
            { address: NOVEL_TOKEN.toUpperCase() },
        ]);
        expect(balances).toHaveLength(1);
        expect(balances[0].holders).toBeNull();
    });

    test("a known-list token with an unknown count is listed with holders null", async () => {
        respondWith([
            item({
                token: {
                    address_hash: USDC_CONTRACT,
                    symbol: "USDC",
                    holders_count: null,
                },
            }),
        ]);
        const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
        expect(balances).toHaveLength(1);
        expect(balances[0].holders).toBeNull();
    });

    test("no test in this file performed a network request", () => {
        expect(global.fetch).not.toHaveBeenCalled();
    });
});
