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

// E2E for the per-page collection + tag flow added in v0.61.0. Mocks
// the entire API so the spec runs without a backend. The same
// fixtures + mockReader scaffolding as `mobile-reader.spec.ts`;
// kept inline so the file stays self-contained.

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

const mangaId = 'a9999999-9999-9999-9999-999999999999';
const chapterId = 'c9999999-9999-9999-9999-999999999999';
const pageId = 'p11111111-1111-1111-1111-111111111111';
const collectionId = 'cc111111-1111-1111-1111-111111111111';

const mangaFixture = {
    id: mangaId,
    title: 'Berserk',
    status: 'ongoing',
    alt_titles: [],
    description: null,
    cover_image_path: `mangas/${mangaId}/cover.png`,
    created_at: '2026-01-01T00:00:00Z',
    updated_at: '2026-01-01T00:00:00Z',
    authors: [{ id: 'au1', name: 'Kentaro Miura' }],
    genres: [],
    tags: []
};

const chapterFixture = {
    id: chapterId,
    manga_id: mangaId,
    number: 1,
    title: 'The Brand',
    page_count: 1,
    created_at: '2026-01-01T00:00:00Z'
};

const pagesFixture = [
    {
        id: pageId,
        chapter_id: chapterId,
        page_number: 1,
        storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0001.png`,
        content_type: 'image/png'
    }
];

const userFixture = {
    id: 'u11111111-1111-1111-1111-111111111111',
    username: 'tester',
    created_at: '2026-01-01T00:00:00Z',
    is_admin: false
};

const collectionFixture = {
    id: collectionId,
    user_id: userFixture.id,
    name: 'Favorite panels',
    description: null,
    created_at: '2026-01-01T00:00:00Z',
    updated_at: '2026-01-01T00:00:00Z',
    manga_count: 0,
    sample_covers: []
};

/**
 * Wire the full mock surface for the reader plus the new
 * `pages/:id/my-collections`, `pages/:id/my-tags`, `me/collections`,
 * and `collections/:id/pages` endpoints. `myCollectionsState` lets a
 * test mutate what /my-collections returns mid-flow so the "re-open
 * menu after add → In 1 collection" assertion is exercised.
 */
async function mockReader(
    page: Page,
    state: {
        collectionsContainingPage: string[];
        tagsOnPage: string[];
        admin?: boolean;
        analyzeCalls?: number;
    }
) {
    await page.route('**/api/v1/auth/config', (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ self_register_enabled: true, private_mode: false })
        })
    );
    await page.route('**/api/v1/auth/me', (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                user: { ...userFixture, is_admin: state.admin ?? false }
            })
        })
    );
    // Admin force-analyze endpoint — counts invocations for assertions.
    await page.route(`**/api/v1/admin/pages/${pageId}/analyze`, (route) => {
        state.analyzeCalls = (state.analyzeCalls ?? 0) + 1;
        return route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ enqueued: true })
        });
    });
    await page.route('**/api/v1/auth/me/preferences', (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                reader_mode: 'single',
                reader_page_gap: 'small'
            })
        })
    );
    await page.route('**/api/v1/me/bookmarks*', (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } })
        })
    );
    await page.route(`**/api/v1/mangas/${mangaId}`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(mangaFixture)
        })
    );
    await page.route(`**/api/v1/mangas/${mangaId}/chapters`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [chapterFixture],
                page: { limit: 50, offset: 0, total: 1 }
            })
        })
    );
    await page.route(`**/api/v1/mangas/${mangaId}/chapters\\?*`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [chapterFixture],
                page: { limit: 50, offset: 0, total: 1 }
            })
        })
    );
    await page.route(`**/api/v1/mangas/${mangaId}/chapters/${chapterId}`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(chapterFixture)
        })
    );
    await page.route(
        `**/api/v1/mangas/${mangaId}/chapters/${chapterId}/pages`,
        (route) =>
            route.fulfill({
                status: 200,
                contentType: 'application/json',
                body: JSON.stringify({ pages: pagesFixture })
            })
    );
    await page.route(`**/api/v1/me/read-progress/${mangaId}`, (route) =>
        route.fulfill({
            status: 404,
            contentType: 'application/json',
            body: JSON.stringify({ error: { code: 'not_found', message: 'not found' } })
        })
    );

    // The new endpoints — these are what the context menu hits.
    await page.route(`**/api/v1/pages/${pageId}/my-collections`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ collection_ids: state.collectionsContainingPage })
        })
    );
    await page.route(`**/api/v1/pages/${pageId}/my-tags`, (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ tags: state.tagsOnPage })
        })
    );
    await page.route('**/api/v1/me/collections*', (route) =>
        route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [collectionFixture],
                page: { limit: 200, offset: 0, total: 1 }
            })
        })
    );
    await page.route(
        `**/api/v1/collections/${collectionId}/pages`,
        async (route) => {
            if (route.request().method() === 'POST') {
                state.collectionsContainingPage = [collectionId];
                return route.fulfill({ status: 201, body: '' });
            }
            return route.continue();
        }
    );

    // A landscape page image (wide aspect) so that, under the width-driven
    // reader sizing, the rendered page is SHORTER than the viewport. A tall
    // page would push its centre off-screen, and Playwright's `.click()`
    // would scroll it into view first — that synthetic scroll trips the
    // context menu's (by-design) close-on-scroll and flakes these tests.
    // Real users right-click at the visible cursor with no such scroll.
    const image = `<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="400"><rect width="1400" height="400" fill="#888"/></svg>`;
    await page.route('**/api/v1/files/**', (route) =>
        route.fulfill({ status: 200, contentType: 'image/svg+xml', body: image })
    );
}

test.describe('page context menu (desktop right-click)', () => {
    test('right-click opens the menu with empty-state hints', async ({ page }) => {
        const state = { collectionsContainingPage: [], tagsOnPage: [] };
        await mockReader(page, state);
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        await expect(page.getByTestId('reader-page')).toBeVisible();
        await expect(page.getByTestId('page-context-menu')).toBeHidden();

        await page.getByTestId('reader-page').click({ button: 'right' });

        const menu = page.getByTestId('page-context-menu');
        await expect(menu).toBeVisible();
        await expect(
            page.getByTestId('page-context-collections-line')
        ).toHaveText('Not in any collection');
        await expect(page.getByTestId('page-context-tags-line')).toHaveText(
            'No tags yet'
        );
    });

    test('Escape closes the menu', async ({ page }) => {
        await mockReader(page, { collectionsContainingPage: [], tagsOnPage: [] });
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        await page.getByTestId('reader-page').click({ button: 'right' });
        await expect(page.getByTestId('page-context-menu')).toBeVisible();

        await page.keyboard.press('Escape');
        await expect(page.getByTestId('page-context-menu')).toBeHidden();
    });

    test('add-to-collection → modal → toggle → re-open menu shows "In 1 collection"', async ({
        page
    }) => {
        const state = { collectionsContainingPage: [], tagsOnPage: [] };
        await mockReader(page, state);
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        await page.getByTestId('reader-page').click({ button: 'right' });
        await page.getByTestId('page-context-add-to-collection').click();

        // Modal opens with the user's one collection unchecked.
        const modal = page.getByTestId('add-to-collection-modal');
        await expect(modal).toBeVisible();
        const checkbox = modal.getByTestId(`collection-toggle-${collectionId}`);
        await expect(checkbox).not.toBeChecked();

        await checkbox.check();
        // The mock flips containing-page state on POST. Close + re-open
        // the menu to verify the contextual line reflects the new
        // server state.
        await page.keyboard.press('Escape');
        await expect(modal).toBeHidden();

        await page.getByTestId('reader-page').click({ button: 'right' });
        await expect(page.getByTestId('page-context-collections-line')).toHaveText(
            'In 1 collection'
        );
    });

    test('non-admin does not see the Queue for analysis action', async ({ page }) => {
        await mockReader(page, { collectionsContainingPage: [], tagsOnPage: [] });
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        await page.getByTestId('reader-page').click({ button: 'right' });
        await expect(page.getByTestId('page-context-menu')).toBeVisible();
        await expect(page.getByTestId('page-context-analyze')).toBeHidden();
    });

    test('admin can queue the page for analysis from the menu', async ({ page }) => {
        const state = {
            collectionsContainingPage: [],
            tagsOnPage: [],
            admin: true,
            analyzeCalls: 0
        };
        await mockReader(page, state);
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        await page.getByTestId('reader-page').click({ button: 'right' });
        const item = page.getByTestId('page-context-analyze');
        await expect(item).toBeVisible();
        await expect(item).toContainText('Queue for analysis');

        await item.click();
        await expect(item).toContainText('Queued for analysis');
        expect(state.analyzeCalls).toBe(1);
    });

    test('Shift+right-click falls through to the browser native menu', async ({
        page
    }) => {
        await mockReader(page, { collectionsContainingPage: [], tagsOnPage: [] });
        await page.setViewportSize(DESKTOP);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        // Hold Shift while right-clicking. The reader's
        // `oncontextmenu` returns early on shiftKey, so the in-app
        // menu must NOT open. (Playwright doesn't surface the
        // native browser menu, but we can assert ours stays hidden.)
        await page.keyboard.down('Shift');
        await page.getByTestId('reader-page').click({ button: 'right' });
        await page.keyboard.up('Shift');

        await expect(page.getByTestId('page-context-menu')).toBeHidden();
    });
});

test.describe('page action sheet (mobile long-press)', () => {
    test('long-press on a page image opens the action sheet', async ({ page }) => {
        await mockReader(page, { collectionsContainingPage: [], tagsOnPage: [] });
        await page.setViewportSize(MOBILE);
        await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);

        // Switch to continuous mode so the per-image long-press
        // handler is wired (single mode goes through TapZone). Both
        // codepaths funnel into the same Sheet, but the per-image
        // wiring is the riskier of the two — it's the one the audit
        // flagged for the multitouch fix.
        await page.getByTestId('reader-settings-btn').click();
        await page
            .getByTestId('reader-settings-sheet')
            .getByRole('radio', { name: 'Continuous' })
            .click();
        // Close the settings sheet so its scrim isn't blocking.
        await page.keyboard.press('Escape');

        const pageEl = page.getByTestId('reader-page-1');
        await expect(pageEl).toBeVisible();

        // Synthesize a touch pointerdown, wait past the 450ms timer.
        const box = await pageEl.boundingBox();
        if (!box) throw new Error('page image has no bounding box');
        const cx = box.x + box.width / 2;
        const cy = box.y + box.height / 2;

        await pageEl.dispatchEvent('pointerdown', {
            pointerType: 'touch',
            clientX: cx,
            clientY: cy,
            bubbles: true
        });
        await page.waitForTimeout(550);

        await expect(page.getByTestId('page-action-sheet')).toBeVisible();
        await expect(page.getByTestId('page-action-add-to-collection')).toBeVisible();
        await expect(page.getByTestId('page-action-add-tag')).toBeVisible();
        await expect(page.getByTestId('page-action-save-image')).toBeVisible();
        await expect(page.getByTestId('page-action-copy-link')).toBeVisible();

        // The editor itself opens as a bottom Sheet on mobile (not a centered
        // Modal), so the action-sheet → editor flow stays one consistent
        // idiom. Sheet renders a `${testid}-scrim`; Modal a `${testid}-backdrop`.
        await page.getByTestId('page-action-add-tag').click();
        await expect(page.getByTestId('add-tags-modal-scrim')).toBeVisible();
        await expect(page.getByTestId('add-tags-modal-backdrop')).toHaveCount(0);
    });
});
