/**
 * JavaScript tests for XSS Protection and Dropdown Highlighting
 * Run with: npm test tests/infrastructure_tests/test_xss_protection.test.js
 *
 * Tests the safeSetInnerHTML function and dropdown search highlighting
 * which requires DOMPurify to be loaded.
 */

// Mock DOMPurify for testing
const mockDOMPurify = {
    sanitize: jest.fn((content) => {
        // Simple mock that allows span tags with class attribute
        // but removes script tags
        let result = content;
        // Remove script tags
        result = result.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
        // Keep allowed tags like span with class
        return result;
    }),
    addHook: jest.fn((hookName, callback) => {
        // Mock addHook for tabnabbing protection
        // Stores callbacks but doesn't execute them in tests
        mockDOMPurify._hooks ||= {};
        mockDOMPurify._hooks[hookName] ||= [];
        mockDOMPurify._hooks[hookName].push(callback);
    }),
    _hooks: {}
};

// Setup global mocks
global.DOMPurify = mockDOMPurify;
global.window = {
    DOMPurify: mockDOMPurify
};
global.document = {
    createElement: jest.fn(() => ({
        className: '',
        textContent: '',
        innerHTML: '',
        style: {},
        setAttribute: jest.fn(),
        appendChild: jest.fn(),
        removeChild: jest.fn(),
        addEventListener: jest.fn(),
        querySelectorAll: jest.fn(() => []),
        firstChild: null
    }))
};

// Load the XSS protection module
require('../../src/local_deep_research/web/static/js/security/xss-protection.js');

describe('XSS Protection Module', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });

    describe('DOMPurify Integration', () => {
        test('DOMPurify should be available globally', () => {
            expect(global.DOMPurify).toBeDefined();
            expect(global.DOMPurify.sanitize).toBeDefined();
        });

        test('window.DOMPurify should be set', () => {
            expect(global.window.DOMPurify).toBeDefined();
        });
    });

    describe('escapeHtml', () => {
        test('should escape HTML special characters', () => {
            const { escapeHtml } = global.window;
            expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
            expect(escapeHtml('a & b')).toBe('a &amp; b');
            expect(escapeHtml('"quoted"')).toBe('&quot;quoted&quot;');
            expect(escapeHtml("'single'")).toBe('&#39;single&#39;');
        });

        test('should handle non-string input', () => {
            const { escapeHtml } = global.window;
            expect(escapeHtml(123)).toBe('123');
            expect(escapeHtml(null)).toBe('');
            expect(escapeHtml(undefined)).toBe('');
        });
    });

    describe('safeSetInnerHTML', () => {
        test('should use DOMPurify when allowHtmlTags is true', () => {
            const { safeSetInnerHTML } = global.window;
            const element = {
                innerHTML: '',
                textContent: ''
            };

            const htmlContent = '<span class="ldr-highlight">test</span>';
            safeSetInnerHTML(element, htmlContent, true);

            expect(mockDOMPurify.sanitize).toHaveBeenCalled();
            expect(element.innerHTML).toBe(htmlContent);
        });

        test('should use textContent when allowHtmlTags is false', () => {
            const { safeSetInnerHTML } = global.window;
            const element = {
                innerHTML: '',
                textContent: ''
            };

            const content = 'plain text';
            safeSetInnerHTML(element, content, false);

            expect(element.textContent).toBe(content);
        });

        test('should handle null element gracefully', () => {
            const { safeSetInnerHTML } = global.window;
            expect(() => safeSetInnerHTML(null, 'content')).not.toThrow();
        });

        test('should handle null content gracefully', () => {
            const { safeSetInnerHTML } = global.window;
            const element = { innerHTML: '', textContent: '' };
            expect(() => safeSetInnerHTML(element, null)).not.toThrow();
        });
    });

    describe('safeSetTextContent', () => {
        test('should set textContent safely', () => {
            const { safeSetTextContent } = global.window;
            const element = { textContent: '' };

            safeSetTextContent(element, '<script>alert("xss")</script>');
            expect(element.textContent).toBe('<script>alert("xss")</script>');
        });

        test('should handle null element gracefully', () => {
            const { safeSetTextContent } = global.window;
            expect(() => safeSetTextContent(null, 'content')).not.toThrow();
        });
    });

    describe('sanitizeHtml', () => {
        test('should use DOMPurify to sanitize HTML', () => {
            const { sanitizeHtml } = global.window;
            const dirty = '<script>alert("xss")</script><b>safe</b>';
            const result = sanitizeHtml(dirty);

            expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
                dirty,
                expect.any(Object)
            );
        });

        test('should return empty string for empty input', () => {
            const { sanitizeHtml } = global.window;
            expect(sanitizeHtml('')).toBe('');
            expect(sanitizeHtml(null)).toBe('');
            expect(sanitizeHtml(undefined)).toBe('');
        });
    });
});

describe('Dropdown Highlight Rendering', () => {
    describe('Highlight HTML Structure', () => {
        test('highlight span should have correct class', () => {
            const highlightHtml = '<span class="ldr-highlight">search</span>';
            expect(highlightHtml).toContain('class="ldr-highlight"');
        });

        test('highlight pattern should match search terms', () => {
            // Simulate the highlightText function from custom_dropdown.js (indexOf implementation)
            const showAllOptions = false;
            function highlightText(text, search) {
                if (!search.trim() || showAllOptions) return text;
                const lowerText = text.toLowerCase();
                const lowerSearch = search.toLowerCase();
                let result = '';
                let lastIdx = 0;
                let idx;
                while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) {
                    result += text.substring(lastIdx, idx);
                    result += '<span class="ldr-highlight">' + text.substring(idx, idx + search.length) + '</span>';
                    lastIdx = idx + search.length;
                }
                result += text.substring(lastIdx);
                return result;
            }

            const result = highlightText('bytedance-seed/seed-1.6-flash-h', 'flas');
            expect(result).toBe('bytedance-seed/seed-1.6-<span class="ldr-highlight">flas</span>h-h');
        });

        test('highlight should be case-insensitive', () => {
            const showAllOptions = false;
            function highlightText(text, search) {
                if (!search.trim() || showAllOptions) return text;
                const lowerText = text.toLowerCase();
                const lowerSearch = search.toLowerCase();
                let result = '';
                let lastIdx = 0;
                let idx;
                while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) {
                    result += text.substring(lastIdx, idx);
                    result += '<span class="ldr-highlight">' + text.substring(idx, idx + search.length) + '</span>';
                    lastIdx = idx + search.length;
                }
                result += text.substring(lastIdx);
                return result;
            }

            const result = highlightText('FLASH Model', 'flash');
            expect(result).toContain('ldr-highlight');
            expect(result).toContain('FLASH');
        });

        test('highlight should handle multiple matches', () => {
            const showAllOptions = false;
            function highlightText(text, search) {
                if (!search.trim() || showAllOptions) return text;
                const lowerText = text.toLowerCase();
                const lowerSearch = search.toLowerCase();
                let result = '';
                let lastIdx = 0;
                let idx;
                while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) {
                    result += text.substring(lastIdx, idx);
                    result += '<span class="ldr-highlight">' + text.substring(idx, idx + search.length) + '</span>';
                    lastIdx = idx + search.length;
                }
                result += text.substring(lastIdx);
                return result;
            }

            const result = highlightText('test test test', 'test');
            const matches = result.match(/ldr-highlight/g);
            expect(matches).toHaveLength(3);
        });

        test('highlight should handle special characters in search without error', () => {
            const showAllOptions = false;
            function highlightText(text, search) {
                if (!search.trim() || showAllOptions) return text;
                const lowerText = text.toLowerCase();
                const lowerSearch = search.toLowerCase();
                let result = '';
                let lastIdx = 0;
                let idx;
                while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) {
                    result += text.substring(lastIdx, idx);
                    result += '<span class="ldr-highlight">' + text.substring(idx, idx + search.length) + '</span>';
                    lastIdx = idx + search.length;
                }
                result += text.substring(lastIdx);
                return result;
            }

            // indexOf naturally handles special characters — no regex to break
            expect(() => highlightText('test (parentheses)', '(paren')).not.toThrow();
            expect(() => highlightText('test [brackets]', '[brack')).not.toThrow();
            expect(() => highlightText('test.dot', '.')).not.toThrow();
        });
    });

    describe('DOMPurify Sanitization Config', () => {
        test('should allow span tags', () => {
            const { sanitizeHtml } = global.window;
            const input = '<span class="ldr-highlight">text</span>';
            sanitizeHtml(input);

            // Verify DOMPurify was called with config that allows span
            expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
                input,
                expect.objectContaining({
                    ALLOWED_TAGS: expect.arrayContaining(['span'])
                })
            );
        });

        test('should allow class attribute', () => {
            const { sanitizeHtml } = global.window;
            const input = '<span class="ldr-highlight">text</span>';
            sanitizeHtml(input);

            expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
                input,
                expect.objectContaining({
                    ALLOWED_ATTR: expect.arrayContaining(['class'])
                })
            );
        });

        test('should forbid script tags', () => {
            const { sanitizeHtml } = global.window;
            const input = '<script>alert("xss")</script>';
            sanitizeHtml(input);

            expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
                input,
                expect.objectContaining({
                    FORBID_TAGS: expect.arrayContaining(['script'])
                })
            );
        });

        test('should forbid event handlers', () => {
            const { sanitizeHtml } = global.window;
            const input = '<div onclick="alert()">test</div>';
            sanitizeHtml(input);

            expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
                input,
                expect.objectContaining({
                    FORBID_ATTR: expect.arrayContaining(['onclick'])
                })
            );
        });
    });
});

describe('App.js DOMPurify Export', () => {
    test('DOMPurify should be exported to window object', () => {
        // This test verifies the fix - DOMPurify must be on window
        // for safeSetInnerHTML to use it instead of falling back to textContent
        expect(global.window.DOMPurify).toBeDefined();
        expect(typeof global.window.DOMPurify.sanitize).toBe('function');
    });
});

describe('DOMPurify KEEP_CONTENT Configuration', () => {
    test('should preserve text content when sanitizing highlighted dropdown items', () => {
        const { sanitizeHtml } = global.window;

        // Simulate a model name with highlighting like in dropdowns
        const modelWithHighlight = 'google/<span class="ldr-highlight">gemma</span>-3n-e2b-it:free (OPENROUTER)';
        const result = sanitizeHtml(modelWithHighlight);

        // Verify DOMPurify was called with KEEP_CONTENT: true
        // This ensures text content is preserved even if tags are processed
        expect(mockDOMPurify.sanitize).toHaveBeenCalledWith(
            modelWithHighlight,
            expect.objectContaining({
                KEEP_CONTENT: true
            })
        );
    });

    test('sanitized output should contain original text content', () => {
        // The mock preserves content, simulating KEEP_CONTENT: true behavior
        const input = 'google/<span class="ldr-highlight">gemma</span>-3n-e2b-it:free';
        const result = mockDOMPurify.sanitize(input, {});

        // Text content should be preserved, not empty
        expect(result).toContain('google/');
        expect(result).toContain('gemma');
        expect(result).toContain('-3n-e2b-it:free');
    });

    test('should handle complex model names with special characters', () => {
        const { sanitizeHtml } = global.window;

        // Test various model name formats that appear in dropdowns
        const testCases = [
            'meta-llama/llama-3.3-70b-instruct:free',
            'anthropic/claude-3.5-sonnet:beta',
            'openai/gpt-4o-2024-11-20',
            'bytedance-seed/seed-1.6-flash-h'
        ];

        testCases.forEach(modelName => {
            const highlighted = modelName.replace(/(llama|claude|gpt|seed)/gi,
                '<span class="ldr-highlight">$1</span>');
            const result = sanitizeHtml(highlighted);

            // Should call sanitize and preserve content
            expect(mockDOMPurify.sanitize).toHaveBeenCalled();
        });
    });
});

describe('Dynamic DOMPurify Detection', () => {
    test('safeSetInnerHTML should check for DOMPurify availability at call time', () => {
        const { safeSetInnerHTML } = global.window;
        const element = { innerHTML: '', textContent: '' };

        // Clear previous calls
        mockDOMPurify.sanitize.mockClear();

        // Call safeSetInnerHTML with HTML content
        safeSetInnerHTML(element, '<span class="ldr-highlight">test</span>', true);

        // DOMPurify should be used since it's available
        expect(mockDOMPurify.sanitize).toHaveBeenCalled();
    });

    test('safeSetInnerHTML should work even if DOMPurify loaded after script init', () => {
        // This test verifies the dynamic check behavior
        // In production, Vite modules load after regular scripts
        // The hasDOMPurify() function checks availability at call time, not load time

        const { safeSetInnerHTML } = global.window;
        const element = { innerHTML: '', textContent: '' };

        // Simulate calling after DOMPurify is available
        expect(global.DOMPurify).toBeDefined();

        safeSetInnerHTML(element, '<b>test</b>', true);

        // Should use DOMPurify, not fall back to textContent
        expect(mockDOMPurify.sanitize).toHaveBeenCalled();
        expect(element.innerHTML).toBeTruthy();
    });
});
