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

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 prefsFixture = {
    reader_mode: 'continuous' as const,
    reader_page_gap: 'medium' as const,
    updated_at: '2026-05-17T12:00:00Z'
};

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

    beforeEach(() => {
        fetchSpy = vi.spyOn(globalThis, 'fetch');
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });

    it('GAP_PX exposes the four named options with increasing values', () => {
        expect(GAP_PX.none).toBe(0);
        expect(GAP_PX.small).toBeGreaterThan(GAP_PX.none);
        expect(GAP_PX.medium).toBeGreaterThan(GAP_PX.small);
        expect(GAP_PX.large).toBeGreaterThan(GAP_PX.medium);
    });

    it('getPreferences GETs /v1/auth/me/preferences and returns the row', async () => {
        fetchSpy.mockResolvedValueOnce(ok(prefsFixture));
        await expect(getPreferences()).resolves.toEqual(prefsFixture);
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/auth\/me\/preferences$/);
    });

    it('getPreferences returns null on 401 (anonymous browsing)', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'unauthenticated'));
        await expect(getPreferences()).resolves.toBeNull();
    });

    it('getPreferences re-throws non-401 errors', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(500, 'internal_error', 'boom'));
        await expect(getPreferences()).rejects.toMatchObject({ status: 500 });
    });

    it('updatePreferences PATCHes JSON and returns the new row', async () => {
        fetchSpy.mockResolvedValueOnce(ok(prefsFixture));
        const r = await updatePreferences({
            reader_mode: 'continuous',
            reader_page_gap: 'medium'
        });
        expect(r).toEqual(prefsFixture);
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/auth\/me\/preferences$/);
        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',
            reader_page_gap: 'medium'
        });
    });

    it('updatePreferences sends only the keys it was given (partial)', async () => {
        fetchSpy.mockResolvedValueOnce(ok(prefsFixture));
        await updatePreferences({ reader_mode: 'single' });
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(JSON.parse(init.body as string)).toEqual({ reader_mode: 'single' });
    });

    it('updatePreferences surfaces 400 invalid_input via ApiError', async () => {
        fetchSpy.mockResolvedValueOnce(
            envelope(400, 'invalid_input', 'reader_mode must be one of ...')
        );
        await expect(
            updatePreferences({ reader_mode: 'continuous' })
        ).rejects.toMatchObject({ status: 400, code: 'invalid_input' });
    });
});
