import {
    describe,
    it,
    expect,
    vi,
    beforeEach,
    afterEach,
    type MockInstance
} from 'vitest';
import {
    addPageToCollection,
    removePageFromCollection,
    listCollectionPages,
    getMyCollectionsContainingPage
} from './page_collections';

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 pageItemFixture(extra: Record<string, unknown> = {}) {
    return {
        page_id: 'p1',
        chapter_id: 'ch1',
        manga_id: 'm1',
        page_number: 1,
        chapter_number: 1,
        chapter_title: null,
        manga_title: 'Berserk',
        storage_key: 'mangas/m1/chapters/ch1/pages/0001.png',
        added_at: '2026-01-01T00:00:00Z',
        ...extra
    };
}

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

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

    it('listCollectionPages hits the breadcrumb endpoint', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({
                items: [pageItemFixture()],
                page: { limit: 50, offset: 0, total: 1 }
            })
        );
        const r = await listCollectionPages('c1');
        expect(r.items[0].manga_title).toBe('Berserk');
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/collections\/c1\/pages$/);
    });

    it('listCollectionPages forwards pagination params', async () => {
        fetchSpy.mockResolvedValueOnce(
            ok({ items: [], page: { limit: 10, offset: 20, total: 0 } })
        );
        await listCollectionPages('c1', { limit: 10, offset: 20 });
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toContain('limit=10');
        expect(url).toContain('offset=20');
    });

    it('addPageToCollection POSTs { page_id }', async () => {
        fetchSpy.mockResolvedValueOnce(ok({}, 201));
        await addPageToCollection('c1', 'p1');
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(init.method).toBe('POST');
        expect(JSON.parse(init.body as string)).toEqual({ page_id: 'p1' });
    });

    it('removePageFromCollection DELETEs with both ids encoded', async () => {
        fetchSpy.mockResolvedValueOnce(noContent());
        await removePageFromCollection('c with space', 'p1');
        const url = fetchSpy.mock.calls[0][0] as string;
        // Path includes encoded "c with space" + page id.
        expect(url).toContain('/v1/collections/c%20with%20space/pages/p1');
        const init = fetchSpy.mock.calls[0][1] as RequestInit;
        expect(init.method).toBe('DELETE');
    });

    it('getMyCollectionsContainingPage unwraps `collection_ids`', async () => {
        fetchSpy.mockResolvedValueOnce(ok({ collection_ids: ['c1', 'c2'] }));
        const ids = await getMyCollectionsContainingPage('p1');
        expect(ids).toEqual(['c1', 'c2']);
        const url = fetchSpy.mock.calls[0][0] as string;
        expect(url).toMatch(/\/v1\/pages\/p1\/my-collections$/);
    });
});
