import { test, expect, UI_SETTLE_TIMEOUT, type Page } from '../../fixtures.js';
import {
  DIALOG,
  unique,
  openCreateDialog,
  saveDialog,
  searchAndFind,
  openRowEditor,
  deleteViaDialog,
  expectGoneFromSearch,
  seedTeamWithMember,
} from '../../helpers/settings.js';

/**
 * Create a team via the UI dialog. Hard-asserts the dialog closes:
 * a rejected mutation fails the test instead of being skipped over.
 */
async function createTeamViaUI(page: Page, name: string, short: string): Promise<void> {
  const dialog = await openCreateDialog(page);
  const inputs = dialog.locator('input');
  await inputs.first().fill(name);
  await inputs.nth(1).fill(short);
  await saveDialog(dialog);
}

test.describe('Settings: Teams', () => {
  test('teams page loads and shows team list', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/teams');
    await expect(adminPage).toHaveURL(/\/settings\/teams/);
    await expect(adminPage.locator('masterev-infinite-data-table').first()).toBeVisible({ timeout: 10_000 });
  });

  test('create a new team', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Create ${id}`;
    await adminPage.goto('/admin/settings/teams');

    await createTeamViaUI(adminPage, teamName, `C${id.slice(0, 4)}`);

    // Verify via search; a row that never appears means the create failed.
    await searchAndFind(adminPage, teamName);
  });

  test('create and edit a team', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Edit ${id}`;
    const editedName = `E2E Edited ${id}`;
    await adminPage.goto('/admin/settings/teams');

    // Step 1: Create
    await createTeamViaUI(adminPage, teamName, `D${id.slice(0, 4)}`);

    // Step 2: Search for the created team and open its edit dialog
    // (row-scoped click + hydration assert live in openRowEditor).
    const dialog = await openRowEditor(adminPage, teamName);

    // Step 3: Modify the name
    await dialog.locator('input').first().fill(editedName);
    await saveDialog(dialog);

    // Step 4: Verify the updated name via search
    await searchAndFind(adminPage, editedName);
  });

  test('create and delete a team', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Delete ${id}`;
    await adminPage.goto('/admin/settings/teams');

    // Step 1: Create
    await createTeamViaUI(adminPage, teamName, `F${id.slice(0, 4)}`);

    // Step 2: Search and open the edit dialog (scoped to the matching
    // row so the search filter race doesn't open the wrong team).
    const dialog = await openRowEditor(adminPage, teamName);

    // Step 3+4: Delete and confirm. A freshly created team has no users,
    // so its delete button MUST be visible and enabled (deleteViaDialog
    // asserts both and requires the dialog to close).
    await deleteViaDialog(adminPage, dialog);

    // Step 5: Verify the team is gone from search results.
    await expectGoneFromSearch(adminPage, teamName);
  });

  test('create team with quota and verify', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Quota ${id}`;
    const shortName = `Q${id.slice(0, 4)}`;
    const quotaValue = '50';
    await adminPage.goto('/admin/settings/teams');

    // Step 1: Create team with quota
    const dialog = await openCreateDialog(adminPage);
    const inputs = dialog.locator('input');
    await inputs.first().fill(teamName);
    await inputs.nth(1).fill(shortName);

    // Fill the quota field (p-inputnumber contains a nested input)
    const quotaInput = dialog.locator('p-inputnumber input').first();
    await expect(quotaInput).toBeVisible({ timeout: 3_000 });
    await quotaInput.fill(quotaValue);

    await saveDialog(dialog);

    // Step 2: Re-open the team to verify quota was saved. openRowEditor
    // scopes the edit click to the matching row and waits for the
    // team-name input to hydrate before we read the quota input.
    const editDialog = await openRowEditor(adminPage, teamName);

    const savedQuotaInput = editDialog.locator('p-inputnumber input').first();
    await expect(savedQuotaInput).toBeVisible({ timeout: 3_000 });
    // PrimeNG InputNumber may append a suffix (e.g. " %" or translation key)
    await expect(savedQuotaInput).toHaveValue(new RegExp(`^${quotaValue}`));

    // Close the dialog
    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });
  });

  test('toggle isActive and verify persistence', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Active ${id}`;
    await adminPage.goto('/admin/settings/teams');

    // Step 1: Create team (isActive defaults to true)
    await createTeamViaUI(adminPage, teamName, `A${id.slice(0, 4)}`);

    // Step 2: Open the team and toggle isActive off (row-scoped click +
    // hydration assert confirm we opened the correct team's dialog).
    const dialog = await openRowEditor(adminPage, teamName);

    // PrimeNG v21 ToggleSwitch: the click handler is a HOST listener on <p-toggleswitch>.
    // Use Playwright's real click (not evaluate) on the host element to trigger Angular's handler.
    const toggle = dialog.getByRole('switch').first();
    await expect(toggle).toBeVisible({ timeout: 3_000 });
    const wasChecked = await toggle.isChecked();

    // PrimeNG v21 ToggleSwitch has a host click listener. We need a robust toggle strategy:
    // 1. Try Playwright's real click on the p-toggleswitch host element
    // 2. If that doesn't work, try dispatching a click event via evaluate
    // 3. Verify the form control actually updated by checking the switch state
    const toggleHost = dialog.locator('p-toggleswitch').first();
    await expect(toggleHost).toBeVisible({ timeout: 3_000 });
    // Wait for form bindings and ControlValueAccessor to fully initialize
    await adminPage.waitForTimeout(1_000);

    // Strategy 1: Playwright real click
    await toggleHost.click({ force: true });
    await adminPage.waitForTimeout(500);

    let toggleChanged = await expect(async () => {
      const isNowChecked = await toggle.isChecked();
      expect(isNowChecked).not.toBe(wasChecked);
    }).toPass({ timeout: 3_000 }).then(() => true).catch(() => false);

    if (!toggleChanged) {
      // Strategy 2: dispatch click event via evaluate on the host element
      await toggleHost.evaluate((el) => {
        el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, composed: true }));
      });
      await adminPage.waitForTimeout(500);

      toggleChanged = await expect(async () => {
        const isNowChecked = await toggle.isChecked();
        expect(isNowChecked).not.toBe(wasChecked);
      }).toPass({ timeout: 3_000 }).then(() => true).catch(() => false);
    }

    // Neither a real click nor a dispatched click flipping the switch means
    // the toggle is broken. That is a product bug, not a skip.
    expect(
      toggleChanged,
      'toggle click must flip the isActive switch (real click AND dispatched click both failed)'
    ).toBe(true);

    await saveDialog(dialog);

    // Step 3: Re-open and verify isActive state was toggled.
    // Reload the page to ensure fresh data from the backend.
    await adminPage.goto('/admin/settings/teams');
    const editDialog = await openRowEditor(adminPage, teamName);

    const savedToggle = editDialog.getByRole('switch').first();
    await expect(savedToggle).toBeVisible({ timeout: 3_000 });
    // The persisted state must be the opposite of the original. Poll with a
    // generous budget so slow Convex propagation settles instead of skipping.
    await expect
      .poll(() => savedToggle.isChecked(), {
        timeout: UI_SETTLE_TIMEOUT,
        message: 'persisted isActive state must be the opposite of the pre-toggle state',
      })
      .toBe(!wasChecked);

    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });
  });

  test('quota unit toggle switches between count and percentage', async ({ adminPage }) => {
    const id = unique();
    const teamName = `E2E Unit ${id}`;
    await adminPage.goto('/admin/settings/teams');

    // Step 1: Create team
    const dialog = await openCreateDialog(adminPage);
    const inputs = dialog.locator('input');
    await inputs.first().fill(teamName);
    await inputs.nth(1).fill(`U${id.slice(0, 4)}`);

    // The quota unit toggle button is next to the InputNumber
    const unitToggle = dialog.locator('p-inputgroup p-button').first();
    await expect(unitToggle).toBeVisible({ timeout: 3_000 });

    // Click toggle to switch unit — the suffix on the input should change
    const quotaInput = dialog.locator('p-inputnumber input').first();
    await quotaInput.fill('50');

    // Click the unit toggle
    await unitToggle.click();

    // Fill a value after toggle to verify input still works
    await quotaInput.fill('25');

    await saveDialog(dialog);

    // Step 2: Re-open and verify quota was saved (row-scoped click +
    // hydration assert via openRowEditor).
    const editDialog = await openRowEditor(adminPage, teamName);

    const savedQuota = editDialog.locator('p-inputnumber input').first();
    await expect(savedQuota).toBeVisible({ timeout: 3_000 });
    // Value should have been saved (either as percentage or count)
    const savedValue = await savedQuota.inputValue();
    expect(savedValue).toBeTruthy();

    await editDialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(editDialog).not.toBeVisible({ timeout: 5_000 });
  });

  test('delete button disabled when team has users', async ({ adminPage, customerName }) => {
    // Seed a team with a real member instead of hunting ambient staging
    // data: the precondition is guaranteed, never skipped over.
    const { teamName } = await seedTeamWithMember(customerName);

    await adminPage.goto('/admin/settings/teams');
    const dialog = await openRowEditor(adminPage, teamName);

    // A team with users must render its delete button DISABLED. PrimeNG
    // puts the disabled state on the INNER <button>, not the <p-button>
    // host that carries the data-testid.
    const deleteButton = dialog.locator('[data-testid="delete-button"]').first();
    await expect(deleteButton).toBeVisible({ timeout: 5_000 });
    await expect(deleteButton.locator('button').first()).toBeDisabled();

    await dialog.locator('[data-testid="cancel-button"]').first().click();
    await expect(dialog).not.toBeVisible({ timeout: 5_000 });
  });

  test('team users dialog opens on user count click', async ({ adminPage, customerName }) => {
    // Seed a team with a member so the user-count tag is guaranteed to exist.
    const { teamName } = await seedTeamWithMember(customerName);

    await adminPage.goto('/admin/settings/teams');
    const rows = await searchAndFind(adminPage, teamName);

    // Find the user count p-tag with user icon (not the team short tag)
    // inside the seeded team's row.
    const userCountTag = rows
      .first()
      .locator('p-tag[icon="pi pi-user"], .p-tag:has(.pi-user)')
      .first();
    await expect(userCountTag).toBeVisible({ timeout: 10_000 });
    await userCountTag.click();

    // Verify the team users dialog opens and shows a data table
    const dialog = adminPage.locator(DIALOG).first();
    await expect(
      dialog,
      'clicking the user-count tag must open the team users dialog'
    ).toBeVisible({ timeout: 5_000 });

    const usersTable = dialog.locator('masterev-infinite-data-table').first();
    await expect(usersTable).toBeVisible({ timeout: 10_000 });

    // Close the dialog via the header close button
    const closeButton = dialog.locator('.p-dialog-header button, [data-testid="cancel-button"]').first();
    await closeButton.click();
    await expect(dialog).not.toBeVisible({ timeout: 5_000 });
  });
});
