import { test, expect, type Page } from './fixtures';

// Continuous mode eager-loads a window of pages ahead of the reading position
// (the deep-linked page + a lead, min 6 pages), so those are fetched and sized
// up front. Pages beyond the window are `loading="lazy"` — the browser fetches
// them natively as the reader scrolls near, so opening a long chapter (up to
// 2000 pages) doesn't fire every request or decode every image at once. Unloaded
// lazy pages reserve height so they stay distinct scroll targets for the
// progress IntersectionObserver.

const MANGA_ID = 'm1';
const CH1 = 'c1';
// Enough pages that the last one sits tens of thousands of px below the fold —
// well beyond any browser's lazy-load distance threshold — so "far pages don't
// prefetch at the top" is unambiguous rather than threshold-brittle.
const PAGE_COUNT = 40;

function pages(prefix: string, n: number) {
    return Array.from({ length: n }, (_, i) => ({
        id: `${prefix}p${i + 1}`,
        chapter_id: prefix,
        page_number: i + 1,
        storage_key: `${prefix}/${i + 1}`,
        content_type: 'image/svg+xml'
    }));
}

function chapter(id: string, number: number, count: number) {
    return { id, manga_id: MANGA_ID, number, title: null, page_count: count, created_at: '2026-01-01T00:00:00Z', size_bytes: 0 };
}

async function mockReader(page: Page) {
    await page.route('**/api/v1/**', async (route) => {
        const { pathname } = new URL(route.request().url());
        const json = (status: number, body: unknown) =>
            route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) });

        if (pathname.includes('/files/')) {
            // A viewBox + rect gives the <img> a real intrinsic size so pages
            // get real (tall) heights — offscreen pages then stay below the
            // fold instead of collapsing to 0 and piling into the viewport.
            return route.fulfill({ status: 200, contentType: 'image/svg+xml', body: '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1200" viewBox="0 0 800 1200"><rect width="800" height="1200" fill="#888"/></svg>' });
        }
        if (pathname.endsWith('/auth/config')) return json(200, { self_register_enabled: true, private_mode: false });
        if (pathname.endsWith('/auth/me')) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
        if (pathname.endsWith('/auth/me/preferences')) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
        if (pathname.endsWith(`/chapters/${CH1}/pages`)) return json(200, { pages: pages(CH1, PAGE_COUNT) });
        if (pathname.endsWith(`/chapters/${CH1}`)) return json(200, chapter(CH1, 1, PAGE_COUNT));
        if (pathname.includes(`/mangas/${MANGA_ID}/chapters`)) {
            return json(200, { items: [chapter(CH1, 1, PAGE_COUNT)], page: { limit: 200, offset: 0, total: 1 } });
        }
        if (pathname.endsWith(`/mangas/${MANGA_ID}`)) return json(200, { id: MANGA_ID, title: 'Berserk', status: 'ongoing', alt_titles: [], description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', authors: [], genres: [], tags: [], content_warnings: [], chapter_storage_bytes: 0 });
        if (pathname.includes('/me/read-progress')) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
        return json(503, { error: { code: 'e2e_unmocked', message: pathname } });
    });
}

async function goContinuous(page: Page, path: string) {
    await page.addInitScript(() => localStorage.setItem('mangalord-reader-mode', 'continuous'));
    await page.goto(path);
    await expect(page.getByTestId('reader-continuous')).toBeVisible();
}

// EAGER_MIN in the reader: opening at page 1 (index 0) eager-loads pages 1..6.
const EAGER_THROUGH = 6;

test('eager-loads a leading window and leaves far pages lazy', async ({ page }) => {
    await mockReader(page);
    await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}`);

    // The leading window is eager — warmed up front so the reader doesn't
    // pop-in / reflow as it scrolls onto the next few pages.
    for (let n = 1; n <= EAGER_THROUGH; n++) {
        await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute('loading', 'eager');
    }
    // Pages beyond the window stay lazy — not fetched until scrolled near, so
    // a 2000-page chapter doesn't fire 2000 requests / decodes at once.
    for (let n = EAGER_THROUGH + 1; n <= PAGE_COUNT; n++) {
        await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute('loading', 'lazy');
    }
});

test('a windowed page is warmed up front without scrolling', async ({ page }) => {
    await mockReader(page);
    await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}`);

    // A page inside the eager window finishes loading without any scroll — the
    // preload payoff (no pop-in as the reader advances a few pages) is kept.
    // (We can't assert the far lazy pages stay *unfetched* here: headless
    // Chromium prefetches `loading="lazy"` images regardless of distance. The
    // attribute partition above is the mechanism that defers them in the real
    // OOM target — mobile Safari. Playwright can only pin the attributes.)
    await expect(async () => {
        const loaded = await page
            .getByTestId(`reader-page-${EAGER_THROUGH}`)
            .evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0);
        expect(loaded).toBe(true);
    }).toPass();
    expect(await page.evaluate(() => window.scrollY)).toBe(0);
});

test('deep-link ?page=N keeps the target inside the eager window and lands the scroll', async ({
    page
}) => {
    await mockReader(page);
    // initialIndex = 19 → eagerThrough = 22, so pages 1..23 are eager (the
    // target and everything above it settle height before the scroll fires).
    await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}?page=20`);

    await expect(page.getByTestId('reader-page-20')).toHaveAttribute('loading', 'eager');
    await expect(page.getByTestId('reader-page-22')).toHaveAttribute('loading', 'eager');
    // A page far past the widened window is still lazy.
    await expect(page.getByTestId(`reader-page-${PAGE_COUNT}`)).toHaveAttribute('loading', 'lazy');

    // The scroll-to-`?page=N` effect lands the target in view (proves the
    // eager window settled the heights above it — a short-landing scroll would
    // leave page 20 out of the viewport).
    await expect(page.getByTestId('reader-page-20')).toBeInViewport();
});
