import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
import {
    updateReadProgress,
    listMyReadProgress,
    listMyReadProgressOrEmpty,
    getMyReadProgressForManga,
    clearReadProgress
} from './read_progress';

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

function noContent(): Response {
    return new Response(null, { status: 204 });
}

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

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

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

    it('updateReadProgress PUTs to /v1/me/read-progress', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({
                user_id: 'u1',
                manga_id: 'm1',
                chapter_id: 'c1',
                page: 5,
                updated_at: '2026-05-17T12:00:00Z'
            })
        );
        const r = await updateReadProgress({ manga_id: 'm1', chapter_id: 'c1', page: 5 });
        expect(r.page).toBe(5);
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(init.method).toBe('PUT');
        expect(JSON.parse(init.body as string)).toEqual({
            manga_id: 'm1',
            chapter_id: 'c1',
            page: 5
        });
    });

    it('listMyReadProgress returns the paged envelope', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({
                items: [],
                page: { limit: 50, offset: 0, total: 0 }
            })
        );
        const r = await listMyReadProgress();
        expect(r.items).toEqual([]);
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/me\/read-progress$/);
    });

    it('listMyReadProgressOrEmpty returns empty page on 401', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'login required'));
        const r = await listMyReadProgressOrEmpty();
        expect(r.items).toEqual([]);
    });

    it('getMyReadProgressForManga returns null on 404 (not yet read)', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(404, 'not_found', 'no progress'));
        const r = await getMyReadProgressForManga('m1');
        expect(r).toBeNull();
    });

    it('getMyReadProgressForManga returns null on 401 (guest)', async () => {
        fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'login'));
        const r = await getMyReadProgressForManga('m1');
        expect(r).toBeNull();
    });

    it('getMyReadProgressForManga returns the row with chapter_number when present', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({
                manga_id: 'm1',
                chapter_id: 'c1',
                chapter_number: 7,
                page: 3,
                updated_at: '2026-05-17T12:00:00Z'
            })
        );
        const r = await getMyReadProgressForManga('m1');
        expect(r?.chapter_id).toBe('c1');
        expect(r?.chapter_number).toBe(7);
        expect(r?.page).toBe(3);
    });

    it('clearReadProgress DELETEs the resource', async () => {
        fetchSpy.mockResolvedValueOnce(noContent());
        await clearReadProgress('m1');
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(init.method).toBe('DELETE');
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/me\/read-progress\/m1$/);
    });
});
