import {
    describe,
    it,
    expect,
    vi,
    beforeEach,
    afterEach,
    type MockInstance
} from 'vitest';
import { preferences } from './preferences.svelte';

function ok(body: unknown, status = 200): Response {
    return new Response(JSON.stringify(body), {
        status,
        headers: { 'content-type': 'application/json' }
    });
}

function envelope(status: number, code: string, message: string): Response {
    return new Response(JSON.stringify({ error: { code, message } }), {
        status,
        headers: { 'content-type': 'application/json' }
    });
}

const flush = () => new Promise((r) => setTimeout(r, 0));

describe('preferences store', () => {
    let fetchSpy: MockInstance<typeof globalThis.fetch>;

    beforeEach(async () => {
        localStorage.clear();
        // Default fetch implementation returns 401 so the reset calls below
        // don't escape as real network requests (jsdom can't resolve
        // /api/...). Each test overrides with mockResolvedValueOnce.
        fetchSpy = vi
            .spyOn(globalThis, 'fetch')
            .mockResolvedValue(envelope(401, 'unauthenticated', 'reset'));
        // Reset the singleton's state for the next test.
        preferences.setMode('single');
        preferences.setGap('none');
        await flush();
        fetchSpy.mockReset();
    });
    afterEach(() => {
        vi.restoreAllMocks();
        localStorage.clear();
    });

    it('init hydrates from localStorage immediately and pulls server values when authenticated', async () => {
        localStorage.setItem('mangalord-reader-mode', 'continuous');
        localStorage.setItem('mangalord-reader-gap', 'small');

        fetchSpy.mockResolvedValueOnce(
            ok({
                reader_mode: 'single',
                reader_page_gap: 'large',
                updated_at: '2026-05-17T12:00:00Z'
            })
        );

        const p = preferences.init();
        // Synchronous hydration from localStorage happens before the await.
        expect(preferences.readerMode).toBe('continuous');
        expect(preferences.readerPageGap).toBe('small');

        await p;
        // Server response overrides the localStorage values.
        expect(preferences.readerMode).toBe('single');
        expect(preferences.readerPageGap).toBe('large');
        expect(localStorage.getItem('mangalord-reader-mode')).toBe('single');
        expect(localStorage.getItem('mangalord-reader-gap')).toBe('large');
    });

    it('init keeps localStorage values when the user is anonymous (401)', async () => {
        localStorage.setItem('mangalord-reader-mode', 'continuous');
        localStorage.setItem('mangalord-reader-gap', 'medium');
        fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'no session'));

        await preferences.init();
        expect(preferences.readerMode).toBe('continuous');
        expect(preferences.readerPageGap).toBe('medium');
    });

    it('setMode updates state, writes localStorage, and PATCHes the server', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({
                reader_mode: 'continuous',
                reader_page_gap: 'none',
                updated_at: '2026-05-17T12:00:00Z'
            })
        );
        preferences.setMode('continuous');
        expect(preferences.readerMode).toBe('continuous');
        expect(localStorage.getItem('mangalord-reader-mode')).toBe('continuous');

        await flush();
        expect(fetchSpy).toHaveBeenCalledOnce();
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(init.method).toBe('PATCH');
        expect(JSON.parse(init.body as string)).toEqual({ reader_mode: 'continuous' });
    });

    it('setGap survives a 401 (guest) without throwing', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'no session'));
        preferences.setGap('large');
        expect(preferences.readerPageGap).toBe('large');
        await flush();
        // localStorage still holds the choice — the guest preference persists.
        expect(localStorage.getItem('mangalord-reader-gap')).toBe('large');
    });

    it('setMode reverts the optimistic update when the server returns 5xx', async () => {
        // Suppress the expected console.error so the test output stays clean.
        const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
        fetchSpy.mockResolvedValueOnce(envelope(500, 'internal_error', 'boom'));
        preferences.setMode('continuous');
        expect(preferences.readerMode).toBe('continuous');
        await flush();
        expect(preferences.readerMode).toBe('single');
        expect(localStorage.getItem('mangalord-reader-mode')).toBe('single');
        errSpy.mockRestore();
    });

    it('clearForLogout resets state and removes localStorage entries', () => {
        localStorage.setItem('mangalord-reader-mode', 'continuous');
        localStorage.setItem('mangalord-reader-gap', 'large');
        preferences.clearForLogout();
        expect(preferences.readerMode).toBe('single');
        expect(preferences.readerPageGap).toBe('none');
        expect(localStorage.getItem('mangalord-reader-mode')).toBeNull();
        expect(localStorage.getItem('mangalord-reader-gap')).toBeNull();
    });

    it('init ignores an unknown reader_page_gap from the server (forward compat)', async () => {
        localStorage.setItem('mangalord-reader-mode', 'continuous');
        localStorage.setItem('mangalord-reader-gap', 'medium');
        fetchSpy.mockResolvedValueOnce(
            ok({
                reader_mode: 'continuous',
                reader_page_gap: 'huge',
                updated_at: '2026-05-17T12:00:00Z'
            })
        );
        await preferences.init();
        // Mode comes through (known value); unknown gap is rejected, the
        // pre-existing 'medium' value is retained.
        expect(preferences.readerMode).toBe('continuous');
        expect(preferences.readerPageGap).toBe('medium');
    });
});
