// TODO: Edit Billable Rate
// TODO: Resend Email Invitation
// TODO: Remove Invitation
import { expect, test } from '../playwright/fixtures';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import type { Page } from '@playwright/test';
import { inviteAndAcceptMember } from './utils/members';
import {
    createPlaceholderMemberViaImportApi,
    getMembersViaApi,
    updateMemberBillableRateViaApi,
    updateOrganizationSettingViaApi,
} from './utils/api';
import { getTableRowNames } from './utils/table';

// Tests that invite + accept members need more time
test.describe.configure({ timeout: 45000 });

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

async function openInviteMemberModal(page: Page) {
    await Promise.all([
        page.getByRole('button', { name: 'Invite Member' }).click(),
        expect(page.getByPlaceholder('Member Email')).toBeVisible(),
    ]);
}

test('test that new manager can be invited and accepted', async ({ page, browser }) => {
    const memberId = Math.round(Math.random() * 100000);
    const memberEmail = `manager+${memberId}@invite.test`;

    await inviteAndAcceptMember(page, browser, 'Invited Mgr', memberEmail, 'Manager');

    // Verify the member appears in the members table with the correct role
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'Invited Mgr' });
    await expect(memberRow).toBeVisible();
    await expect(memberRow.getByText('Manager', { exact: true })).toBeVisible();
});

test('test that new employee can be invited and accepted', async ({ page, browser }) => {
    const memberId = Math.round(Math.random() * 100000);
    const memberEmail = `employee+${memberId}@invite.test`;

    await inviteAndAcceptMember(page, browser, 'Invited Emp', memberEmail, 'Employee');

    // Verify the member appears in the members table with the correct role
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'Invited Emp' });
    await expect(memberRow).toBeVisible();
    await expect(memberRow.getByText('Employee', { exact: true })).toBeVisible();
});

test('test that new admin can be invited and accepted', async ({ page, browser }) => {
    const memberId = Math.round(Math.random() * 100000);
    const memberEmail = `admin+${memberId}@invite.test`;

    await inviteAndAcceptMember(page, browser, 'Invited Adm', memberEmail, 'Administrator');

    // Verify the member appears in the members table with the correct role
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'Invited Adm' });
    await expect(memberRow).toBeVisible();
    await expect(memberRow.getByText('Admin', { exact: true })).toBeVisible();
});

test('test that error shows if no role is selected', async ({ page }) => {
    await goToMembersPage(page);
    await openInviteMemberModal(page);
    const noRoleId = Math.round(Math.random() * 10000);

    await page.getByLabel('Email').fill(`new+${noRoleId}@norole.test`);
    await Promise.all([
        page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
        expect(page.getByText('Please select a role')).toBeVisible(),
    ]);
});

test('test that organization billable rate can be updated with all existing time entries', async ({
    page,
}) => {
    await goToMembersPage(page);
    const newBillableRate = Math.round(Math.random() * 10000);
    await page.getByRole('row').first().getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await page.getByRole('combobox').last().click();
    await page.getByRole('option', { name: 'Custom Rate' }).click();
    await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString());
    await page.getByRole('button', { name: 'Update Member' }).click();

    await Promise.all([
        page.getByRole('button', { name: 'Yes, update existing time' }).click(),
        page.waitForRequest(
            async (request) =>
                request.url().includes('/members/') &&
                request.method() === 'PUT' &&
                request.postDataJSON().billable_rate === newBillableRate * 100
        ),
        page.waitForResponse(
            async (response) =>
                response.url().includes('/organizations/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200 &&
                (await response.json()).data.billable_rate === newBillableRate * 100
        ),
    ]);
});

test('test that switching member billable rate from custom back to default rate works', async ({
    page,
    ctx,
}) => {
    // Set a known org billable rate
    await updateOrganizationSettingViaApi(ctx, { billable_rate: 12000 });

    // Create a placeholder member with a custom billable rate
    await createPlaceholderMemberViaImportApi(ctx, 'CustomToDefault Member');
    const members = await getMembersViaApi(ctx);
    const member = members.find((m) => m.name === 'CustomToDefault Member');
    expect(member).toBeDefined();
    await updateMemberBillableRateViaApi(ctx, member!.id, 25000);

    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'CustomToDefault Member' });
    await expect(memberRow).toBeVisible();

    // Open edit modal
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Verify it starts on Custom Rate
    const billableCombobox = page.getByRole('dialog').getByRole('combobox').last();
    await expect(billableCombobox).toContainText('Custom Rate');

    // Switch to Default Rate
    await billableCombobox.click();
    await page.getByRole('option', { name: 'Default Rate' }).click();
    await expect(billableCombobox).toContainText('Default Rate');

    // Verify the billable rate input is disabled
    await expect(page.getByPlaceholder('Billable Rate')).toBeDisabled();

    // Submit — billable_rate changes from 25000 to null, so confirmation dialog appears
    await page.getByRole('button', { name: 'Update Member' }).click();
    await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible();
    await expect(page.getByText('the default rate of the organization')).toBeVisible();

    // Confirm the update
    await Promise.all([
        page.getByRole('button', { name: 'Yes, update existing time' }).click(),
        page.waitForRequest(
            (request) =>
                request.url().includes('/members/') &&
                request.method() === 'PUT' &&
                request.postDataJSON().billable_rate === null
        ),
    ]);

    // Verify both dialogs are closed
    await expect(page.getByRole('dialog')).not.toBeVisible();
});

test('test that default rate shows disabled input with organization billable rate', async ({
    page,
    ctx,
}) => {
    // Set a known org billable rate (150.00)
    await updateOrganizationSettingViaApi(ctx, { billable_rate: 15000 });

    await goToMembersPage(page);

    // Open edit modal for the owner (who uses default rate by default)
    await page.getByRole('row').first().getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Verify it's on Default Rate
    const billableCombobox = page.getByRole('dialog').getByRole('combobox').last();
    await expect(billableCombobox).toContainText('Default Rate');

    // Verify the input is disabled and shows the org rate (formatted with currency)
    const billableInput = page.getByPlaceholder('Billable Rate');
    await expect(billableInput).toBeDisabled();
    await expect(billableInput).toHaveAttribute('aria-valuenow', '150');

    // Close the dialog
    await page.getByRole('button', { name: 'Cancel' }).click();
    await expect(page.getByRole('dialog')).not.toBeVisible();
});

test('test that cancelling the billable rate confirmation dialog does not update the member', async ({
    page,
    ctx,
}) => {
    // Create a placeholder member with a custom billable rate
    await createPlaceholderMemberViaImportApi(ctx, 'CancelConfirm Member');
    const members = await getMembersViaApi(ctx);
    const member = members.find((m) => m.name === 'CancelConfirm Member');
    expect(member).toBeDefined();
    await updateMemberBillableRateViaApi(ctx, member!.id, 10000);

    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'CancelConfirm Member' });
    await expect(memberRow).toBeVisible();

    // Open edit modal
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Change the billable rate
    await page.getByPlaceholder('Billable Rate').fill('200');

    // Click Update Member — confirmation dialog should appear
    await page.getByRole('button', { name: 'Update Member' }).click();
    await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible();

    // Set up listener to verify no PUT request is sent after cancel
    let putRequestSent = false;
    page.on('request', (request) => {
        if (request.url().includes('/members/') && request.method() === 'PUT') {
            putRequestSent = true;
        }
    });

    // Click Cancel on the confirmation dialog
    await page.getByRole('button', { name: 'Cancel' }).click();

    // Verify confirmation dialog is closed
    await expect(
        page.getByRole('heading', { name: 'Update Member Billable Rate' })
    ).not.toBeVisible();

    // Verify no API call was made
    expect(putRequestSent).toBe(false);
});

test('test that changing role of placeholder member is rejected', async ({ page, ctx }) => {
    const placeholderName = 'RoleChange ' + Math.floor(Math.random() * 10000);

    // Create a placeholder member via import
    await createPlaceholderMemberViaImportApi(ctx, placeholderName);

    // Go to members page and verify placeholder exists with role "Placeholder"
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: placeholderName });
    await expect(memberRow).toBeVisible();
    await expect(memberRow.getByText('Placeholder', { exact: true })).toBeVisible();

    // Open the edit modal for the placeholder member
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Change role to Employee
    const roleSelect = page.getByRole('dialog').getByRole('combobox').first();
    await roleSelect.click();
    await expect(page.getByRole('option', { name: 'Employee' })).toBeVisible();
    await page.getByRole('option', { name: 'Employee' }).click();
    await expect(roleSelect).toContainText('Employee');

    // Submit the change - the API should reject it with 400
    await Promise.all([
        page.getByRole('button', { name: 'Update Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/members/') &&
                response.request().method() === 'PUT' &&
                response.status() === 400
        ),
    ]);

    // Verify error notification is shown
    await expect(page.getByText('Failed to update member')).toBeVisible();
});

test('test that changing member role updates the role in the member table', async ({
    page,
    browser,
}) => {
    const memberId = Math.floor(Math.random() * 100000);
    const memberEmail = `member+${memberId}@rolechange.test`;

    // Invite and accept a new Employee member
    await inviteAndAcceptMember(page, browser, 'Jane Smith', memberEmail, 'Employee');

    // Verify the new member appears with the Employee role
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: 'Jane Smith' });
    await expect(memberRow).toBeVisible();
    await expect(memberRow.getByText('Employee', { exact: true })).toBeVisible();

    // Open the edit modal
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Edit').click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Change role to Manager
    const roleSelect = page.getByRole('dialog').getByRole('combobox').first();
    await roleSelect.click();
    await expect(page.getByRole('option', { name: 'Manager' })).toBeVisible();
    await page.getByRole('option', { name: 'Manager' }).click();
    await expect(roleSelect).toContainText('Manager');

    // Submit the change and verify the API call succeeds
    await Promise.all([
        page.getByRole('button', { name: 'Update Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/members/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
    ]);

    // Verify dialog closed
    await expect(page.getByRole('dialog')).not.toBeVisible();

    // Verify the role updated in the table
    await expect(memberRow.getByText('Manager', { exact: true })).toBeVisible();
});

test('test that merging a placeholder member works', async ({ page, ctx }) => {
    const placeholderName = 'Merge Target ' + Math.floor(Math.random() * 10000);

    // Create a placeholder member via import
    await createPlaceholderMemberViaImportApi(ctx, placeholderName);

    // Go to members page
    await goToMembersPage(page);
    await expect(page.getByText(placeholderName)).toBeVisible();

    // Find the placeholder member row and open actions menu
    const placeholderRow = page.getByRole('row').filter({ hasText: placeholderName });
    await placeholderRow.getByRole('button').click();

    // Click Merge
    await page.getByTestId('member_merge').click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Merge Member' })).toBeVisible();

    // Select the current user (the owner) as merge target via MemberCombobox
    // The MemberCombobox renders a Button as trigger; clicking it opens the popover with the combobox input
    await page.getByRole('dialog').getByRole('button', { name: 'Select a member...' }).click();

    // Wait for dropdown options to load
    const firstOption = page.getByRole('option').first();
    await expect(firstOption).toBeVisible({ timeout: 10000 });
    await firstOption.click();

    // Submit merge
    await Promise.all([
        page.getByRole('button', { name: 'Merge Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/member/') &&
                response.url().includes('/merge-into') &&
                response.ok()
        ),
    ]);

    // Wait for merge dialog to close after successful merge
    await expect(page.getByRole('dialog').filter({ hasText: 'Merge Member' })).not.toBeVisible();

    // Verify placeholder member is no longer in the members table
    await expect(page.getByRole('main').getByText(placeholderName)).not.toBeVisible();
});

test('test that deleting a placeholder member works', async ({ page, ctx }) => {
    const placeholderName = 'Delete Target ' + Math.floor(Math.random() * 10000);

    // Create a placeholder member via import
    await createPlaceholderMemberViaImportApi(ctx, placeholderName);

    // Go to members page
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: placeholderName });
    await expect(memberRow).toBeVisible();

    // Open actions menu and click Delete
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Delete').click();

    // Verify delete modal is shown
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Delete Member' })).toBeVisible();

    // Try to delete without checking the confirmation checkbox
    await page.getByRole('button', { name: 'Delete Member' }).click();

    // Should show validation error
    await expect(
        page.getByText('You must confirm that you understand the consequences of this action')
    ).toBeVisible();

    // Check the confirmation checkbox
    await page.getByRole('checkbox').click();

    // Click Delete Member button and wait for API response
    await Promise.all([
        page.getByRole('button', { name: 'Delete Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/members/') &&
                response.request().method() === 'DELETE' &&
                response.ok()
        ),
    ]);

    // Verify modal is closed
    await expect(page.getByRole('dialog')).not.toBeVisible();

    // Verify member is removed from the table
    await expect(page.getByRole('main').getByText(placeholderName)).not.toBeVisible();
});

test('test that member delete modal can be cancelled', async ({ page, ctx }) => {
    const placeholderName = 'Delete Cancel ' + Math.floor(Math.random() * 10000);

    // Create a placeholder member via import
    await createPlaceholderMemberViaImportApi(ctx, placeholderName);

    // Go to members page
    await goToMembersPage(page);
    const memberRow = page.getByRole('row').filter({ hasText: placeholderName });
    await expect(memberRow).toBeVisible();

    // Open actions menu and click Delete
    await memberRow.getByRole('button').click();
    await page.getByRole('menuitem').getByText('Delete').click();

    // Verify delete modal is shown
    await expect(page.getByRole('dialog')).toBeVisible();

    // Set up listener to verify no DELETE request is sent
    let deleteRequestSent = false;
    page.on('request', (request) => {
        if (request.url().includes('/members/') && request.method() === 'DELETE') {
            deleteRequestSent = true;
        }
    });

    // Click Cancel
    await page.getByRole('button', { name: 'Cancel' }).click();

    // Verify modal is closed
    await expect(page.getByRole('dialog')).not.toBeVisible();

    // Verify member is still in the table
    await expect(memberRow).toBeVisible();

    // Verify no DELETE request was sent
    expect(deleteRequestSent).toBe(false);
});

test('test that organization owner cannot be deleted', async ({ page }) => {
    await goToMembersPage(page);

    // Find the owner row (John Doe with Owner role)
    const ownerRow = page.getByRole('row').filter({ hasText: 'Owner' });
    await expect(ownerRow).toBeVisible();

    // Open the actions menu for the owner
    await ownerRow.getByRole('button').click();

    // Click Delete
    await page.getByRole('menuitem').getByText('Delete').click();

    // Verify delete modal is shown
    await expect(page.getByRole('dialog')).toBeVisible();

    // Check the confirmation checkbox
    await page.getByRole('checkbox').click();

    // Try to delete - should fail with 400 error
    const responsePromise = page.waitForResponse(
        (response) =>
            response.url().includes('/members/') && response.request().method() === 'DELETE'
    );
    await page.getByRole('button', { name: 'Delete Member' }).click();
    const response = await responsePromise;

    // Verify the API returned an error status
    expect(response.status()).toBe(400);

    // Close the modal by pressing Escape
    await page.keyboard.press('Escape');

    // Refresh and verify the owner is still there
    await goToMembersPage(page);
    await expect(page.getByRole('row').filter({ hasText: 'Owner' })).toBeVisible();
});

// =============================================
// Context Menu Tests
// =============================================

test('test that member context menu edit updates the member billable rate', async ({
    page,
    ctx,
}) => {
    const memberName = 'CtxEditMember ' + Math.floor(1 + Math.random() * 10000);
    await createPlaceholderMemberViaImportApi(ctx, memberName);
    await goToMembersPage(page);

    const row = page.getByRole('row').filter({ hasText: memberName }).first();
    await expect(row).toBeVisible();
    await row.click({ button: 'right' });
    await expect(page.getByRole('menu')).toBeVisible();
    await page.getByRole('menuitem', { name: 'Edit' }).click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();

    // Change billable rate from default to custom
    const billableRateSelect = page.getByRole('dialog').getByRole('combobox').last();
    await billableRateSelect.click();
    await page.getByRole('option', { name: 'Custom Rate' }).click();

    // Set a custom billable rate
    await page.getByPlaceholder('Billable Rate').fill('150');

    // Click Update Member — confirmation dialog should appear
    await page.getByRole('button', { name: 'Update Member' }).click();
    await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible();

    // Confirm the billable rate change
    await Promise.all([
        page.getByRole('button', { name: 'Yes, update existing time entries' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/members/') &&
                response.request().method() === 'PUT' &&
                response.status() === 200
        ),
    ]);

    // Verify dialog closed
    await expect(page.getByRole('dialog')).not.toBeVisible();
});

test('test that member context menu merge merges the member', async ({ page, ctx }) => {
    const memberName = 'CtxMergeMember ' + Math.floor(1 + Math.random() * 10000);
    await createPlaceholderMemberViaImportApi(ctx, memberName);
    await goToMembersPage(page);

    const row = page.getByRole('row').filter({ hasText: memberName }).first();
    await expect(row).toBeVisible();
    await row.click({ button: 'right' });
    await expect(page.getByRole('menu')).toBeVisible();
    await page.getByRole('menuitem', { name: 'Merge' }).click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Merge Member' })).toBeVisible();

    // Select the first available member as merge target
    await page.getByRole('dialog').getByRole('button', { name: 'Select a member...' }).click();
    const firstOption = page.getByRole('option').first();
    await expect(firstOption).toBeVisible({ timeout: 10000 });
    await firstOption.click();

    // Submit merge
    await Promise.all([
        page.getByRole('button', { name: 'Merge Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/member/') &&
                response.url().includes('/merge-into') &&
                response.ok()
        ),
    ]);

    // Verify placeholder member is no longer visible
    await expect(page.getByRole('dialog').filter({ hasText: 'Merge Member' })).not.toBeVisible();
    await expect(page.getByRole('main').getByText(memberName)).not.toBeVisible();
});

test('test that member context menu deactivate deactivates the member', async ({
    page,
    browser,
}) => {
    const memberId = Math.floor(Math.random() * 100000);
    const memberEmail = `member+${memberId}@deactivate.test`;
    const memberName = 'Deactivate Target';

    // Invite and accept a new Employee member
    await inviteAndAcceptMember(page, browser, memberName, memberEmail, 'Employee');

    await goToMembersPage(page);
    const row = page.getByRole('row').filter({ hasText: memberName }).first();
    await expect(row).toBeVisible();

    // Open context menu and click Deactivate
    await row.click({ button: 'right' });
    await expect(page.getByRole('menu')).toBeVisible();
    await page.getByRole('menuitem', { name: 'Deactivate' }).click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Deactivate User' })).toBeVisible();

    // Confirm deactivation
    await Promise.all([
        page.getByRole('button', { name: 'Deactivate' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/make-placeholder') &&
                response.request().method() === 'POST' &&
                response.ok()
        ),
    ]);

    // Verify dialog closed and member role changed to Placeholder
    await expect(page.getByRole('dialog')).not.toBeVisible();
    await expect(row.getByText('Placeholder', { exact: true })).toBeVisible();
});

test('test that member context menu delete deletes the member', async ({ page, ctx }) => {
    const memberName = 'CtxDeleteMember ' + Math.floor(1 + Math.random() * 10000);
    await createPlaceholderMemberViaImportApi(ctx, memberName);
    await goToMembersPage(page);

    const row = page.getByRole('row').filter({ hasText: memberName }).first();
    await expect(row).toBeVisible();
    await row.click({ button: 'right' });
    await expect(page.getByRole('menu')).toBeVisible();
    await page.getByRole('menuitem', { name: 'Delete' }).click();
    await expect(page.getByRole('dialog')).toBeVisible();
    await expect(page.getByRole('heading', { name: 'Delete Member' })).toBeVisible();

    // Check the confirmation checkbox
    await page.getByRole('checkbox').click();

    // Click Delete Member button and wait for API response
    await Promise.all([
        page.getByRole('button', { name: 'Delete Member' }).click(),
        page.waitForResponse(
            (response) =>
                response.url().includes('/members/') &&
                response.request().method() === 'DELETE' &&
                response.ok()
        ),
    ]);

    // Verify modal closed and member removed from table
    await expect(page.getByRole('dialog')).not.toBeVisible();
    await expect(page.getByRole('main').getByText(memberName)).not.toBeVisible();
});

// =============================================
// Invitations Tab Tests
// =============================================

test('test that invitation shows in invitations tab and can be revoked', async ({ page }) => {
    const inviteEmail = `invite+${Math.floor(Math.random() * 100000)}@pending.test`;

    await goToMembersPage(page);
    await openInviteMemberModal(page);

    await page.getByPlaceholder('Member Email').fill(inviteEmail);
    await page.getByRole('button', { name: 'Employee' }).click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/invitations') &&
                response.request().method() === 'POST' &&
                response.status() === 204
        ),
        page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
    ]);

    // Wait for modal to close
    await expect(page.getByPlaceholder('Member Email')).not.toBeVisible();

    // Switch to Invitations tab and verify the invitation is visible
    await page.getByText('Invitations', { exact: true }).click();
    await expect(page.getByText(inviteEmail)).toBeVisible();

    // Find and click the actions menu for this invitation
    const invitationRow = page.locator('tr, [role="row"]').filter({ hasText: inviteEmail });
    await invitationRow.getByRole('button').click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/invitations/') &&
                response.request().method() === 'DELETE' &&
                response.status() === 204
        ),
        page.getByRole('menuitem').getByText('Delete').click(),
    ]);

    // Verify invitation is removed
    await expect(page.getByText(inviteEmail)).not.toBeVisible();
});

test('test that invitation can be resent', async ({ page }) => {
    const inviteEmail = `resend+${Math.floor(Math.random() * 100000)}@invite.test`;

    await goToMembersPage(page);
    await openInviteMemberModal(page);

    await page.getByPlaceholder('Member Email').fill(inviteEmail);
    await page.getByRole('button', { name: 'Employee' }).click();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/invitations') &&
                response.request().method() === 'POST' &&
                response.status() === 204
        ),
        page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
    ]);

    // Wait for modal to close
    await expect(page.getByPlaceholder('Member Email')).not.toBeVisible();

    // Switch to Invitations tab
    await page.getByText('Invitations', { exact: true }).click();
    await expect(page.getByText(inviteEmail)).toBeVisible();

    // Find and click the actions menu, then resend
    const invitationRow = page.locator('tr, [role="row"]').filter({ hasText: inviteEmail });
    await invitationRow.getByRole('button').click();
    // Wait for dropdown menu to appear
    await expect(page.getByRole('menuitem').getByText('Resend Invitation')).toBeVisible();
    await Promise.all([
        page.waitForResponse(
            (response) =>
                response.url().includes('/resend') && response.request().method() === 'POST'
        ),
        page.getByRole('menuitem').getByText('Resend Invitation').click(),
    ]);
});

test('test that admin user cannot transfer ownership', async ({ page, browser }) => {
    const memberId = Math.floor(Math.random() * 100000);
    const memberEmail = `admin+${memberId}@perms.test`;

    // Invite and accept an admin member
    await inviteAndAcceptMember(
        page,
        browser,
        'Admin User ' + memberId,
        memberEmail,
        'Administrator'
    );

    // Go to members page and verify the admin exists
    await goToMembersPage(page);
    const adminRow = page.getByRole('row').filter({ hasText: 'Admin User' });
    await expect(adminRow).toBeVisible();

    // The owner should still be the owner
    const ownerRow = page.getByRole('row').filter({ hasText: 'Owner' });
    await expect(ownerRow).toBeVisible();

    // Open actions menu for the admin - should NOT have "Transfer Ownership" option
    await adminRow.getByRole('button').click();
    await expect(page.getByRole('menuitem').getByText('Edit')).toBeVisible();
});

test('test that accepted invitation disappears from invitations tab', async ({ page, browser }) => {
    const memberId = Math.round(Math.random() * 100000);
    const memberEmail = `accepted+${memberId}@invite.test`;

    // Invite and accept the member
    await inviteAndAcceptMember(page, browser, 'Accepted Member', memberEmail, 'Employee');

    // Go to members page and switch to Invitations tab
    await goToMembersPage(page);
    await page.getByRole('tab', { name: 'Invitations' }).click();

    // The accepted invitation should not be visible
    await expect(page.getByText(memberEmail)).not.toBeVisible();
});

// =============================================
// Sorting Tests
// =============================================

// Helper to clear localStorage before tests that check sorting
async function clearMemberTableState(page: Page) {
    await page.evaluate(() => {
        localStorage.removeItem('member-table-state');
    });
}

test('test that sorting members by name, role, and status works', async ({ page, ctx }) => {
    // Create two placeholder members with names that sort predictably around "John Doe"
    await createPlaceholderMemberViaImportApi(ctx, 'AAA SortFirst');
    await createPlaceholderMemberViaImportApi(ctx, 'ZZZ SortLast');

    await goToMembersPage(page);
    await clearMemberTableState(page);
    await page.reload();

    const table = page.getByTestId('member_table');
    await expect(table).toBeVisible();

    // -- Name sorting (default is already name asc after clearing state) --
    const nameHeader = table.getByText('Name').first();
    let names = await getTableRowNames(table);
    expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('ZZZ SortLast'));

    await nameHeader.click(); // toggle to desc
    names = await getTableRowNames(table);
    expect(names.indexOf('ZZZ SortLast')).toBeLessThan(names.indexOf('AAA SortFirst'));

    // -- Role sorting --
    const roleHeader = table.getByText('Role').first();
    await roleHeader.click(); // asc: Owner(0) < Placeholder(4)
    names = await getTableRowNames(table);
    const ownerIdx = names.indexOf('John Doe');
    const placeholderIdx = names.indexOf('AAA SortFirst');
    expect(ownerIdx).toBeLessThan(placeholderIdx);

    await roleHeader.click(); // desc: Placeholder first
    names = await getTableRowNames(table);
    expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('John Doe'));

    // -- Status sorting --
    const statusHeader = table.getByText('Status').first();
    await statusHeader.click(); // asc: Active(0) < Inactive(1)
    names = await getTableRowNames(table);
    expect(names.indexOf('John Doe')).toBeLessThan(names.indexOf('AAA SortFirst'));

    await statusHeader.click(); // desc: Inactive first
    names = await getTableRowNames(table);
    expect(names.indexOf('AAA SortFirst')).toBeLessThan(names.indexOf('John Doe'));

    // -- Email: just verify sort indicator appears --
    const emailHeader = table.getByText('Email').first();
    await emailHeader.click();
    await expect(emailHeader.locator('svg')).toBeVisible();
});

test('test that member sort state persists after page reload', async ({ page }) => {
    await goToMembersPage(page);
    await clearMemberTableState(page);
    await page.reload();

    const table = page.getByTestId('member_table');
    await expect(table).toBeVisible();

    // Click Role header twice to set descending sort
    const roleHeader = table.getByText('Role').first();
    await roleHeader.click();
    await expect(roleHeader.locator('svg')).toBeVisible();
    await roleHeader.click();
    await expect(roleHeader.locator('svg')).toBeVisible();

    // Reload the page
    await page.reload();

    // Verify the sort indicator is still visible on Role column
    await expect(page.getByTestId('member_table')).toBeVisible();
    await expect(
        page.getByTestId('member_table').getByText('Role').first().locator('svg')
    ).toBeVisible();
});

test('test that sorting members by billable rate works', async ({ page, ctx }) => {
    // Create two placeholder members and set different billable rates
    await createPlaceholderMemberViaImportApi(ctx, 'HighRate Member');
    await createPlaceholderMemberViaImportApi(ctx, 'LowRate Member');

    const members = await getMembersViaApi(ctx);
    const highRateMember = members.find((m) => m.name === 'HighRate Member');
    const lowRateMember = members.find((m) => m.name === 'LowRate Member');
    expect(highRateMember).toBeDefined();
    expect(lowRateMember).toBeDefined();

    await updateMemberBillableRateViaApi(ctx, highRateMember!.id, 20000);
    await updateMemberBillableRateViaApi(ctx, lowRateMember!.id, 5000);

    await goToMembersPage(page);
    await clearMemberTableState(page);
    await page.reload();

    const table = page.getByTestId('member_table');
    await expect(table).toBeVisible();

    // First click = desc (highest first), null rates last
    const billableHeader = table.getByText('Billable Rate').first();
    await billableHeader.click();
    await expect(billableHeader.locator('svg')).toBeVisible();
    let names = await getTableRowNames(table);
    expect(names.indexOf('HighRate Member')).toBeLessThan(names.indexOf('LowRate Member'));

    // Second click = asc (lowest first), null rates still last
    await billableHeader.click();
    names = await getTableRowNames(table);
    expect(names.indexOf('LowRate Member')).toBeLessThan(names.indexOf('HighRate Member'));
});

// =============================================
// Employee Permission Tests
// =============================================

test.describe('Employee Sidebar Navigation', () => {
    test('employee sidebar shows correct navigation links', async ({ employee }) => {
        await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
        await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
            timeout: 10000,
        });

        // Visible links
        await expect(employee.page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
        await expect(employee.page.getByRole('link', { name: 'Time', exact: true })).toBeVisible();
        await expect(employee.page.getByRole('link', { name: 'Calendar' })).toBeVisible();
        await expect(employee.page.getByRole('link', { name: 'Projects' })).toBeVisible();
        await expect(employee.page.getByRole('link', { name: 'Clients' })).toBeVisible();
        await expect(employee.page.getByRole('link', { name: 'Tags' })).toBeVisible();

        // Hidden links
        await expect(employee.page.getByRole('link', { name: 'Members' })).not.toBeVisible();
        await expect(
            employee.page.getByRole('link', { name: 'Settings', exact: true })
        ).not.toBeVisible();
    });

    test('employee cannot see members list or invite members', async ({ employee }) => {
        await employee.page.goto(PLAYWRIGHT_BASE_URL + '/members');

        // Page loads but the members API returns 403 (no members:view permission)
        await expect(employee.page.getByRole('heading', { name: 'Members' })).toBeVisible({
            timeout: 10000,
        });

        // Member table is empty — no rows rendered (only headers)
        await expect(employee.page.getByTestId('member_table').locator('[role="row"]')).toHaveCount(
            0
        );

        // Employee should NOT see the Invite Member button
        await expect(
            employee.page.getByRole('button', { name: 'Invite member' })
        ).not.toBeVisible();
    });
});
