import { expect, test } from '../playwright/fixtures';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import {
    assertThatTimerHasStarted,
    assertThatTimerIsStopped,
    newTimeEntryResponse,
    startOrStopTimerWithButton,
    stoppedTimeEntryResponse,
} from './utils/currentTimeEntry';
import type { Page } from '@playwright/test';
import { newTagResponse } from './utils/tags';
import {
    createProjectViaApi,
    createTaskViaApi,
    createClientViaApi,
    createTimeEntryViaApi,
    archiveProjectViaApi,
    markTaskDoneViaApi,
    updateOrganizationCurrencyViaWeb,
} from './utils/api';

// Date picker button name patterns for different date formats
const DATE_DISPLAY_PATTERN = /^\d{4}-\d{2}-\d{2}$|^\d{2}\/\d{2}\/\d{4}$|^\d{2}\.\d{2}\.\d{4}$/;

async function goToDashboard(page: Page) {
    await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
}

test('test that starting and stopping a timer without description and project works', async ({
    page,
}) => {
    await goToDashboard(page);
    await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerHasStarted(page);
    await page.waitForTimeout(1500);
    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that billable icon shows dollar sign for USD currency', async ({ page, ctx }) => {
    await updateOrganizationCurrencyViaWeb(page, ctx, 'USD');
    await goToDashboard(page);
    await page.waitForLoadState('networkidle');
    const billableButton = page.getByRole('button', { name: 'Non Billable' }).first();
    await expect(billableButton).toBeVisible();
    await expect(billableButton.locator('svg')).toHaveAttribute('viewBox', '0 0 8 14');
});

test('test that billable icon shows euro sign for EUR currency', async ({ page, ctx }) => {
    await updateOrganizationCurrencyViaWeb(page, ctx, 'EUR');
    await goToDashboard(page);
    await page.waitForLoadState('networkidle');
    const billableButton = page.getByRole('button', { name: 'Non Billable' }).first();
    await expect(billableButton).toBeVisible();
    await expect(billableButton.locator('svg')).toHaveAttribute('viewBox', '0 0 12 12');
});

test('test that starting and stopping a timer with a description works', async ({ page }) => {
    await goToDashboard(page);
    // Wait for the description input to be editable before filling
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    await page.getByTestId('time_entry_description').fill('New Time Entry Description');
    await Promise.all([
        newTimeEntryResponse(page, {
            description: 'New Time Entry Description',
        }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerHasStarted(page);
    await page.waitForTimeout(1500);
    await Promise.all([
        stoppedTimeEntryResponse(page, {
            description: 'New Time Entry Description',
        }),
        await startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that starting the time entry starts the live timer and that it keeps running after reload', async ({
    page,
}) => {
    await goToDashboard(page);

    await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerHasStarted(page);
    const beforeTimerValue = await page.getByTestId('time_entry_time').inputValue();
    await page.waitForTimeout(2000);
    const afterWaitTimeValue = await page.getByTestId('time_entry_time').inputValue();
    expect(afterWaitTimeValue).not.toEqual(beforeTimerValue);
    await page.reload();
    await expect(page.getByTestId('time_entry_time')).toBeVisible();

    const afterReloadTimerValue = await page.getByTestId('time_entry_time').inputValue();
    await page.waitForTimeout(2000);
    const afterReloadAfterWaitTimerValue = await page.getByTestId('time_entry_time').inputValue();
    expect(afterReloadTimerValue).not.toEqual(afterReloadAfterWaitTimerValue);
});

test('test that starting and updating the description while running works', async ({ page }) => {
    await goToDashboard(page);

    await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerHasStarted(page);
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    await page.getByTestId('time_entry_description').fill('New Time Entry Description');

    await Promise.all([
        newTimeEntryResponse(page, {
            status: 200,
            description: 'New Time Entry Description',
        }),
        page.getByTestId('time_entry_description').press('Tab'),
    ]);
    await Promise.all([
        stoppedTimeEntryResponse(page, {
            description: 'New Time Entry Description',
        }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that starting and updating the time while running works', async ({ page }) => {
    await goToDashboard(page);
    const [createResponse] = await Promise.all([
        newTimeEntryResponse(page),
        await startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerHasStarted(page);
    await expect(page.getByTestId('time_entry_time')).toBeEditable();
    await page.getByTestId('time_entry_time').fill('20min');

    await Promise.all([
        page.waitForResponse(async (response) => {
            return (
                response.url().includes('/time-entries') &&
                response.status() === 200 &&
                (await response.headerValue('Content-Type')) === 'application/json' &&
                (await response.json()).data.id !== null &&
                (await response.json()).data.start !== null &&
                (await response.json()).data.start !== (await createResponse.json()).data.start &&
                (await response.json()).data.end === null &&
                (await response.json()).data.project_id === null &&
                (await response.json()).data.description === '' &&
                (await response.json()).data.task_id === null &&
                (await response.json()).data.user_id !== null &&
                JSON.stringify((await response.json()).data.tags) === JSON.stringify([])
            );
        }),
        page.getByTestId('time_entry_time').press('Enter'),
    ]);

    await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);
    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that entering a human readable time starts the timer on blur', async ({ page }) => {
    await goToDashboard(page);
    await page.getByTestId('time_entry_time').fill('20min');
    await Promise.all([
        newTimeEntryResponse(page),
        page.getByTestId('time_entry_time').press('Tab'),
    ]);
    await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20:/);
    await assertThatTimerHasStarted(page);

    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that entering a number in the time range starts the timer on blur', async ({ page }) => {
    await goToDashboard(page);
    await page.getByTestId('time_entry_time').fill('5');
    await Promise.all([
        newTimeEntryResponse(page),
        page.getByTestId('time_entry_time').press('Tab'),
    ]);
    await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:05:/);
    await assertThatTimerHasStarted(page);

    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that entering a value with the format hh:mm in the time range starts the timer on blur', async ({
    page,
}) => {
    await goToDashboard(page);
    await page.getByTestId('time_entry_time').fill('12:30');
    await Promise.all([
        newTimeEntryResponse(page),
        page.getByTestId('time_entry_time').press('Tab'),
    ]);
    await expect(page.getByTestId('time_entry_time')).toHaveValue(/12:30:/);
    await assertThatTimerHasStarted(page);

    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that entering a random value in the time range does not start the timer on blur', async ({
    page,
}) => {
    await goToDashboard(page);
    await page.getByTestId('time_entry_time').fill('asdasdasd');
    await page.getByTestId('time_entry_time').press('Tab');
    await assertThatTimerIsStopped(page);
});

test('test that entering a time starts the timer on enter', async ({ page }) => {
    await goToDashboard(page);
    await page.getByTestId('time_entry_time').fill('20min');
    await Promise.all([
        newTimeEntryResponse(page),
        page.getByTestId('time_entry_time').press('Enter'),
    ]);
    await assertThatTimerHasStarted(page);
    await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerIsStopped(page);
});

test('test that adding a new tag works', async ({ page }) => {
    const newTagName = 'New Tag' + Math.floor(Math.random() * 10000);
    await goToDashboard(page);

    await page.getByTestId('tag_dropdown').click();
    await page.getByText('Create new tag').click();
    await page.getByPlaceholder('Tag Name').fill(newTagName);

    await Promise.all([
        newTagResponse(page, { name: newTagName }),
        page.getByRole('button', { name: 'Create Tag' }).click(),
    ]);

    // Wait for tags query refetch after invalidation
    await page.waitForResponse(
        (response) => response.url().includes('/tags') && response.status() === 200
    );

    await page.getByTestId('tag_dropdown').click();
    await expect(page.getByRole('option', { name: newTagName })).toBeVisible();
});

test('test that adding a new tag when the timer is running', async ({ page }) => {
    const newTagName = 'New Tag' + Math.floor(Math.random() * 10000);
    await goToDashboard(page);
    await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerHasStarted(page);
    await page.getByTestId('tag_dropdown').click();
    await page.getByText('Create new tag').click();
    await page.getByPlaceholder('Tag Name').fill(newTagName);

    const [tagCreateResponse] = await Promise.all([
        newTagResponse(page, { name: newTagName }),
        page.getByRole('button', { name: 'Create Tag' }).click(),
    ]);
    const tagId = (await tagCreateResponse.json()).data.id;
    await newTimeEntryResponse(page, { status: 200, tags: [tagId] });
    await page.getByTestId('tag_dropdown').click();
    await expect(page.getByRole('option', { name: newTagName })).toBeVisible();
    await page.getByTestId('tag_dropdown_search').press('Escape');
    await expect(page.getByTestId('tag_dropdown_search')).not.toBeVisible();

    await Promise.all([
        stoppedTimeEntryResponse(page, { tags: [tagId] }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that setting an end time with a different date via the timetracker range selector works', async ({
    page,
}) => {
    await goToDashboard(page);

    // Start a timer
    await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
    await assertThatTimerHasStarted(page);

    // Open the time range dropdown by clicking on the time display
    await page.getByTestId('time_entry_time').click();
    const rangeStart = page.getByTestId('time_entry_range_start');
    await expect(rangeStart).toBeVisible();

    // Click "Set End Time" button
    await page.getByRole('button', { name: 'Set End Time' }).click();

    // The end time picker should now be visible with a Confirm button
    const rangeEnd = page.getByTestId('time_entry_range_end');
    await expect(rangeEnd).toBeVisible();
    const confirmButton = page.getByRole('button', { name: 'Confirm' });
    await expect(confirmButton).toBeVisible();

    // Click the end date picker to change the date
    const endDatePickers = page.getByRole('button', { name: DATE_DISPLAY_PATTERN });
    // The second date picker is the end date (first is the start date)
    const endDatePicker = endDatePickers.nth(1);
    await expect(endDatePicker).toBeVisible();
    await endDatePicker.click();

    // Calendar should appear
    const calendarGrid = page.getByRole('gridcell').first();
    await expect(calendarGrid).toBeVisible({ timeout: 5000 });

    // Navigate to the next month and select a day to ensure end > start
    await page.getByRole('button', { name: /Next/i }).click();
    await page.getByRole('gridcell').filter({ hasText: /^15$/ }).first().click();

    // The dropdown should still be open after selecting a date (not auto-closed)
    await expect(rangeEnd).toBeVisible();
    await expect(confirmButton).toBeVisible();

    // Click Confirm to finalize and verify the API call
    const [updateResponse] = await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/time-entries') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        confirmButton.click(),
    ]);
    const updateBody = await updateResponse.json();
    expect(updateBody.data.start).toBeTruthy();
    expect(updateBody.data.end).toBeTruthy();
});

test('test that timer starts on enter with description', async ({ page }) => {
    await goToDashboard(page);
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    await page.getByTestId('time_entry_description').fill('Start on Enter');

    await Promise.all([
        newTimeEntryResponse(page, { description: 'Start on Enter' }),
        page.getByTestId('time_entry_description').press('Enter'),
    ]);
    await assertThatTimerHasStarted(page);

    await Promise.all([
        stoppedTimeEntryResponse(page, { description: 'Start on Enter' }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that timer started on dashboard is visible on time page', async ({ page }) => {
    await goToDashboard(page);

    // Start timer on dashboard
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    await page.getByTestId('time_entry_description').fill('Sync test');
    await Promise.all([
        newTimeEntryResponse(page, { description: 'Sync test' }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerHasStarted(page);

    // Navigate to time page
    await page.goto(PLAYWRIGHT_BASE_URL + '/time');

    // Timer should still be running (the timer button should be red/active)
    await expect(
        page
            .getByTestId('dashboard_timer')
            .getByTestId('timer_button')
            .and(page.locator(':visible'))
    ).toHaveClass(/bg-red-400\/80/);

    // Stop the timer
    await Promise.all([
        stoppedTimeEntryResponse(page, { description: 'Sync test' }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that picking a recently tracked entry starts a timer with its fields', async ({
    page,
    ctx,
}) => {
    const project = await createProjectViaApi(ctx, {
        name: `RecentProj ${Math.floor(Math.random() * 100000)}`,
        is_billable: false,
    });
    await createTimeEntryViaApi(ctx, {
        description: 'Recent work item',
        duration: '1h',
        projectId: project.id,
    });

    await goToDashboard(page);
    const description = page.getByTestId('time_entry_description');
    await expect(description).toBeEditable();

    // Focusing the description opens the "Recently Tracked" dropdown listing the finished entry.
    await description.click();
    const recentEntry = page.getByText('Recent work item').first();
    await expect(recentEntry).toBeVisible();

    // Clicking it (mousedown) copies its fields — including the project — into a new running entry.
    await Promise.all([
        page.waitForResponse(async (response) => {
            if (
                !response.url().includes('/time-entries') ||
                response.request().method() !== 'POST' ||
                response.status() !== 201
            ) {
                return false;
            }
            const body = await response.json();
            return (
                body.data.description === 'Recent work item' &&
                body.data.project_id === project.id &&
                body.data.end === null
            );
        }),
        recentEntry.click(),
    ]);
    await assertThatTimerHasStarted(page);
    await expect(description).toHaveValue('Recent work item');
    await expect(page.getByRole('button', { name: project.name })).toBeVisible();

    // Cleanup: stop the running (project-bearing) entry
    await Promise.all([
        page.waitForResponse(async (response) => {
            if (response.status() !== 200 || !response.url().includes('/time-entries/')) {
                return false;
            }
            const body = await response.json();
            return body.data.description === 'Recent work item' && body.data.end !== null;
        }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

test('test that creating a new project from the time tracker dropdown prefills the search text', async ({
    page,
    ctx,
}) => {
    const existingProjectName = 'Existing Project ' + Math.floor(Math.random() * 10000);
    const searchText = 'PrefillProject ' + Math.floor(Math.random() * 10000);

    // Create a project so the dropdown renders (not the "Add new project" button)
    await createProjectViaApi(ctx, { name: existingProjectName });
    await goToDashboard(page);

    // Open the project dropdown
    await page.getByRole('button', { name: 'No Project' }).click();

    // Type a search term that won't match any existing project
    await page.getByTestId('client_dropdown_search').fill(searchText);

    // Click "Create new Project"
    await page.getByText('Create new Project').click();

    // Verify the project name input is pre-filled with the search text
    await expect(page.getByLabel('Project name')).toHaveValue(searchText);

    // Complete project creation to verify full flow works
    await Promise.all([
        page.waitForResponse(
            async (response) =>
                response.url().includes('/projects') &&
                response.request().method() === 'POST' &&
                response.status() === 201 &&
                (await response.json()).data.name === searchText
        ),
        page.getByRole('button', { name: 'Create Project' }).click(),
    ]);

    // The project dropdown should now show the newly created project
    await expect(page.getByRole('button', { name: searchText })).toBeVisible();
});

test('test that adding a project and tag before starting timer works', async ({ page }) => {
    const newTagName = 'TimerTag ' + Math.floor(Math.random() * 10000);
    await goToDashboard(page);

    // Create and select a tag first
    await page.getByTestId('tag_dropdown').click();
    await page.getByText('Create new tag').click();
    await page.getByPlaceholder('Tag Name').fill(newTagName);

    const [tagCreateResponse] = await Promise.all([
        newTagResponse(page, { name: newTagName }),
        page.getByRole('button', { name: 'Create Tag' }).click(),
    ]);
    const tagId = (await tagCreateResponse.json()).data.id;

    // Wait for tags query refetch (tag is auto-selected after creation)
    await page.waitForResponse(
        (response) => response.url().includes('/tags') && response.status() === 200
    );

    // Fill description and start
    await page.getByTestId('time_entry_description').fill('Entry with tag');
    await Promise.all([
        newTimeEntryResponse(page, { description: 'Entry with tag', tags: [tagId] }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerHasStarted(page);

    await Promise.all([
        stoppedTimeEntryResponse(page, { description: 'Entry with tag', tags: [tagId] }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);
});

// ──────────────────────────────────────────────────
// Project / Task selector dropdown
// Regression coverage for the virtualized + lookup-map refactor of
// TimeTrackerProjectTaskDropdown. The dropdown only (re)filters on open and on search
// change, so we wait for the dashboard prefetch to settle before opening it.
// ──────────────────────────────────────────────────

test.describe('Project Task Dropdown', () => {
    test.describe.configure({ timeout: 60_000 });

    test('test that a project far down a long list can be found via search and selected', async ({
        page,
        ctx,
    }) => {
        // Seed enough projects that the target sits outside the initially rendered window.
        const seed = Math.floor(Math.random() * 100000);
        const prefix = `VirtProj ${seed} `;
        await Promise.all(
            Array.from({ length: 30 }, (_, i) =>
                createProjectViaApi(ctx, { name: prefix + String(i).padStart(2, '0') })
            )
        );
        const target = prefix + '27';

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        await page.getByTestId('client_dropdown_search').fill(target);
        await page.getByRole('option').filter({ hasText: target }).click();

        // The trigger now reflects the selected project.
        await expect(page.getByRole('button', { name: target })).toBeVisible();
    });

    test('test that expanding a project and selecting a task works', async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectName = `ExpandProj ${seed}`;
        const taskName = `ExpandTask ${seed}`;
        const project = await createProjectViaApi(ctx, { name: projectName });
        await createTaskViaApi(ctx, { name: taskName, project_id: project.id });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const projectOption = page.getByRole('option').filter({ hasText: projectName });
        await expect(projectOption).toBeVisible();

        // Expand the project's tasks via the "N Tasks" button, then select the task.
        await projectOption.getByText(/Tasks/).click();
        await page.getByText(taskName, { exact: true }).click();

        // Scoped to the trigger button: the closing dropdown also contains the name while animating out.
        await expect(
            page.getByRole('button', { name: `${projectName} ${taskName}` })
        ).toBeVisible();
    });

    test('test that keyboard navigation selects a project', async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectName = `KbProj ${seed}`;
        await createProjectViaApi(ctx, { name: projectName });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const search = page.getByTestId('client_dropdown_search');
        // On open the search is focused and "No Project" is highlighted.
        await expect(search).toBeFocused();

        // Arrow down from "No Project" to the project, then select it with Enter.
        await search.press('ArrowDown');
        await search.press('Enter');

        await expect(page.getByRole('button', { name: projectName })).toBeVisible();
    });

    test('test that search filters the dropdown by project and client name', async ({
        page,
        ctx,
    }) => {
        const seed = Math.floor(Math.random() * 100000);
        const clientName = `FilterClient ${seed}`;
        const alphaProject = `AlphaProj ${seed}`;
        const betaProject = `BetaProj ${seed}`;
        const client = await createClientViaApi(ctx, { name: clientName });
        await createProjectViaApi(ctx, { name: alphaProject, client_id: client.id });
        await createProjectViaApi(ctx, { name: betaProject });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const search = page.getByTestId('client_dropdown_search');
        const alphaOption = page.getByRole('option').filter({ hasText: alphaProject });
        const betaOption = page.getByRole('option').filter({ hasText: betaProject });

        // Both projects are visible before filtering.
        await expect(alphaOption).toBeVisible();
        await expect(betaOption).toBeVisible();

        // Project-name search shows only the matching project.
        await search.fill('AlphaProj');
        await expect(alphaOption).toBeVisible();
        await expect(betaOption).not.toBeVisible();

        // Client-name search shows the project that belongs to that client.
        await search.fill(clientName);
        await expect(alphaOption).toBeVisible();
        await expect(betaOption).not.toBeVisible();
    });

    test("test that searching by task name surfaces the task's project", async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectWithTask = `TaskSearchProj ${seed}`;
        const taskName = `Findable Task ${seed}`;
        const unrelatedProject = `Unrelated Proj ${seed}`;
        const project = await createProjectViaApi(ctx, { name: projectWithTask });
        await createTaskViaApi(ctx, { name: taskName, project_id: project.id });
        await createProjectViaApi(ctx, { name: unrelatedProject });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        await page.getByTestId('client_dropdown_search').fill(taskName);

        // The project owning the task is shown (with the task), the unrelated project is not.
        await expect(page.getByRole('option').filter({ hasText: projectWithTask })).toBeVisible();
        await expect(page.getByText(taskName, { exact: true })).toBeVisible();
        await expect(
            page.getByRole('option').filter({ hasText: unrelatedProject })
        ).not.toBeVisible();
    });

    test('test that archived projects are hidden from the dropdown', async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const activeProject = `ActiveProj ${seed}`;
        const archivedProject = `ArchivedProj ${seed}`;
        await createProjectViaApi(ctx, { name: activeProject });
        const toArchive = await createProjectViaApi(ctx, { name: archivedProject });
        await archiveProjectViaApi(ctx, toArchive);

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();

        // Wait for the list to load, then confirm the archived project is filtered out.
        await expect(page.getByRole('option').filter({ hasText: activeProject })).toBeVisible();
        await expect(
            page.getByRole('option').filter({ hasText: archivedProject })
        ).not.toBeVisible();
    });

    test('test that done tasks are hidden when expanding a project', async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectName = `DoneTaskProj ${seed}`;
        const activeTask = `Active Task ${seed}`;
        const doneTask = `Done Task ${seed}`;
        const project = await createProjectViaApi(ctx, { name: projectName });
        await createTaskViaApi(ctx, { name: activeTask, project_id: project.id });
        const taskToFinish = await createTaskViaApi(ctx, {
            name: doneTask,
            project_id: project.id,
        });
        await markTaskDoneViaApi(ctx, taskToFinish);

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const projectOption = page.getByRole('option').filter({ hasText: projectName });
        await expect(projectOption).toBeVisible();
        await projectOption.getByText(/Tasks/).click();

        // Only the active task shows; the done task is filtered out.
        await expect(page.getByText(activeTask, { exact: true })).toBeVisible();
        await expect(page.getByText(doneTask, { exact: true })).not.toBeVisible();
    });

    test('test that keyboard navigation can expand a project and select a task', async ({
        page,
        ctx,
    }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectName = `KbTaskProj ${seed}`;
        const taskName = `KbTask ${seed}`;
        const project = await createProjectViaApi(ctx, { name: projectName });
        await createTaskViaApi(ctx, { name: taskName, project_id: project.id });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const search = page.getByTestId('client_dropdown_search');
        await expect(search).toBeFocused();

        // No Project is highlighted on open: down to the project, right to expand its tasks,
        // down to the task, Enter to select it.
        await search.press('ArrowDown');
        await search.press('ArrowRight');
        await search.press('ArrowDown');
        await search.press('Enter');

        // Scoped to the trigger button: the closing dropdown also contains the name while animating out.
        await expect(
            page.getByRole('button', { name: `${projectName} ${taskName}` })
        ).toBeVisible();
    });

    test('test that pressing space selects the highlighted project', async ({ page, ctx }) => {
        const seed = Math.floor(Math.random() * 100000);
        const projectName = `SpaceProj ${seed}`;
        await createProjectViaApi(ctx, { name: projectName });

        await goToDashboard(page);
        await page.waitForLoadState('networkidle');

        await page.getByRole('button', { name: 'No Project' }).click();
        const search = page.getByTestId('client_dropdown_search');
        await expect(search).toBeFocused();

        // Arrow down from "No Project" to the project, then the space shortcut selects it.
        await search.press('ArrowDown');
        await search.press('Space');

        await expect(page.getByRole('button', { name: projectName })).toBeVisible();
    });
});

test('test that simple mode hides the project, tag and billable controls', async ({ page }) => {
    await goToDashboard(page);
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    // Project mode shows the project and billable controls
    await expect(page.getByRole('button', { name: 'No Project' })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Non Billable' }).first()).toBeVisible();

    // Switch to simple mode via the more options dropdown (client-side preference, no request)
    await page.getByRole('button', { name: 'Time entry actions' }).click();
    await page.getByRole('menuitem', { name: 'Switch to simple mode' }).click();

    // Simple mode is the project tracker without the project/tag/billable selectors; the
    // description input and clock-in/out stay.
    await expect(page.getByTestId('time_entry_description')).toBeEditable();
    await expect(page.getByRole('button', { name: 'No Project' })).toHaveCount(0);
    await expect(page.getByRole('button', { name: 'Non Billable' })).toHaveCount(0);

    // Clock in and out
    await Promise.all([
        newTimeEntryResponse(page, { type: 'work' }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerHasStarted(page);
    await page.waitForTimeout(1500);
    await Promise.all([
        stoppedTimeEntryResponse(page, { type: 'work' }),
        startOrStopTimerWithButton(page),
    ]);
    await assertThatTimerIsStopped(page);

    // Switch back to project mode: the controls return
    await page.getByRole('button', { name: 'Time entry actions' }).click();
    await page.getByRole('menuitem', { name: 'Switch to project mode' }).click();
    await expect(page.getByRole('button', { name: 'No Project' })).toBeVisible();
});
