/**
 * Tests for services/theme.js
 *
 * Tests theme resolution, validation, cycling, and storage.
 * Network calls (server sync) are not tested here as they
 * require a running backend.
 */

// Theme metadata must be set BEFORE the module IIFE runs.
// ES module imports are hoisted, so we use dynamic import in beforeAll.

let theme;

beforeAll(async () => {
    // Set up theme metadata before loading the module
    window.LDR_THEME_METADATA = {
        'hashed': { label: 'Hashed', icon: 'fa-hashtag', group: 'core' },
        'light': { label: 'Light', icon: 'fa-sun', group: 'core' },
        'sepia': { label: 'Sepia', icon: 'fa-book', group: 'research' },
        'nord': { label: 'Nord', icon: 'fa-snowflake', group: 'dev' },
        'dracula': { label: 'Dracula', icon: 'fa-ghost', group: 'dev' },
        'system': { label: 'System', icon: 'fa-desktop', group: 'system' },
    };

    // Stub fetch for server sync calls
    globalThis.fetch = vi.fn(() =>
        Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) })
    );

    // Stub api for CSRF
    window.api = { getCsrfToken: () => '' };

    await import('@js/services/theme.js');
    theme = window.themeService;
});

describe('themeService', () => {
    afterEach(() => {
        localStorage.clear();
    });

    describe('VALID_THEMES is derived from injected metadata', () => {
        it('contains every key from window.LDR_THEME_METADATA', () => {
            // Catches a regression where the derivation is removed/hardcoded.
            const metadataKeys = Object.keys(window.LDR_THEME_METADATA);
            for (const key of metadataKeys) {
                expect(theme.VALID_THEMES).toContain(key);
            }
        });
    });

    describe('getEffectiveTheme', () => {
        it('resolves system to sepia when prefers-color-scheme is light', () => {
            // happy-dom matchMedia returns false for dark mode by default
            expect(theme.getEffectiveTheme('system')).toBe('sepia');
        });

        it('returns the theme itself for non-system themes', () => {
            expect(theme.getEffectiveTheme('hashed')).toBe('hashed');
            expect(theme.getEffectiveTheme('light')).toBe('light');
            expect(theme.getEffectiveTheme('nord')).toBe('nord');
        });
    });

    describe('getCurrentTheme / setTheme', () => {
        it('defaults to system when no theme stored', () => {
            localStorage.clear();
            expect(theme.getCurrentTheme()).toBe('system');
        });

        it('stores and retrieves theme', () => {
            theme.setTheme('nord', false);
            expect(theme.getCurrentTheme()).toBe('nord');
        });

        it('falls back to hashed for invalid theme', () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
            theme.setTheme('nonexistent-theme', false);
            expect(theme.getCurrentTheme()).toBe('hashed');
            warnSpy.mockRestore();
        });
    });

    describe('applyTheme', () => {
        it('sets data-theme attribute on documentElement', () => {
            theme.applyTheme('nord');
            expect(document.documentElement.getAttribute('data-theme')).toBe('nord');
        });

        it('resolves system theme before applying', () => {
            theme.applyTheme('system');
            const applied = document.documentElement.getAttribute('data-theme');
            expect(['sepia', 'hashed']).toContain(applied);
        });

        it('dispatches themechange event', () => {
            const handler = vi.fn();
            window.addEventListener('themechange', handler);
            theme.applyTheme('light');
            expect(handler).toHaveBeenCalledTimes(1);
            expect(handler.mock.calls[0][0].detail.theme).toBe('light');
            window.removeEventListener('themechange', handler);
        });
    });

    describe('cycleTheme', () => {
        it('cycles through THEME_CYCLE order', () => {
            theme.setTheme('hashed', false);
            const next = theme.cycleTheme();
            expect(next).toBe('light');
        });

        it('wraps around to first theme after last', () => {
            theme.setTheme('dracula', false);
            const next = theme.cycleTheme();
            expect(next).toBe('hashed');
        });

        it('starts from beginning when current theme is not in cycle', () => {
            theme.setTheme('sepia', false);
            const next = theme.cycleTheme();
            // sepia is not in cycle, index=-1 → becomes 0 → 'hashed'
            expect(next).toBe('hashed');
        });
    });

    describe('clearTheme', () => {
        it('removes theme from localStorage', () => {
            theme.setTheme('nord', false);
            expect(theme.getCurrentTheme()).toBe('nord');
            theme.clearTheme();
            expect(theme.getCurrentTheme()).toBe('system');
        });
    });

    describe('clearAllThemes', () => {
        it('removes all theme keys from localStorage', () => {
            theme.setTheme('nord', false);
            localStorage.setItem('ldr-theme-user2', 'light');
            theme.clearAllThemes();
            expect(localStorage.getItem('ldr-theme-user2')).toBeNull();
        });
    });

    describe('populateThemeDropdown', () => {
        it('populates a select element with theme options', () => {
            const select = document.createElement('select');
            theme.populateThemeDropdown(select);
            const optgroups = select.querySelectorAll('optgroup');
            expect(optgroups.length).toBeGreaterThan(0);
            const options = select.querySelectorAll('option');
            expect(options.length).toBe(Object.keys(window.LDR_THEME_METADATA).length);
        });

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

    describe('saveThemeToServer', () => {
        // Restore each test's fetch mock and CSRF stub so we don't leak
        // assertions across the outer describe block's later tests.
        let originalGetCsrfToken;
        beforeEach(() => {
            originalGetCsrfToken = window.api.getCsrfToken;
        });
        afterEach(() => {
            window.api.getCsrfToken = originalGetCsrfToken;
        });

        it('returns Promise.resolve() without calling fetch when CSRF is missing', async () => {
            window.api.getCsrfToken = () => '';
            const fetchSpy = vi.fn();
            globalThis.fetch = fetchSpy;

            await theme.saveThemeToServer('nord');

            expect(fetchSpy).not.toHaveBeenCalled();
        });

        it('PUTs to /settings/api/app.theme with CSRF header and JSON body', async () => {
            window.api.getCsrfToken = () => 'csrf-test-123';
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}) })
            );

            await theme.saveThemeToServer('nord');

            expect(globalThis.fetch).toHaveBeenCalledTimes(1);
            const [url, init] = globalThis.fetch.mock.calls[0];
            expect(url).toBe('/settings/api/app.theme');
            expect(init.method).toBe('PUT');
            expect(init.headers['Content-Type']).toBe('application/json');
            expect(init.headers['X-CSRFToken']).toBe('csrf-test-123');
            expect(JSON.parse(init.body)).toEqual({ value: 'nord' });
        });

        it('swallows non-ok responses without rejecting the caller', async () => {
            window.api.getCsrfToken = () => 'csrf-test-123';
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) })
            );

            // Should not throw — the .catch in saveThemeToServer turns the
            // HTTP 404 into a warning log.
            await expect(theme.saveThemeToServer('nord')).resolves.toBeUndefined();
        });

        it('swallows network errors without rejecting the caller', async () => {
            window.api.getCsrfToken = () => 'csrf-test-123';
            globalThis.fetch = vi.fn(() => Promise.reject(new Error('offline')));

            await expect(theme.saveThemeToServer('nord')).resolves.toBeUndefined();
        });
    });
});
