/**
 * Tests for services/ui.js
 *
 * Tests UI utility functions: progress bars, spinners, error display,
 * inline errors, escapeHtmlFallback, and notification system.
 */

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

const ui = window.ui;

describe('ui service', () => {
    describe('updateProgressBar', () => {
        let fill, pct;

        beforeEach(() => {
            fill = document.createElement('div');
            fill.id = 'test-fill';
            pct = document.createElement('span');
            pct.id = 'test-pct';
            document.body.appendChild(fill);
            document.body.appendChild(pct);
        });

        afterEach(() => {
            fill.remove();
            pct.remove();
        });

        it('sets width and text for percentage', () => {
            ui.updateProgressBar(fill, pct, 42);
            expect(fill.style.width).toBe('42%');
            expect(pct.textContent).toBe('42%');
        });

        it('clamps percentage to 0-100', () => {
            ui.updateProgressBar(fill, pct, -10);
            expect(fill.style.width).toBe('0%');
            expect(pct.textContent).toBe('0%');

            ui.updateProgressBar(fill, pct, 150);
            expect(fill.style.width).toBe('100%');
            expect(pct.textContent).toBe('100%');
        });

        it('handles null/undefined percentage as 0', () => {
            ui.updateProgressBar(fill, pct, null);
            expect(fill.style.width).toBe('0%');
        });

        it('adds ldr-complete class at 100%', () => {
            ui.updateProgressBar(fill, pct, 100);
            expect(fill.classList.contains('ldr-complete')).toBe(true);
        });

        it('removes ldr-complete class when below 100%', () => {
            fill.classList.add('ldr-complete');
            ui.updateProgressBar(fill, pct, 50);
            expect(fill.classList.contains('ldr-complete')).toBe(false);
        });

        it('accepts string IDs instead of elements', () => {
            ui.updateProgressBar('test-fill', 'test-pct', 75);
            expect(fill.style.width).toBe('75%');
            expect(pct.textContent).toBe('75%');
        });

        it('rounds percentage text', () => {
            ui.updateProgressBar(fill, pct, 33.7);
            expect(pct.textContent).toBe('34%');
        });
    });

    describe('showSpinner / hideSpinner', () => {
        let container;

        beforeEach(() => {
            container = document.createElement('div');
            container.id = 'spinner-container';
            document.body.appendChild(container);
        });

        afterEach(() => {
            container.remove();
        });

        it('creates a spinner element in the container', () => {
            ui.showSpinner(container, 'Loading data...');
            expect(container.querySelector('.ldr-loading-spinner')).not.toBeNull();
        });

        it('displays the message text', () => {
            ui.showSpinner(container, 'Please wait');
            expect(container.textContent).toContain('Please wait');
        });

        it('escapes HTML in the message', () => {
            ui.showSpinner(container, '<script>alert(1)</script>');
            expect(container.innerHTML).not.toContain('<script>');
        });

        it('hideSpinner removes the spinner', () => {
            ui.showSpinner(container);
            expect(container.querySelector('.ldr-loading-spinner')).not.toBeNull();
            ui.hideSpinner(container);
            expect(container.querySelector('.ldr-loading-spinner')).toBeNull();
        });

        it('hideSpinner does nothing when no spinner exists', () => {
            expect(() => ui.hideSpinner(container)).not.toThrow();
        });

        it('accepts string IDs', () => {
            ui.showSpinner('spinner-container', 'Test');
            expect(container.querySelector('.ldr-loading-spinner')).not.toBeNull();
            ui.hideSpinner('spinner-container');
            expect(container.querySelector('.ldr-loading-spinner')).toBeNull();
        });
    });

    describe('showError', () => {
        let container;

        beforeEach(() => {
            container = document.createElement('div');
            container.id = 'error-container';
            document.body.appendChild(container);
        });

        afterEach(() => {
            container.remove();
        });

        it('shows error message in container', () => {
            ui.showError(container, 'Something went wrong');
            expect(container.textContent).toContain('Something went wrong');
        });

        it('escapes HTML in error message', () => {
            ui.showError(container, '<img onerror="xss">');
            expect(container.innerHTML).not.toContain('<img');
        });

        it('creates error element with icon', () => {
            ui.showError(container, 'Error');
            expect(container.querySelector('.ldr-error-message')).not.toBeNull();
            expect(container.querySelector('.fa-exclamation-circle')).not.toBeNull();
        });
    });

    describe('showInlineError / clearInlineError', () => {
        let container;

        beforeEach(() => {
            container = document.createElement('div');
            container.id = 'inline-error-container';
            document.body.appendChild(container);
        });

        afterEach(() => {
            container.remove();
        });

        it('creates an inline error element', () => {
            const el = ui.showInlineError(container, 'Field is required');
            expect(el).not.toBeNull();
            expect(el.classList.contains('ldr-inline-error')).toBe(true);
            expect(el.textContent).toContain('Field is required');
        });

        it('sets role="alert" for accessibility', () => {
            const el = ui.showInlineError(container, 'Error');
            expect(el.getAttribute('role')).toBe('alert');
        });

        it('adds dismiss button by default', () => {
            const el = ui.showInlineError(container, 'Error');
            const closeBtn = el.querySelector('.ldr-inline-error-close');
            expect(closeBtn).not.toBeNull();
            expect(closeBtn.getAttribute('aria-label')).toBe('Dismiss error');
        });

        it('dismiss button removes error', () => {
            const el = ui.showInlineError(container, 'Error');
            const closeBtn = el.querySelector('.ldr-inline-error-close');
            closeBtn.click();
            expect(container.querySelector('.ldr-inline-error')).toBeNull();
        });

        it('replaces existing inline error', () => {
            ui.showInlineError(container, 'First error');
            ui.showInlineError(container, 'Second error');
            const errors = container.querySelectorAll('.ldr-inline-error');
            expect(errors.length).toBe(1);
            expect(errors[0].textContent).toContain('Second error');
        });

        it('clearInlineError removes all errors', () => {
            ui.showInlineError(container, 'Error 1');
            ui.clearInlineError(container);
            expect(container.querySelector('.ldr-inline-error')).toBeNull();
        });

        it('returns null for non-existent container', () => {
            expect(ui.showInlineError('#nonexistent', 'Error')).toBeNull();
        });

        it('accepts string ID for container', () => {
            const el = ui.showInlineError('inline-error-container', 'Test');
            expect(el).not.toBeNull();
        });

        it('uses textContent (not innerHTML) for message', () => {
            const el = ui.showInlineError(container, '<script>xss</script>');
            const span = el.querySelector('span');
            expect(span.textContent).toBe('<script>xss</script>');
            expect(span.innerHTML).not.toContain('<script>');
        });
    });

    describe('showMessage', () => {
        afterEach(() => {
            // Persistent banners stay in the DOM by design; clean up
            // between tests so each test starts fresh.
            document
                .getElementById('notification-banner-polite')
                ?.remove();
            document
                .getElementById('notification-banner-assertive')
                ?.remove();
        });

        it('shows the polite banner for success messages', () => {
            ui.showMessage('Saved!', 'success');
            const polite = document.getElementById(
                'notification-banner-polite',
            );
            expect(polite).not.toBeNull();
            expect(polite.textContent).toContain('Saved!');
            expect(polite.getAttribute('role')).toBe('status');
            expect(polite.getAttribute('aria-live')).toBe('polite');
        });

        it('shows the assertive banner for error messages', () => {
            ui.showMessage('Error occurred', 'error');
            const assertive = document.getElementById(
                'notification-banner-assertive',
            );
            expect(assertive).not.toBeNull();
            expect(assertive.textContent).toContain('Error occurred');
            expect(assertive.getAttribute('role')).toBe('alert');
            expect(assertive.getAttribute('aria-live')).toBe('assertive');
        });

        it('uses the assertive banner for warnings', () => {
            ui.showMessage('Heads up', 'warning');
            const assertive = document.getElementById(
                'notification-banner-assertive',
            );
            expect(assertive.textContent).toContain('Heads up');
        });

        it('uses the polite banner for info messages', () => {
            ui.showMessage('FYI', 'info');
            const polite = document.getElementById(
                'notification-banner-polite',
            );
            expect(polite.textContent).toContain('FYI');
        });

        it('escapes HTML by using textContent', () => {
            ui.showMessage('<img src=x onerror=alert(1)>', 'info');
            const polite = document.getElementById(
                'notification-banner-polite',
            );
            // textContent is set, not innerHTML, so the markup is text.
            expect(polite.textContent).toContain(
                '<img src=x onerror=alert(1)>',
            );
            expect(polite.querySelector('img')).toBeNull();
        });

        it('reuses the same banner element across calls', () => {
            ui.showMessage('First', 'success');
            const firstNode = document.getElementById(
                'notification-banner-polite',
            );
            ui.showMessage('Second', 'info');
            const secondNode = document.getElementById(
                'notification-banner-polite',
            );
            // Same DOM node — live regions must persist for the
            // screen reader to announce subsequent updates.
            expect(secondNode).toBe(firstNode);
            expect(secondNode.textContent).toContain('Second');
            expect(secondNode.textContent).not.toContain('First');
        });

        it('switches between banners when type changes', () => {
            ui.showMessage('Saved', 'success');
            ui.showMessage('Boom', 'error');
            const polite = document.getElementById(
                'notification-banner-polite',
            );
            const assertive = document.getElementById(
                'notification-banner-assertive',
            );
            expect(assertive.textContent).toContain('Boom');
            // Polite is hidden via transform but its prior text
            // is still in the DOM; the visible banner is assertive.
            expect(polite.style.transform).toContain('-100%');
        });
    });
});

describe('renderMarkdown', () => {
    it('returns warning HTML for null/empty input', () => {
        const result = ui.renderMarkdown(null);
        expect(result).toContain('No content available');
    });

    it('returns plaintext fallback when marked is unavailable', () => {
        // happy-dom doesn't have `marked` loaded
        const result = ui.renderMarkdown('# Hello World');
        // Should escape HTML and show as plaintext
        expect(result).toContain('Hello World');
    });
});
