import { test, expect, type Page } from './fixtures';
import { SORT_FIELD_LABELS } from '../src/lib/mangaSort';

// These E2E tests run against the SvelteKit dev server, which proxies /api
// to the backend. Playwright starts vite via `webServer` (see
// playwright.config.ts) unless E2E_BASE_URL points at a different deployment.
//
// Routes are mocked at the page level so the journeys are deterministic and
// don't require a live backend.

const emptyPage = { items: [], page: { limit: 50, offset: 0, total: null } };

async function mockAnonymous(page: Page) {
    // Force public mode so the root +layout.ts doesn't bounce us to /login
    // (a dev backend with PRIVATE_MODE=true must not leak into E2E runs).
    await page.route('**/api/v1/auth/config', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ self_register_enabled: true, private_mode: false })
        });
    });
    await page.route('**/api/v1/auth/me', async (route) => {
        await route.fulfill({
            status: 401,
            contentType: 'application/json',
            body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } })
        });
    });
    await page.route('**/api/v1/auth/me/preferences', async (route) => {
        await route.fulfill({
            status: 401,
            contentType: 'application/json',
            body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } })
        });
    });
    // The home page fetches genres on mount (filter UI). Mocking it keeps
    // `onMount` fast and deterministic — otherwise the unmocked call stalls
    // against the dead dev proxy, and the late-resolving initial load races
    // (and clobbers) a load kicked off by an early search interaction.
    await page.route('**/api/v1/genres*', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify([])
        });
    });
}

test('home page renders the Mangalord heading and search input', async ({ page }) => {
    await mockAnonymous(page);
    await page.route('**/api/v1/mangas*', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(emptyPage)
        });
    });

    await page.goto('/');
    await expect(page.getByRole('link', { name: 'Mangalord' })).toBeVisible();
    await expect(page.getByTestId('search-input')).toBeVisible();
    await expect(page.getByTestId('empty')).toContainText('No mangas yet');
});

test('shows a skeleton grid while the catalog is loading, then the real grid', async ({ page }) => {
    await mockAnonymous(page);

    // Hold the catalog fetch open so the loading state is observable.
    let release: () => void = () => {};
    const gate = new Promise<void>((resolve) => {
        release = resolve;
    });
    await page.route('**/api/v1/mangas*', async (route) => {
        await gate;
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items: [
                    {
                        id: 'm1',
                        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: []
                    }
                ],
                page: { limit: 50, offset: 0, total: 1 }
            })
        });
    });

    await page.goto('/');

    // Skeleton stands in for the grid while the fetch is pending.
    await expect(page.getByTestId('manga-grid-skeleton')).toBeVisible();
    await expect(page.getByTestId('manga-list')).toHaveCount(0);

    // Once data lands, the real grid replaces the skeleton.
    release();
    await expect(page.getByTestId('manga-list')).toContainText('Berserk');
    await expect(page.getByTestId('manga-grid-skeleton')).toHaveCount(0);
});

// Anti-drift guard: the desktop sort <select> and the mobile sort sheet must
// both enumerate exactly SORT_FIELD_LABELS, in the same order. The desktop
// select used to hard-code its <option>s, so a label rename in mangaSort.ts
// would silently diverge the two surfaces. Comparing the rendered options to
// the single source of truth makes that divergence fail loudly.
test('desktop sort options stay in sync with SORT_FIELD_LABELS', async ({ page }) => {
    await mockAnonymous(page);
    await page.route('**/api/v1/mangas*', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(emptyPage)
        });
    });

    await page.goto('/');
    const select = page.getByTestId('sort-select');
    await expect(select).toBeVisible();

    const options = select.locator('option');
    const labels = (await options.allTextContents()).map((s) => s.trim());
    const values = await options.evaluateAll((opts) =>
        opts.map((o) => (o as HTMLOptionElement).value)
    );

    expect(values).toEqual(Object.keys(SORT_FIELD_LABELS));
    expect(labels).toEqual(Object.values(SORT_FIELD_LABELS));
});

test('search updates the manga list', async ({ page }) => {
    await mockAnonymous(page);
    let lastSearch: string | null = null;
    await page.route('**/api/v1/mangas*', async (route) => {
        const url = new URL(route.request().url());
        lastSearch = url.searchParams.get('search');
        const items =
            lastSearch === 'berserk'
                ? [
                      {
                          id: 'b1',
                          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',
                          // MangaCard = Manga & { authors, genres }; the card
                          // component maps over both, so they must be present.
                          authors: [{ id: 'a1', name: 'Kentaro Miura' }],
                          genres: []
                      }
                  ]
                : [];
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ items, page: { limit: 50, offset: 0, total: null } })
        });
    });

    await page.goto('/');
    // Wait for the initial (empty) catalogue to settle before searching, so
    // the search's fetch can't race the mount-time load — mirrors how the
    // pagination test waits for the first render before interacting.
    await expect(page.getByTestId('empty')).toBeVisible();

    await page.getByTestId('search-input').fill('berserk');
    await page.getByRole('button', { name: 'Search' }).click();

    await expect(page.getByTestId('manga-list')).toContainText('Berserk');
    expect(lastSearch).toBe('berserk');
});

test('clicking Next paginates to page 2 and updates the URL', async ({ page }) => {
    await mockAnonymous(page);

    // Fake a catalogue of 75 mangas; page 1 is ids 1..50, page 2 is ids 51..75.
    const TOTAL = 75;
    function mangaAt(i: number) {
        return {
            id: `m${i}`,
            title: `Manga ${i}`,
            author: 'Test',
            description: null,
            cover_image_path: null,
            created_at: '2026-01-01T00:00:00Z',
            updated_at: '2026-01-01T00:00:00Z',
            authors: [],
            genres: []
        };
    }

    await page.route('**/api/v1/mangas*', async (route) => {
        const url = new URL(route.request().url());
        const limit = Number(url.searchParams.get('limit') ?? '50');
        const offset = Number(url.searchParams.get('offset') ?? '0');
        const items: ReturnType<typeof mangaAt>[] = [];
        for (let i = offset + 1; i <= Math.min(offset + limit, TOTAL); i++) {
            items.push(mangaAt(i));
        }
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                items,
                page: { limit, offset, total: TOTAL }
            })
        });
    });

    await page.goto('/');
    await expect(page.getByTestId('manga-total')).toContainText('Showing 1–50 of 75');
    await expect(page.getByTestId('manga-list')).toContainText('Manga 1');
    await expect(page.getByTestId('manga-list')).not.toContainText('Manga 75');

    await page.getByTestId('manga-pager').getByRole('button', { name: /next/i }).click();

    await expect(page).toHaveURL(/[?&]page=2(&|$)/);
    await expect(page.getByTestId('manga-total')).toContainText('Showing 51–75 of 75');
    await expect(page.getByTestId('manga-list')).toContainText('Manga 75');
    await expect(page.getByTestId('manga-list')).not.toContainText('Manga 1');
});
