/**
 * Tests for security/xss-protection.js
 *
 * Tests HTML escaping, sanitization, and safe DOM manipulation
 * functions that protect against XSS attacks.
 */

import '@js/security/xss-protection.js';

const { escapeHtml, escapeHtmlAttribute, sanitizeUserInput,
        safeSetInnerHTML, safeSetTextContent } = window;

describe('escapeHtml', () => {
    it('escapes ampersands', () => {
        expect(escapeHtml('a & b')).toBe('a &amp; b');
    });

    it('escapes angle brackets', () => {
        expect(escapeHtml('<script>alert(1)</script>')).toBe(
            '&lt;script&gt;alert(1)&lt;&#x2F;script&gt;'
        );
    });

    it('escapes double quotes', () => {
        expect(escapeHtml('"hello"')).toBe('&quot;hello&quot;');
    });

    it('escapes single quotes', () => {
        expect(escapeHtml("it's")).toBe('it&#39;s');
    });

    it('escapes forward slashes', () => {
        expect(escapeHtml('a/b')).toBe('a&#x2F;b');
    });

    it('returns empty string for null', () => {
        expect(escapeHtml(null)).toBe('');
    });

    it('returns empty string for undefined', () => {
        expect(escapeHtml(undefined)).toBe('');
    });

    it('converts non-strings to string first', () => {
        expect(escapeHtml(123)).toBe('123');
        expect(escapeHtml(true)).toBe('true');
    });

    it('handles strings with no special characters', () => {
        expect(escapeHtml('hello world')).toBe('hello world');
    });

    it('handles empty string', () => {
        expect(escapeHtml('')).toBe('');
    });

    it('escapes all dangerous characters together', () => {
        expect(escapeHtml('<img onerror="alert(\'xss\')" />')).toBe(
            '&lt;img onerror=&quot;alert(&#39;xss&#39;)&quot; &#x2F;&gt;'
        );
    });
});

describe('escapeHtmlAttribute', () => {
    it('escapes double quotes for attribute context', () => {
        expect(escapeHtmlAttribute('value"with"quotes')).toContain('&quot;');
    });

    it('escapes angle brackets', () => {
        expect(escapeHtmlAttribute('<tag>')).toContain('&lt;');
    });

    it('converts non-strings', () => {
        expect(escapeHtmlAttribute(42)).toBe('42');
    });
});

describe('sanitizeUserInput', () => {
    it('returns empty string for null/undefined', () => {
        expect(sanitizeUserInput(null)).toBe('');
        expect(sanitizeUserInput(undefined)).toBe('');
    });

    it('trims whitespace by default', () => {
        const result = sanitizeUserInput('  hello  ');
        expect(result).not.toMatch(/^\s/);
        expect(result).not.toMatch(/\s$/);
    });

    it('does not trim when option is false', () => {
        const result = sanitizeUserInput('  hello  ', { trimWhitespace: false });
        // The result is escaped, so check it contains the text
        expect(result).toContain('hello');
    });

    it('enforces maxLength', () => {
        const long = 'a'.repeat(200);
        const result = sanitizeUserInput(long, { maxLength: 10 });
        // After escaping, the core content is truncated to 10 chars
        expect(result.length).toBeLessThanOrEqual(10);
    });

    it('normalizes line breaks', () => {
        const input = 'line1\r\nline2\rline3';
        const result = sanitizeUserInput(input, { allowLineBreaks: true });
        expect(result).not.toContain('\r');
    });

    it('escapes HTML by default (allowHtml=false)', () => {
        const result = sanitizeUserInput('<script>alert(1)</script>');
        expect(result).toContain('&lt;');
        expect(result).not.toContain('<script>');
    });

    it('converts non-string input to string', () => {
        expect(sanitizeUserInput(42)).toBe('42');
    });
});

describe('safeSetInnerHTML', () => {
    it('does nothing when element is null', () => {
        expect(() => safeSetInnerHTML(null, 'content')).not.toThrow();
    });

    it('clears content when content is empty/falsy', () => {
        const el = document.createElement('div');
        el.textContent = 'existing';
        safeSetInnerHTML(el, '');
        expect(el.textContent).toBe('');
    });

    it('sets textContent when allowHtmlTags is false', () => {
        const el = document.createElement('div');
        safeSetInnerHTML(el, '<b>bold</b>', false);
        expect(el.textContent).toBe('<b>bold</b>');
        expect(el.innerHTML).not.toContain('<b>');
    });

    it('sets textContent when content is null', () => {
        const el = document.createElement('div');
        el.textContent = 'existing';
        safeSetInnerHTML(el, null);
        expect(el.textContent).toBe('');
    });
});

describe('safeSetTextContent', () => {
    it('does nothing when element is null', () => {
        expect(() => safeSetTextContent(null, 'content')).not.toThrow();
    });

    it('sets text content safely', () => {
        const el = document.createElement('div');
        safeSetTextContent(el, 'hello world');
        expect(el.textContent).toBe('hello world');
    });

    it('converts content to string', () => {
        const el = document.createElement('div');
        safeSetTextContent(el, 123);
        expect(el.textContent).toBe('123');
    });

    it('handles null content gracefully', () => {
        const el = document.createElement('div');
        safeSetTextContent(el, null);
        expect(el.textContent).toBe('');
    });

    it('handles undefined content gracefully', () => {
        const el = document.createElement('div');
        safeSetTextContent(el, undefined);
        expect(el.textContent).toBe('');
    });
});

describe('escapeHtml fuzz / property tests', () => {
    // The most critical security invariant: after escapeHtml, the result
    // should never contain raw <, >, or unescaped quotes that could break
    // out of an HTML context. We fuzz with random injection-like strings
    // to catch any escaping regression.

    const generatePayload = () => {
        const fragments = [
            '<script>', '</script>', '<img src=x onerror=alert(1)>',
            '"><svg/onload=', "'><iframe", '&#x27;', 'javascript:',
            '"', "'", '<', '>', '&', '/', '`', '${', 'data:text/html,',
            '<style>', '</style>', '</textarea>',
        ];
        // Build a random combination
        const len = Math.floor(Math.random() * 5) + 1;
        let payload = '';
        for (let i = 0; i < len; i++) {
            payload += fragments[Math.floor(Math.random() * fragments.length)];
            // Insert random characters between
            const noise = Math.floor(Math.random() * 5);
            for (let j = 0; j < noise; j++) {
                payload += String.fromCharCode(Math.floor(Math.random() * 95) + 32);
            }
        }
        return payload;
    };

    it('escaped output never contains raw < or > (200 random payloads)', () => {
        for (let i = 0; i < 200; i++) {
            const payload = generatePayload();
            const escaped = escapeHtml(payload);
            expect(escaped).not.toMatch(/</);
            expect(escaped).not.toMatch(/>/);
        }
    });

    it('escaped output never contains raw double quotes', () => {
        for (let i = 0; i < 200; i++) {
            const payload = generatePayload();
            const escaped = escapeHtml(payload);
            expect(escaped).not.toMatch(/"/);
        }
    });

    it('escaped output is parseable as text content (round-trips through DOM)', () => {
        // If escaping works, the escaped string parsed as innerHTML and read
        // back via textContent should equal the original input.
        const div = document.createElement('div');
        for (let i = 0; i < 100; i++) {
            const payload = generatePayload();
            const escaped = escapeHtml(payload);
            // eslint-disable-next-line no-unsanitized/property -- test code
            div.innerHTML = escaped;
            // textContent decodes the entities back to the original
            expect(div.textContent).toBe(payload);
            // And no script/element tags were created
            expect(div.children.length).toBe(0);
        }
    });

    it('escaped output never creates executable elements', () => {
        const div = document.createElement('div');
        // Specific known XSS vectors
        const vectors = [
            '<script>alert(1)</script>',
            '<img src=x onerror=alert(1)>',
            '<svg onload=alert(1)>',
            '"><script>alert(1)</script>',
            '<iframe src=javascript:alert(1)>',
            '<a href="javascript:alert(1)">x</a>',
        ];
        for (const vector of vectors) {
            div.innerHTML = escapeHtml(vector);
            // No element children should have been created
            expect(div.children.length).toBe(0);
            expect(div.querySelectorAll('script').length).toBe(0);
            expect(div.querySelectorAll('img').length).toBe(0);
            expect(div.querySelectorAll('svg').length).toBe(0);
            expect(div.querySelectorAll('iframe').length).toBe(0);
        }
    });

    it('idempotent: escaping an already-escaped string does not create XSS', () => {
        // Escaping twice should still be safe (no element creation)
        const div = document.createElement('div');
        for (let i = 0; i < 50; i++) {
            const payload = generatePayload();
            const escapedTwice = escapeHtml(escapeHtml(payload));
            // eslint-disable-next-line no-unsanitized/property -- test code
            div.innerHTML = escapedTwice;
            expect(div.children.length).toBe(0);
        }
    });
});
