// The USD total of an address that holds something this build cannot price
// (issue #261).
//
// Prices exist for the top 25 tokens only, so an address can hold real assets
// with no price attached. Summing what is priced and printing the result as
// the total says "$0.00" for an address holding nothing but unpriced tokens —
// worth-nothing and worth-an-unknown-amount collapsed into one number, in the
// direction that matters. The two are separate facts here, the same way an
// absent holders_count is not a count of zero.
//
// The value and its rendering are asserted directly, and then through the two
// call sites that return their markup as a string: the wallet list on Home and
// the balance warning on the address-removal confirmation. AddressDetail and
// the Home summary line render into the DOM and are covered by tests/e2e.

// helpers.js pulls in state.js, which reads chrome.storage.local at load.
globalThis.chrome = {
    storage: { local: { get: async () => ({}), set: async () => {} } },
};

const {
    prices,
    clearPrices,
    getAddressValue,
    getWalletValue,
    getTotalValue,
    formatAddressTotal,
} = require("../src/shared/prices");
const { state } = require("../src/shared/state");
const { walletListHtml } = require("../src/popup/views/home");
const { balanceWarningHtml } = require("../src/popup/views/deleteAddress");

const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const NOVEL = "0x1111111111111111111111111111111111111111";

// No ETH, and a token no price is known for. The case the user is told is
// worth $0.00 today.
const UNPRICED_ONLY = {
    address: "0x" + "a".repeat(40),
    balance: "0",
    tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
};

// Nothing at all: the address really is worth zero.
const EMPTY = {
    address: "0x" + "b".repeat(40),
    balance: "0",
    tokenBalances: [],
};

// Every holding priced.
const FULLY_PRICED = {
    address: "0x" + "c".repeat(40),
    balance: "1.5",
    tokenBalances: [{ address: USDC, symbol: "USDC", balance: "2500.0" }],
};

// Part priced, part not: 1.5 ETH plus a token with no price.
const PARTLY_PRICED = {
    address: "0x" + "d".repeat(40),
    balance: "1.5",
    tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
};

beforeEach(() => {
    clearPrices();
    prices.ETH = 2000;
    prices.USDC = 1;
    state.wallets = [];
    state.trackedTokens = [];
    state.showZeroBalanceTokens = false;
    state.activeAddress = null;
});

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

// The total line only, in each of the two markup-returning call sites. The
// ETH balance line above it legitimately reads $0.00 for an address with no
// ETH, so the assertions have to name the line under test.
function walletListTotal(addr) {
    state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [addr] }];
    const match = walletListHtml().match(/min-h-\[1rem\]">([^<]*)</);
    return match && match[1];
}

function removalWarningTotal(addr) {
    const match = balanceWarningHtml(addr).match(/mt-1">([^<]*)</);
    return match && match[1];
}

describe("the value of an address, and whether it is the whole value", () => {
    test("an address holding only unpriced tokens has an incomplete value", () => {
        expect(getAddressValue(UNPRICED_ONLY)).toEqual({
            usd: 0,
            partial: true,
        });
    });

    test("an address holding nothing is complete, and zero", () => {
        expect(getAddressValue(EMPTY)).toEqual({ usd: 0, partial: false });
    });

    test("a fully priced address is complete, and unchanged", () => {
        expect(getAddressValue(FULLY_PRICED)).toEqual({
            usd: 5500,
            partial: false,
        });
    });

    test("a partly priced address keeps the part it can price", () => {
        expect(getAddressValue(PARTLY_PRICED)).toEqual({
            usd: 3000,
            partial: true,
        });
    });

    // A token balance of zero is not a holding, so it cannot make the total
    // incomplete: an address with a spent-out unpriced token is worth zero.
    test("a zero balance in an unpriced token leaves the value complete", () => {
        const addr = {
            address: "0x1",
            balance: "0",
            tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "0" }],
        };
        expect(getAddressValue(addr)).toEqual({ usd: 0, partial: false });
    });

    // Before the first price fetch, and on testnet, nothing is knowable: that
    // is a third state, and it stays distinct from both of the others.
    test("no prices at all means no value, not an incomplete one", () => {
        clearPrices();
        expect(getAddressValue(FULLY_PRICED)).toEqual({
            usd: null,
            partial: false,
        });
    });

    test("one unpriced holding makes a wallet and the grand total partial", () => {
        const wallet = { addresses: [FULLY_PRICED, UNPRICED_ONLY] };
        expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: true });
        expect(getTotalValue([wallet])).toEqual({ usd: 5500, partial: true });
    });

    test("a wallet of fully priced addresses stays complete", () => {
        const wallet = { addresses: [FULLY_PRICED, EMPTY] };
        expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: false });
    });
});

describe("how that value is written on screen", () => {
    test("a complete total is the figure", () => {
        expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe(
            "Total: $5,500.00",
        );
    });

    test("an address worth zero says so", () => {
        expect(formatAddressTotal(getAddressValue(EMPTY))).toBe("Total: $0.00");
    });

    // The figure is still worth having — it is the ETH the user does hold —
    // but on its own it understates the address, so it is named as partial.
    test("a partly priced total is given, and marked as partial", () => {
        expect(formatAddressTotal(getAddressValue(PARTLY_PRICED))).toBe(
            "Total: $3,000.00 plus unpriced tokens",
        );
    });

    // Nothing priced is held, so there is no figure to give: printing the
    // $0.00 sum of an empty set is the bug.
    test("a total with nothing priced in it gives no figure", () => {
        const line = formatAddressTotal(getAddressValue(UNPRICED_ONLY));
        expect(line).toBe("Total: unpriced tokens only");
        expect(line).not.toContain("$");
    });

    test("an unknown value is written as nothing at all", () => {
        clearPrices();
        expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe("");
    });
});

describe("the wallet list on Home", () => {
    test("an address holding only unpriced tokens is not totalled at $0.00", () => {
        expect(walletListTotal(UNPRICED_ONLY)).toBe(
            "Total: unpriced tokens only",
        );
    });

    test("an address holding nothing is still totalled at $0.00", () => {
        expect(walletListTotal(EMPTY)).toBe("Total: $0.00");
    });

    test("a fully priced address shows its total", () => {
        expect(walletListTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
    });

    test("a partly priced address shows the priced part, marked partial", () => {
        expect(walletListTotal(PARTLY_PRICED)).toBe(
            "Total: $3,000.00 plus unpriced tokens",
        );
    });

    test("an address whose value is unknown keeps its blank line", () => {
        clearPrices();
        expect(walletListTotal(FULLY_PRICED)).toBe("&nbsp;");
    });
});

describe("the balance warning on the address-removal confirmation", () => {
    // "This address holds a balance." followed by "Total: $0.00" is a flat
    // contradiction, on the one screen whose job is to warn.
    test("an address holding only unpriced tokens is not totalled at $0.00", () => {
        expect(balanceWarningHtml(UNPRICED_ONLY)).toContain(
            "This address holds a balance.",
        );
        expect(removalWarningTotal(UNPRICED_ONLY)).toBe(
            "Total: unpriced tokens only",
        );
    });

    test("a fully priced address still shows its total", () => {
        expect(removalWarningTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
    });

    test("a partly priced address shows the priced part, marked partial", () => {
        expect(removalWarningTotal(PARTLY_PRICED)).toBe(
            "Total: $3,000.00 plus unpriced tokens",
        );
    });

    test("no total line is written when the value is unknown", () => {
        clearPrices();
        expect(removalWarningTotal(FULLY_PRICED)).toBe(null);
    });
});
