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

// Mocks the auth endpoints at the network level so the journey is
// deterministic and doesn't require a live backend.

const userFixture = {
    id: 'user-1',
    username: 'alice',
    created_at: '2026-01-01T00:00:00Z'
};
const emptyPage = { items: [], page: { limit: 50, offset: 0, total: null } };

async function stubAnonymousThenAuthenticated(page: Page) {
    let loggedIn = false;
    await page.route('**/api/v1/auth/me', async (route) => {
        if (loggedIn) {
            await route.fulfill({
                status: 200,
                contentType: 'application/json',
                body: JSON.stringify({ user: userFixture })
            });
        } else {
            await route.fulfill({
                status: 401,
                contentType: 'application/json',
                body: JSON.stringify({
                    error: { code: 'unauthenticated', message: 'unauthenticated' }
                })
            });
        }
    });
    await page.route('**/api/v1/auth/login', async (route) => {
        loggedIn = true;
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({ user: userFixture })
        });
    });
    await page.route('**/api/v1/auth/logout', async (route) => {
        loggedIn = false;
        await route.fulfill({ status: 204 });
    });
    await page.route('**/api/v1/mangas*', async (route) => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify(emptyPage)
        });
    });
}

test('login then logout flips the layout between authenticated and anonymous', async ({
    page
}) => {
    await stubAnonymousThenAuthenticated(page);

    await page.goto('/');
    // Initially anonymous → Login / Register links visible.
    await expect(page.getByTestId('nav-login')).toBeVisible();

    // Log in.
    await page.goto('/login');
    // Wait for hydration to finish before interacting — the nav-login link
    // is only rendered once /me resolves, so it doubles as a hydration
    // signal. Clicking before hydration would submit the form via the
    // browser default (action="javascript:void(0)") and our handler would
    // never run.
    await expect(page.getByTestId('nav-login')).toBeVisible();
    await page.getByTestId('login-username').fill('alice');
    await page.getByTestId('login-password').fill('hunter2hunter2');
    await page.getByTestId('login-submit').click();

    // Authenticated → username + Logout button.
    await expect(page.getByTestId('session-user')).toContainText('alice');
    await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible();

    // Log out.
    await page.getByRole('button', { name: 'Logout' }).click();
    await expect(page).toHaveURL(/\/login$/);
    await expect(page.getByTestId('nav-login')).toBeVisible();
});

test('login redirects to a safe ?next= target after authenticating', async ({ page }) => {
    await stubAnonymousThenAuthenticated(page);

    // Arrive at /login carrying a same-origin ?next= (the shape the layout
    // produces when bouncing an unauthenticated user off a gated page).
    await page.goto('/login?next=%2Fsearch');
    await expect(page.getByTestId('nav-login')).toBeVisible();
    await page.getByTestId('login-username').fill('alice');
    await page.getByTestId('login-password').fill('hunter2hunter2');
    await page.getByTestId('login-submit').click();

    // Lands on the requested page, not the default root.
    await expect(page).toHaveURL(/\/search$/);
});

test('login ignores an unsafe ?next= and falls back to /', async ({ page }) => {
    await stubAnonymousThenAuthenticated(page);

    // A protocol-relative open-redirect attempt must be discarded.
    await page.goto('/login?next=%2F%2Fevil.com');
    await expect(page.getByTestId('nav-login')).toBeVisible();
    await page.getByTestId('login-username').fill('alice');
    await page.getByTestId('login-password').fill('hunter2hunter2');
    await page.getByTestId('login-submit').click();

    // Authenticated and on the site root — never navigated off-origin.
    await expect(page).toHaveURL(/\/$/);
    await expect(page.getByTestId('session-user')).toContainText('alice');
});

test('login rejects the %09 control-char open-redirect bypass', async ({ page }) => {
    await stubAnonymousThenAuthenticated(page);

    // "/\t/evil.com" passes a naive startsWith('/') && !startsWith('//')
    // guard but collapses to "//evil.com" once the browser strips the tab.
    await page.goto('/login?next=%2F%09%2Fevil.com');
    await expect(page.getByTestId('nav-login')).toBeVisible();
    await page.getByTestId('login-username').fill('alice');
    await page.getByTestId('login-password').fill('hunter2hunter2');
    await page.getByTestId('login-submit').click();

    // Stayed on-origin at the root, authenticated.
    await expect(page).toHaveURL(/\/$/);
    await expect(page.getByTestId('session-user')).toContainText('alice');
});

test('login surfaces the API error message on bad credentials', async ({ page }) => {
    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/login', async (route) => {
        await route.fulfill({
            status: 401,
            contentType: 'application/json',
            body: JSON.stringify({
                error: { code: 'unauthenticated', message: 'unauthenticated' }
            })
        });
    });

    await page.goto('/login');
    await expect(page.getByTestId('nav-login')).toBeVisible();
    await page.getByTestId('login-username').fill('alice');
    await page.getByTestId('login-password').fill('wrongpassword');
    await page.getByTestId('login-submit').click();

    await expect(page.getByTestId('login-error')).toBeVisible();
});
