import { expect } from '@playwright/test';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures';

dayjs.extend(utc);
import {
    createProjectViaApi,
    createClientViaApi,
    createTaskViaApi,
    createTimeEntryViaApi,
    createTimeEntryWithTagViaApi,
    createBareTimeEntryViaApi,
    createBillableProjectViaApi,
    createTimeEntryWithBillableStatusViaApi,
    createTagViaApi,
    createReportViaApi,
    updateOrganizationSettingViaApi,
} from './utils/api';
import {
    goToReporting,
    goToReportingShared,
    waitForReportingUpdate,
    saveAsSharedReport,
} from './utils/reporting';

// Each test registers a new user and creates test data via API
test.describe.configure({ timeout: 30000 });

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

// ──────────────────────────────────────────────────
// Shared Report Lifecycle Tests
// ──────────────────────────────────────────────────

test('test that saving a report creates a shared report and its shareable link shows correct data', async ({
    page,
    ctx,
}) => {
    const projectName = 'SharedProject ' + Math.floor(Math.random() * 10000);
    const reportName = 'SharedReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // Verify report appears on shared tab
    await goToReportingShared(page);
    await expect(page.getByTestId('report_table')).toBeVisible();
    await expect(page.getByText(reportName)).toBeVisible();
    await expect(page.getByText('Public', { exact: true })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Copy URL' })).toBeVisible();

    // Navigate to shareable link and verify report data
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText(projectName)).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
});

test('test that a shared report grouped by date shows date labels formatted by the organization setting', async ({
    page,
    ctx,
}) => {
    const reportName = 'DateGroupReport ' + Math.floor(Math.random() * 10000);

    await updateOrganizationSettingViaApi(ctx, { date_format: 'point-separated-d-m-yyyy' });
    await createTimeEntryViaApi(ctx, {
        description: 'Entry for date grouping',
        duration: '1h',
    });

    await goToReporting(page);

    // Switch the grouping to "Date"
    const groupBySelects = page.locator('[data-testid="reporting_view"]').getByRole('combobox');
    await groupBySelects.filter({ hasText: 'Project' }).first().click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/time-entries/aggregate') &&
                response.url().includes('group=day') &&
                response.status() === 200
        ),
        page.getByRole('option', { name: 'Date', exact: true }).click(),
    ]);

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // Verify row labels are formatted correctly
    await page.goto(shareableLink);
    await expect(page.getByText('Total')).toBeVisible();
    await expect(page.getByText(/^\d{1,2}\.\d{1,2}\.\d{4}$/)).toBeVisible();
    await expect(page.getByText(/^\d{4}-\d{2}-\d{2}$/)).toHaveCount(0);
});

test('test that shared report with invalid secret shows no data', async ({ page }) => {
    await page.goto(PLAYWRIGHT_BASE_URL + '/shared-report#invalid-secret-value');
    await expect(page.getByText('No time entries found').first()).toBeVisible();
});

test('test that a shared report can be edited to toggle public/private and then deleted', async ({
    page,
    ctx,
}) => {
    const projectName = 'EditDelProject ' + Math.floor(Math.random() * 10000);
    const reportName = 'EditDelReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    await saveAsSharedReport(page, reportName);

    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();
    await expect(page.getByText('Public', { exact: true })).toBeVisible();

    // Click more options and edit
    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();

    // Uncheck public and save
    await page.getByLabel('Public').click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        page.getByRole('button', { name: 'Update Report' }).click(),
    ]);

    // Verify status changed to private
    await expect(page.getByText('Private')).toBeVisible();
    await expect(page.getByText('--')).toBeVisible();

    // Delete the report
    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'DELETE' &&
                response.status() === 204
        ),
        page.getByRole('menuitem', { name: /^Delete Report/ }).click(),
    ]);

    await expect(page.getByText('No shared reports found')).toBeVisible();
});

// ──────────────────────────────────────────────────
// Shared Report Filter Tests
// ──────────────────────────────────────────────────

test('test that shared report respects project filter', async ({ page, ctx }) => {
    const projectA = 'FilterProjA ' + Math.floor(Math.random() * 10000);
    const projectB = 'FilterProjB ' + Math.floor(Math.random() * 10000);
    const reportName = 'FilterProjReport ' + Math.floor(Math.random() * 10000);

    const projA = await createProjectViaApi(ctx, { name: projectA });
    const projB = await createProjectViaApi(ctx, { name: projectB });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectA}`,
        duration: '1h',
        projectId: projA.id,
    });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectB}`,
        duration: '2h',
        projectId: projB.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectA)).toBeVisible();

    // Filter by project A
    await page.getByRole('button', { name: 'Projects' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: projectA }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText(projectA)).toBeVisible();
    await expect(page.getByText(projectB)).not.toBeVisible();
});

test('test that shared report with No Project filter shows entries without a project', async ({
    page,
    ctx,
}) => {
    const projectName = 'NoProjFilter ' + Math.floor(Math.random() * 10000);
    const reportName = 'NoProjReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });
    await createBareTimeEntryViaApi(ctx, 'Bare entry no project', '2h');

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Filter by "No Project"
    await page.getByRole('button', { name: 'Projects' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: 'No Project' }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    // The "No Project" group should show, but the project name should not appear as a group
    await expect(page.getByText('Total')).toBeVisible();
    await expect(page.getByText(projectName)).not.toBeVisible();
});

test('test that shared report with No Task filter shows entries without a task', async ({
    page,
    ctx,
}) => {
    const projectName = 'NoTaskProj ' + Math.floor(Math.random() * 10000);
    const taskName = 'NoTaskFilter ' + Math.floor(Math.random() * 10000);
    const reportName = 'NoTaskReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    const task = await createTaskViaApi(ctx, { name: taskName, project_id: project.id });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName} - ${taskName}`,
        duration: '1h',
        projectId: project.id,
        taskId: task.id,
    });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '2h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Filter by "No Task"
    await page.getByRole('button', { name: 'Tasks' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: 'No Task' }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
});

test('test that shared report respects task filter', async ({ page, ctx }) => {
    const projectName = 'TaskFilterProj ' + Math.floor(Math.random() * 10000);
    const taskA = 'TaskA ' + Math.floor(Math.random() * 10000);
    const taskB = 'TaskB ' + Math.floor(Math.random() * 10000);
    const reportName = 'TaskFilterReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    const task = await createTaskViaApi(ctx, { name: taskA, project_id: project.id });
    await createTaskViaApi(ctx, { name: taskB, project_id: project.id });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${taskA}`,
        duration: '1h',
        projectId: project.id,
        taskId: task.id,
    });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName} no task`,
        duration: '2h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Filter by task A
    await page.getByRole('button', { name: 'Tasks' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: taskA }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
    await expect(page.getByText('1:00:00').first()).toBeVisible();
    await expect(page.getByText('3:00:00')).not.toBeVisible();
});

test('test that shared report respects client filter', async ({ page, ctx }) => {
    const clientA = 'ClientA ' + Math.floor(Math.random() * 10000);
    const clientB = 'ClientB ' + Math.floor(Math.random() * 10000);
    const projectA = 'ClientFilterProjA ' + Math.floor(Math.random() * 10000);
    const projectB = 'ClientFilterProjB ' + Math.floor(Math.random() * 10000);
    const reportName = 'ClientFilterReport ' + Math.floor(Math.random() * 10000);

    const cliA = await createClientViaApi(ctx, { name: clientA });
    const cliB = await createClientViaApi(ctx, { name: clientB });
    const projA = await createProjectViaApi(ctx, { name: projectA, client_id: cliA.id });
    const projB = await createProjectViaApi(ctx, { name: projectB, client_id: cliB.id });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${clientA}`,
        duration: '1h',
        projectId: projA.id,
    });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${clientB}`,
        duration: '2h',
        projectId: projB.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectA)).toBeVisible();

    // Filter by client A
    await page.getByRole('button', { name: 'Clients' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: clientA }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText(projectA)).toBeVisible();
    await expect(page.getByText(projectB)).not.toBeVisible();
});

test('test that shared report respects tag filter', async ({ page, ctx }) => {
    const tagA = 'TagA ' + Math.floor(Math.random() * 10000);
    const tagB = 'TagB ' + Math.floor(Math.random() * 10000);
    const reportName = 'TagFilterReport ' + Math.floor(Math.random() * 10000);

    const tagObjA = await createTagViaApi(ctx, { name: tagA });
    await createTagViaApi(ctx, { name: tagB });
    await createTimeEntryViaApi(ctx, {
        description: `Entry with ${tagA}`,
        duration: '1h',
        tags: [tagObjA.id],
    });
    await createBareTimeEntryViaApi(ctx, 'Entry no tags', '2h');

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText('Total')).toBeVisible();

    // Filter by tag A
    await page.getByRole('button', { name: 'Tags' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: tagA }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
    await expect(page.getByText('1:00:00').first()).toBeVisible();
    await expect(page.getByText('3:00:00')).not.toBeVisible();
});

test('test that shared report respects member filter', async ({ page, ctx }) => {
    const projectName = 'MemberFilterProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'MemberFilterReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Filter by current member (John Doe)
    await page.getByRole('button', { name: 'Members' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: 'John Doe' }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report — should still show data since all entries belong to this member
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText(projectName)).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
});

test('test that shared report with billable filter only shows billable entries', async ({
    page,
    ctx,
}) => {
    const reportName = 'BillableFilterReport ' + Math.floor(Math.random() * 10000);

    // Create one billable (1h) and one non-billable (2h) entry
    await createTimeEntryWithBillableStatusViaApi(ctx, true, '1h');
    await createTimeEntryWithBillableStatusViaApi(ctx, false, '2h');

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText('Total')).toBeVisible();

    // Filter by billable only
    await page.getByRole('combobox').filter({ hasText: 'Billable' }).click();
    await Promise.all([
        page.getByRole('option', { name: 'Billable', exact: true }).click(),
        waitForReportingUpdate(page),
    ]);

    // Verify only 1h shows before saving
    await expect(page.getByTestId('reporting_view').getByText('1:00:00').first()).toBeVisible();

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // Navigate to the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();

    // Shared report should only show the 1h billable entry, not the 2h non-billable
    await expect(page.getByText('1:00:00').first()).toBeVisible();
    await expect(page.getByText('3:00:00')).not.toBeVisible();
});

// ──────────────────────────────────────────────────
// Report Date Picker Tests
// ──────────────────────────────────────────────────

test('test that creating a report with an expiration date works', async ({ page, ctx }) => {
    const projectName = 'DatePickerProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'DatePickerReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Open the save report modal
    await page.getByRole('button', { name: 'Save Report' }).click();
    await page.getByLabel('Name').fill(reportName);

    // The "Public" checkbox should be checked by default, showing the date picker
    const datePicker = page
        .getByRole('dialog')
        .getByRole('button', { name: DATE_PICKER_BUTTON_PATTERN });
    await expect(datePicker).toBeVisible();
    await datePicker.click();

    // Select a date in the next month
    const calendarGrid = page.getByRole('gridcell').first();
    await expect(calendarGrid).toBeVisible({ timeout: 5000 });
    await page.getByRole('button', { name: /Next/i }).click();
    await page.getByRole('gridcell').filter({ hasText: /^15$/ }).first().click();

    // Wait for the calendar to close
    await expect(calendarGrid).not.toBeVisible();

    // Create the report and verify it includes the public_until date
    const [response] = await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports') &&
                response.request().method() === 'POST' &&
                response.status() === 201
        ),
        page.getByRole('dialog').getByRole('button', { name: 'Create Report' }).click(),
    ]);
    const responseBody = await response.json();
    expect(responseBody.data.public_until).toBeTruthy();
});

test('test that editing a report to make it public with expiration date works', async ({
    page,
    ctx,
}) => {
    const projectName = 'EditDateProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'EditDateReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Open the save report modal and create a private report
    await page.getByRole('button', { name: 'Save Report' }).click();
    await page.getByLabel('Name').fill(reportName);

    // Uncheck "Public" to create a private report
    await page.getByLabel('Public').click();

    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports') &&
                response.request().method() === 'POST' &&
                response.status() === 201
        ),
        page.getByRole('dialog').getByRole('button', { name: 'Create Report' }).click(),
    ]);

    // Go to shared reports and edit
    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();
    await expect(page.getByText('Private')).toBeVisible();

    // Click more options and edit
    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();

    // Check "Public" to make it public - this should show the date picker
    await page.getByLabel('Public').click();

    // The date picker should now be visible
    const datePicker = page
        .getByRole('dialog')
        .getByRole('button', { name: DATE_PICKER_BUTTON_PATTERN });
    await expect(datePicker).toBeVisible();
    await datePicker.click();

    // Select a date in the next month
    const calendarGrid = page.getByRole('gridcell').first();
    await expect(calendarGrid).toBeVisible({ timeout: 5000 });
    await page.getByRole('button', { name: /Next/i }).click();
    await page.getByRole('gridcell').filter({ hasText: /^20$/ }).first().click();

    // Wait for the calendar to close
    await expect(calendarGrid).not.toBeVisible();

    // Update the report and verify it includes the public_until date
    const [response] = await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        page.getByRole('button', { name: 'Update Report' }).click(),
    ]);
    const responseBody = await response.json();
    expect(responseBody.data.public_until).toBeTruthy();
    expect(responseBody.data.is_public).toBe(true);
});

test('test that shared report with No Client filter shows entries without a client', async ({
    page,
    ctx,
}) => {
    const clientName = 'NoClientCli ' + Math.floor(Math.random() * 10000);
    const projectName = 'NoClientProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'NoClientReport ' + Math.floor(Math.random() * 10000);

    const client = await createClientViaApi(ctx, { name: clientName });
    const project = await createProjectViaApi(ctx, { name: projectName, client_id: client.id });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });
    await createBareTimeEntryViaApi(ctx, 'Entry without client', '2h');

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Filter by "No Client"
    await page.getByRole('button', { name: 'Clients' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: 'No Client' }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
    await expect(page.getByText(projectName)).not.toBeVisible();
});

test('test that shared report with No Tag filter shows entries without tags', async ({
    page,
    ctx,
}) => {
    const tagName = 'NoTagFilter ' + Math.floor(Math.random() * 10000);
    const reportName = 'NoTagReport ' + Math.floor(Math.random() * 10000);

    await createTimeEntryWithTagViaApi(ctx, tagName, '1h');
    await createBareTimeEntryViaApi(ctx, 'Entry without tags', '2h');

    await goToReporting(page);
    await expect(page.getByText('Total')).toBeVisible();

    // Filter by "No Tag"
    await page.getByRole('button', { name: 'Tags' }).first().click();
    await Promise.all([
        page.getByRole('option').filter({ hasText: 'No Tag' }).click(),
        waitForReportingUpdate(page),
    ]);
    await page.keyboard.press('Escape');

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // View the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText('Total')).toBeVisible();
});

test('test that creating a report with empty name shows validation error', async ({
    page,
    ctx,
}) => {
    const projectName = 'EmptyNameProj ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Open the save report modal
    await page.getByRole('button', { name: 'Save Report' }).click();

    // Leave name empty and try to create
    await page.getByRole('dialog').getByRole('button', { name: 'Create Report' }).click();

    // Should show validation error
    await expect(page.getByText('The name field is required')).toBeVisible();
});

test('test that updating report name works', async ({ page, ctx }) => {
    const projectName = 'UpdateNameProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'OriginalName ' + Math.floor(Math.random() * 10000);
    const newReportName = 'UpdatedName ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    await saveAsSharedReport(page, reportName);

    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();

    // Click more options and edit
    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();

    // Update the name
    await page.getByLabel('Name', { exact: true }).fill(newReportName);

    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        page.getByRole('button', { name: 'Update Report' }).click(),
    ]);

    // Verify the name was updated in the table
    await expect(page.getByText(newReportName)).toBeVisible();
    await expect(page.getByText(reportName)).not.toBeVisible();
});

test('test that updating expiration date on already-public report works', async ({ page, ctx }) => {
    const projectName = 'UpdateExpDateProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'UpdateExpDateReport ' + Math.floor(Math.random() * 10000);

    const project = await createProjectViaApi(ctx, { name: projectName });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    // Create a public report (already public by default)
    await saveAsSharedReport(page, reportName);

    // Go to shared reports and edit
    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();

    // Click more options and edit
    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();

    // The date picker should be visible (report is already public)
    const datePicker = page
        .getByRole('dialog')
        .getByRole('button', { name: DATE_PICKER_BUTTON_PATTERN });
    await expect(datePicker).toBeVisible();
    await datePicker.click();

    // Select the 25th of next month
    const calendarGrid = page.getByRole('gridcell').first();
    await expect(calendarGrid).toBeVisible({ timeout: 5000 });
    await page.getByRole('button', { name: /Next/i }).click();
    await page.getByRole('gridcell').filter({ hasText: /^25$/ }).first().click();

    // Wait for the calendar to close
    await expect(calendarGrid).not.toBeVisible();

    // Update the report and verify it includes the correct public_until date
    const [response] = await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        page.getByRole('button', { name: 'Update Report' }).click(),
    ]);
    const responseBody = await response.json();
    expect(responseBody.data.public_until).toBeTruthy();

    // Verify the date is the 25th of a future month
    const returnedDate = new Date(responseBody.data.public_until);
    expect(returnedDate.getUTCDate()).toBe(25);

    // The returned date should be in the future
    const now = new Date();
    expect(returnedDate.getTime()).toBeGreaterThan(now.getTime());
});

test('test that clearing the expiration date on a report works', async ({ page, ctx }) => {
    const reportName = 'ClearExpReport ' + Math.floor(Math.random() * 10000);

    // Create a public report with an expiration date via API
    await createReportViaApi(ctx, {
        name: reportName,
        is_public: true,
        public_until: dayjs().add(1, 'month').utc().format('YYYY-MM-DDTHH:mm:ss[Z]'),
    });

    // Go to shared reports and edit the report
    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();

    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();
    await expect(page.getByRole('dialog')).toBeVisible();

    // The date picker should show a date (not "Pick a date")
    await expect(
        page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
    ).not.toBeVisible();

    // Click the clear button (X icon) to remove the expiration date
    const clearButton = page
        .getByRole('dialog')
        .locator('[role="button"]')
        .filter({ has: page.locator('svg.lucide-x') });
    await expect(clearButton).toBeVisible();
    await clearButton.click();

    // The date picker should now show "Pick a date"
    await expect(
        page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
    ).toBeVisible();

    // The clear button should no longer be visible
    await expect(clearButton).not.toBeVisible();

    // Update the report and verify public_until is null
    const [updateResponse] = await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/reports/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
        page.getByRole('button', { name: 'Update Report' }).click(),
    ]);
    const updateBody = await updateResponse.json();
    expect(updateBody.data.public_until).toBeNull();
});

test('test that date picker clear button is not visible when no date is set', async ({
    page,
    ctx,
}) => {
    const reportName = 'NoClearReport ' + Math.floor(Math.random() * 10000);

    // Create a public report without an expiration date via API
    await createReportViaApi(ctx, {
        name: reportName,
        is_public: true,
        public_until: null,
    });

    // Go to shared reports and edit the report
    await goToReportingShared(page);
    await expect(page.getByText(reportName)).toBeVisible();

    await page
        .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
        .click();
    await page.getByRole('menuitem', { name: /^Edit Report/ }).click();
    await expect(page.getByRole('dialog')).toBeVisible();

    // The date picker should show "Pick a date"
    await expect(
        page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
    ).toBeVisible();

    // The clear button should NOT be visible
    const clearButton = page
        .getByRole('dialog')
        .locator('[role="button"]')
        .filter({ has: page.locator('svg.lucide-x') });
    await expect(clearButton).not.toBeVisible();
});

// ──────────────────────────────────────────────────
// Shared Report Cost Column Tests
// ──────────────────────────────────────────────────

test('test that shared report displays cost column correctly aligned with data rows', async ({
    page,
    ctx,
}) => {
    const projectName = 'BillableProj ' + Math.floor(Math.random() * 10000);
    const reportName = 'BillableReport ' + Math.floor(Math.random() * 10000);

    const project = await createBillableProjectViaApi(ctx, {
        name: projectName,
        billable_rate: 10000, // 100.00 per hour
    });
    await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration: '1h',
        projectId: project.id,
        billable: true,
    });

    await goToReporting(page);
    await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();

    const { shareableLink } = await saveAsSharedReport(page, reportName);

    // Navigate to the shared report
    await page.goto(shareableLink);
    await expect(page.getByText('Reporting')).toBeVisible();
    await expect(page.getByText(projectName)).toBeVisible();

    // Verify the table header has all three columns
    await expect(page.getByText('Name', { exact: true })).toBeVisible();
    await expect(page.getByText('Duration', { exact: true })).toBeVisible();
    await expect(page.getByText('Cost', { exact: true })).toBeVisible();

    // Verify the Total row displays both duration and cost
    await expect(page.getByText('Total')).toBeVisible();

    // The data rows should render cost values (not just header + duration)
    // With 1h at 100/h the cost should be displayed somewhere in the table
    // If showCost is not passed to ReportingRow, only the header "Cost" and
    // the Total row cost will render, but individual row costs will be missing
    const table = page.locator('[style*="grid-template-columns"]');
    // Count elements containing the cost value - header "Cost" + project row cost + total row cost = 3
    // If broken (showCost not passed), the project row won't render its cost cell
    await expect(table.getByText(/100/).first()).toBeVisible();

    // Verify the cost value appears at least twice in the table
    // (once for the data row, once for the total) beyond just the header
    const costValues = table.getByText(/100/);
    await expect(costValues).toHaveCount(2);
});
