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

// E2E for the admin Analysis section: coverage overview + badges, drill
// manga → chapter → page, the page-detail modal, and the enqueue actions.
// Fully mocked.

const DESKTOP = { width: 1280, height: 720 } as const;

const mangaId = 'a9999999-9999-9999-9999-999999999999';
const chapterId = 'c9999999-9999-9999-9999-999999999999';
const pageDone = 'p1111111-1111-1111-1111-111111111111';
const pageNone = 'p2222222-2222-2222-2222-222222222222';

const adminUser = {
    id: 'u11111111-1111-1111-1111-111111111111',
    username: 'admin',
    created_at: '2026-01-01T00:00:00Z',
    is_admin: true
};

const systemStats = {
    disk: null,
    memory: { total_bytes: 1, used_bytes: 0, percent_used: 0 },
    cpu: { percent_used: 0 },
    alerts: []
};

type Captured = { reenqueue: Record<string, unknown> | null; analyzeCalls: number };

async function mockAdmin(page: Page, cap: Captured) {
    await page.route('**/api/v1/auth/config', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ self_register_enabled: true, private_mode: false })
        })
    );
    await page.route('**/api/v1/auth/me', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ user: adminUser })
        })
    );
    await page.route('**/api/v1/auth/me/preferences', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ reader_mode: 'single', reader_page_gap: 'small' })
        })
    );
    await page.route('**/api/v1/me/bookmarks*', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } })
        })
    );
    await page.route('**/api/v1/admin/system', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(systemStats)
        })
    );

    // Coverage overview. Registered before the more specific routes below
    // so the later-registered (more specific) globs win for their URLs.
    await page.route('**/api/v1/admin/analysis/mangas**', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [
                    {
                        manga_id: mangaId,
                        title: 'Berserk',
                        total_pages: 2,
                        analyzed_pages: 1
                    }
                ],
                page: { limit: 25, offset: 0, total: 1 }
            })
        })
    );
    await page.route('**/api/v1/admin/analysis/mangas/*/chapters', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [
                    {
                        chapter_id: chapterId,
                        number: 1,
                        title: 'The Brand',
                        total_pages: 2,
                        analyzed_pages: 1
                    }
                ]
            })
        })
    );
    await page.route('**/api/v1/admin/analysis/chapters/*/pages', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [
                    { page_id: pageDone, page_number: 1, status: 'done' },
                    { page_id: pageNone, page_number: 2, status: 'none' }
                ]
            })
        })
    );
    await page.route(`**/api/v1/admin/analysis/pages/${pageDone}`, (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                page_id: pageDone,
                page_number: 1,
                chapter_id: chapterId,
                manga_id: mangaId,
                status: 'done',
                is_nsfw: true,
                scene_description: 'A rainy street at night.',
                model: 'test-model',
                error: null,
                analyzed_at: '2026-06-13T12:00:00Z',
                ocr: [{ kind: 'speech', text: 'Hello there' }],
                tags: ['action', 'city'],
                content_warnings: ['gore']
            })
        })
    );
    await page.route(`**/api/v1/admin/analysis/pages/${pageNone}`, (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                page_id: pageNone,
                page_number: 2,
                chapter_id: chapterId,
                manga_id: mangaId,
                status: 'none',
                is_nsfw: false,
                scene_description: null,
                model: null,
                error: null,
                analyzed_at: null,
                ocr: [],
                tags: [],
                content_warnings: []
            })
        })
    );

    // Analysis history (terminal-outcome log). Registered before the
    // page-detail routes are matched by their more-specific globs.
    await page.route('**/api/v1/admin/analysis/history**', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [
                    {
                        page_id: pageDone,
                        page_number: 1,
                        chapter_id: chapterId,
                        chapter_number: 1,
                        manga_id: mangaId,
                        manga_title: 'Berserk',
                        status: 'done',
                        is_nsfw: true,
                        model: 'test-model',
                        error: null,
                        analyzed_at: '2026-06-13T12:00:00Z',
                        duration_ms: 2400
                    }
                ],
                page: { limit: 25, offset: 0, total: 1 }
            })
        })
    );

    await page.route('**/api/v1/admin/analysis/metrics**', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                n: 3204,
                ok: 3159,
                failed: 45,
                avg_ms: 2400,
                by_model: [{ model: 'qwen2-vl-7b', avg_ms: 2400, n: 3100 }]
            })
        })
    );
    // The metrics tab also pulls a per-bucket time series for the trend
    // charts. Registered after the aggregate `metrics**` route so it wins
    // for the more specific `/metrics/series` path.
    await page.route('**/api/v1/admin/analysis/metrics/series**', (r) =>
        r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                buckets: [
                    { t: '2026-06-12T00:00:00Z', n: 1600, ok: 1580, failed: 20, avg_ms: 2300 },
                    { t: '2026-06-13T00:00:00Z', n: 1604, ok: 1579, failed: 25, avg_ms: 2500 }
                ]
            })
        })
    );

    // Default: keep the SSE connection pending (no events) so tests that
    // don't care about live updates don't trigger reconnect churn. The
    // live-updates test overrides this with a fulfilling stream.
    await page.route(
        '**/api/v1/admin/analysis/status/stream',
        () => new Promise(() => {})
    );

    await page.route('**/api/v1/admin/analysis/reenqueue', (r) => {
        cap.reenqueue = JSON.parse(r.request().postData() ?? '{}');
        return r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ enqueued: 12 })
        });
    });
    await page.route(`**/api/v1/admin/pages/${pageNone}/analyze`, (r) => {
        cap.analyzeCalls += 1;
        return r.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ enqueued: true })
        });
    });
}

test.describe('/admin/analysis', () => {
    test('overview shows coverage badges and queues the whole library', async ({
        page
    }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        const badge = page.getByTestId(`admin-analysis-coverage-manga-${mangaId}`);
        await expect(badge).toBeVisible();
        await expect(badge).toContainText('Partial 1/2');

        await page.getByTestId('admin-analysis-enqueue-library').click();
        await expect(page.getByTestId('admin-analysis-notice')).toContainText(
            'Enqueued 12 pages'
        );
        expect(cap.reenqueue).toEqual({ only_unanalyzed: true });
    });

    test('drill manga → chapter → page and inspect the result', async ({ page }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        await page.getByTestId(`admin-analysis-expand-${mangaId}`).click();
        await expect(
            page.getByTestId(`admin-analysis-coverage-chapter-${chapterId}`)
        ).toContainText('Partial 1/2');

        await page.getByTestId(`admin-analysis-expand-chapter-${chapterId}`).click();
        const doneChip = page.getByTestId(`admin-analysis-page-${pageDone}`);
        await expect(doneChip).toHaveAttribute('data-status', 'done');

        await doneChip.click();
        const modal = page.getByTestId('admin-analysis-detail');
        await expect(modal).toBeVisible();
        await expect(
            page.getByTestId('admin-analysis-detail-status')
        ).toContainText('Analyzed');
        await expect(modal).toContainText('model test-model');
        // OCR-only backend: the detail surfaces the extracted OCR lines (with
        // their kind) — tags / scene / NSFW belong to the dormant vision
        // backend and are intentionally not shown here.
        await expect(modal).toContainText('OCR text');
        await expect(modal).toContainText('Hello there');
    });

    test('live SSE events drive the indicator and activity ticker', async ({
        page
    }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        // Override the default hanging stream with one that emits two frames.
        const frames =
            `event: analysis\ndata: ${JSON.stringify({
                kind: 'started',
                page_id: pageNone,
                manga_id: mangaId,
                manga_title: 'Berserk',
                chapter_id: chapterId,
                chapter_number: 1,
                page_number: 2
            })}\n\n` +
            `event: analysis\ndata: ${JSON.stringify({
                kind: 'completed',
                page_id: pageNone,
                manga_id: mangaId,
                manga_title: 'Berserk',
                chapter_id: chapterId,
                chapter_number: 1,
                page_number: 2
            })}\n\n`;
        let served = false;
        await page.route('**/api/v1/admin/analysis/status/stream', (r) => {
            if (served) return new Promise(() => {}); // hang on reconnect
            served = true;
            return r.fulfill({
                status: 200,
                headers: { 'content-type': 'text/event-stream' },
                body: frames
            });
        });

        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        // The two SSE frames are parsed and applied to the activity ticker.
        // (The mocked stream closes after its body, so the live pill flips
        // back to "Reconnecting…" — the ticker is the durable signal.)
        const tick = page.getByTestId('admin-analysis-ticker');
        await expect(tick).toContainText('Analyzing Berserk');
        await expect(tick).toContainText('Analyzed page 2');
    });

    test('now-analyzing banner appears from SSE and Jump expands the chapter', async ({
        page
    }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        // A single `started` frame, then hang so the banner stays put.
        const frame = `event: analysis\ndata: ${JSON.stringify({
            kind: 'started',
            page_id: pageDone,
            manga_id: mangaId,
            manga_title: 'Berserk',
            chapter_id: chapterId,
            chapter_number: 1,
            page_number: 1
        })}\n\n`;
        let served = false;
        await page.route('**/api/v1/admin/analysis/status/stream', (r) => {
            if (served) return new Promise(() => {});
            served = true;
            return r.fulfill({
                status: 200,
                headers: { 'content-type': 'text/event-stream' },
                body: frame
            });
        });

        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        const banner = page.getByTestId('admin-analysis-now');
        await expect(banner).toContainText('Now analyzing');
        await expect(banner).toContainText('Berserk · Ch 1 · Page 1');

        // Jump expands the manga + chapter and the analyzing chip appears.
        await page.getByTestId('admin-analysis-now-jump').click();
        await expect(page.getByTestId(`admin-analysis-page-${pageDone}`)).toBeVisible();
    });

    test('history tab lists terminal analyses and opens the detail modal', async ({
        page
    }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        await page.getByTestId('admin-analysis-tab-history').click();
        const row = page.getByTestId(`analysis-history-row-${pageDone}`);
        await expect(row).toContainText('Berserk · Ch 1 · p1');
        await expect(row).toContainText('test-model'); // model column
        await expect(row).toContainText('2.4s'); // duration column
        // (No NSFW flag: the active OCR backend extracts text only.)

        await row.click();
        await expect(page.getByTestId('admin-analysis-detail')).toBeVisible();
        await expect(page.getByTestId('admin-analysis-detail-status')).toContainText(
            'Analyzed'
        );
    });

    test('metrics tab shows aggregate timing + trend charts', async ({ page }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        await page.getByTestId('admin-analysis-tab-metrics').click();
        // Aggregate tiles for the selected window.
        await expect(page.getByTestId('analysis-metrics-n')).toContainText('3204');
        await expect(page.getByTestId('analysis-metrics-avg')).toContainText('2.4s');
        const metrics = page.getByTestId('analysis-metrics');
        await expect(metrics).toContainText('Success');
        await expect(metrics).toContainText('99%'); // 3159/3204
        await expect(metrics).toContainText('45'); // failed
        // The per-bucket trend charts render from the series endpoint.
        await expect(page.getByTestId('analysis-metrics-charts')).toBeVisible();
    });

    test('queue an unanalyzed page from its detail modal', async ({ page }) => {
        const cap: Captured = { reenqueue: null, analyzeCalls: 0 };
        await mockAdmin(page, cap);
        await page.setViewportSize(DESKTOP);
        await page.goto('/admin/analysis');

        await page.getByTestId(`admin-analysis-expand-${mangaId}`).click();
        await page.getByTestId(`admin-analysis-expand-chapter-${chapterId}`).click();
        await page.getByTestId(`admin-analysis-page-${pageNone}`).click();

        await expect(page.getByTestId('admin-analysis-detail-status')).toContainText(
            'Not analyzed'
        );
        await page.getByTestId('admin-analysis-detail-queue').click();

        await expect(page.getByTestId('admin-analysis-notice')).toContainText(
            'Queued page 2'
        );
        expect(cap.analyzeCalls).toBe(1);
    });
});
