// The escape every view depends on, and the length bound on a displayed
// token symbol. Both were added for #307, where a token whose symbol()
// returned an <iframe> tag rendered that iframe inside the popup.

const { escapeHtml } = require("../src/shared/html");
const {
    displaySymbol,
    MAX_SYMBOL_LENGTH,
    UNKNOWN_SYMBOL,
} = require("../src/shared/symbolDisplay");

// The payload from the issue's reproduction, verbatim.
const HOSTILE_SYMBOL =
    '<iframe id="pwn" src="https://dapp.e2e.test/" ' +
    'style="position:fixed;left:0;top:0;width:360px;height:600px;z-index:99999"></iframe>';

describe("escapeHtml", () => {
    test("escapes all five characters, quotes included", () => {
        expect(escapeHtml("&<>\"'")).toBe("&amp;&lt;&gt;&quot;&#39;");
    });

    // The regression this function was rewritten for. The previous
    // implementation round-tripped through a detached div's textContent,
    // and an HTML text node serializes a quote as itself — so a value with
    // a quote in it broke straight out of data-copy="..." and href="...".
    test("escapes quotes, which the textContent round trip did not", () => {
        expect(escapeHtml('a"b')).toBe("a&quot;b");
        expect(escapeHtml("a'b")).toBe("a&#39;b");
    });

    test("does not double-escape an ampersand it just introduced", () => {
        expect(escapeHtml("&lt;")).toBe("&amp;lt;");
        expect(escapeHtml("&amp;")).toBe("&amp;amp;");
    });

    test("leaves a string with nothing to escape untouched", () => {
        expect(escapeHtml("USDC")).toBe("USDC");
        expect(escapeHtml("")).toBe("");
    });

    test("renders the hostile symbol inert", () => {
        const out = escapeHtml(HOSTILE_SYMBOL);
        expect(out).not.toContain("<");
        expect(out).not.toContain(">");
        expect(out).not.toContain('"');
        expect(out).toContain("&lt;iframe");
    });

    // A quoted attribute is broken out of by a quote, a bare one by a
    // space; both are closed here. Asserted as a whole attribute rather
    // than character by character, because it is the attribute that has to
    // survive, not the escape table.
    test("a value carrying a quote stays inside its attribute", () => {
        const evil = '" onload="alert(1)';
        const attr = `data-copy="${escapeHtml(evil)}"`;
        expect(attr).toBe('data-copy="&quot; onload=&quot;alert(1)"');
        expect(attr.split('"').length - 1).toBe(2);
    });

    test("null and undefined render as nothing rather than as words", () => {
        expect(escapeHtml(null)).toBe("");
        expect(escapeHtml(undefined)).toBe("");
    });

    test("coerces a non-string without losing the escape", () => {
        expect(escapeHtml(42)).toBe("42");
        expect(escapeHtml({ toString: () => "<b>" })).toBe("&lt;b&gt;");
    });
});

describe("displaySymbol", () => {
    test("passes every symbol in the bundled list through unchanged", () => {
        const { TOKENS } = require("../src/shared/tokenList");
        for (const t of TOKENS) {
            expect([t.address, displaySymbol(t.symbol)]).toEqual([
                t.address,
                t.symbol,
            ]);
        }
    });

    test("caps an over-long symbol and marks it as truncated", () => {
        const long = "A".repeat(4096);
        const out = displaySymbol(long);
        expect(out.length).toBe(MAX_SYMBOL_LENGTH);
        expect(out.endsWith("…")).toBe(true);
    });

    test("keeps a symbol of exactly the cap intact", () => {
        const exact = "A".repeat(MAX_SYMBOL_LENGTH);
        expect(displaySymbol(exact)).toBe(exact);
    });

    test("substitutes a placeholder for an absent symbol", () => {
        expect(displaySymbol("")).toBe(UNKNOWN_SYMBOL);
        expect(displaySymbol(null)).toBe(UNKNOWN_SYMBOL);
        expect(displaySymbol(undefined)).toBe(UNKNOWN_SYMBOL);
    });

    // The cap is a layout bound and nothing more: it must not be mistaken
    // for the thing that makes a symbol safe to render. A short hostile
    // symbol passes through it untouched, and is inert only because the
    // caller escapes it afterwards.
    test("does not sanitize — a short markup symbol survives it verbatim", () => {
        expect(displaySymbol("<img src=x>")).toBe("<img src=x>");
        expect(escapeHtml(displaySymbol("<img src=x>"))).toBe(
            "&lt;img src=x&gt;",
        );
    });
});
