import { test, expect, UI_SETTLE_TIMEOUT, type Page } from '../../fixtures.js';
import { unique } from '../../helpers/settings.js';

/**
 * Templates are deploy-seeded: every customer gets the global page
 * templates (privacy, sitenotice; de+en versions each), students-enabled
 * customers additionally get the magic-link mail, and each Convex process
 * contributes its ProcessConfig mail templates. An empty sidebar therefore
 * means the seed did not run — a product failure, never a skip.
 *
 * Mail templates specifically do NOT exist for every customer: see the
 * `hasMailTemplates` flag in src/config.ts (customer gating akin to a
 * feature flag) — the two mail-template tests skip for those customers
 * and hard-assert everywhere else.
 */

/** Wait for the template sidebar to load and return template item buttons */
function sidebarItems(page: Page) {
  // Template items are buttons with a truncated label span (group headers don't have this)
  return page.locator('button:has(span.truncate)');
}

/** Assert the deploy-seeded sidebar has items and return their locator. */
async function expectSidebarItems(page: Page) {
  const items = sidebarItems(page);
  await expect(
    items.first(),
    'sidebar must list deploy-seeded templates — an empty sidebar means the template seed did not run'
  ).toBeVisible({ timeout: 10_000 });
  return items;
}

/** Select the first template item in the sidebar and wait for the editor to load. */
async function selectFirstTemplate(page: Page): Promise<void> {
  const items = await expectSidebarItems(page);
  await items.first().click();
  await expect(page.locator('input#template-title')).toBeVisible({ timeout: 10_000 });
}

// Run this describe block serially within the file. The tests all
// click the "first" template in the sidebar and modify it; running
// them in parallel causes one test's edits to leak into another's
// reads (revert reloads the freshly-saved value, quill editor
// detaches mid-test, etc.). Serialising removes the shared-state
// race without slowing the overall suite meaningfully because the
// template tests are short.
test.describe.configure({ mode: 'serial' });

test.describe('Settings: Templates', () => {
  test.beforeEach(({ features }) => {
    test.skip(!features.templates, 'Templates feature not enabled');
  });

  test('templates page loads with sidebar items', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await expect(adminPage).toHaveURL(/\/settings\/templates/);

    // Wait for template items to appear in sidebar
    const items = await expectSidebarItems(adminPage);
    expect(await items.count()).toBeGreaterThan(0);
  });

  test('select template and verify editor loads', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    // Verify editor components
    await expect(adminPage.locator('quill-editor')).toBeVisible({ timeout: 5_000 });
    await expect(adminPage.locator('input#template-title')).toBeVisible();

    // Verify language tabs
    const tabs = adminPage.locator('[role="tab"]');
    expect(await tabs.count()).toBeGreaterThan(0);
  });

  test('group collapse and expand', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await expectSidebarItems(adminPage);

    // Find an expanded group header button (contains chevron-down icon)
    const expandedHeader = adminPage.locator('button:has(.pi-chevron-down)').first();
    await expect(expandedHeader).toBeVisible({ timeout: 5_000 });

    // Click the group header to collapse
    await expandedHeader.click();

    // Verify it collapsed (chevron-right appears after animation completes)
    await expect(async () => {
      await expect(adminPage.locator('button:has(.pi-chevron-right)').first()).toBeVisible();
    }).toPass({ timeout: 5_000 });

    // Click again to expand
    await adminPage.locator('button:has(.pi-chevron-right)').first().click();

    // Verify it expanded again
    await expect(async () => {
      await expect(adminPage.locator('button:has(.pi-chevron-down)').first()).toBeVisible();
    }).toPass({ timeout: 5_000 });
  });

  test('search filters template items', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    const items = await expectSidebarItems(adminPage);

    // Wait for sidebar to fully render (count stabilizes above 1)
    let countBefore = 0;
    await expect(async () => {
      countBefore = await items.count();
      expect(countBefore).toBeGreaterThan(1);
    }).toPass({ timeout: 10_000 });

    // Get text of first template to use as search term
    const firstItemText = await items.first().textContent();
    const searchTerm = firstItemText?.trim().slice(0, 5) ?? 'mail';

    const searchInput = adminPage.locator('masterev-search-input input').first();
    await expect(searchInput).toBeVisible({ timeout: 5_000 });
    await searchInput.fill(searchTerm);

    // Wait for filtering to take effect — filtered count should be <= original
    await expect(async () => {
      const countAfter = await items.count();
      expect(countAfter).toBeLessThanOrEqual(countBefore);
      expect(countAfter).toBeGreaterThan(0);
    }).toPass({ timeout: 5_000 });

    // Clear search to restore all items
    await searchInput.fill('');
    await expect(async () => {
      const restored = await items.count();
      expect(restored).toBeGreaterThanOrEqual(countBefore);
    }).toPass({ timeout: 5_000 });
  });

  test('edit template title and save', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    const titleInput = adminPage.locator('input#template-title');
    const newTitle = `E2E Test Title ${unique()}`;

    // Clear and fill with new title
    await titleInput.clear();
    await titleInput.fill(newTitle);

    // Save button should become enabled (form is dirty)
    const saveButton = adminPage.locator('p-button button').filter({ hasText: /save|speichern/i }).first();
    await expect(saveButton).toBeEnabled({ timeout: 5_000 });

    // Click save
    await saveButton.click();

    // Wait for save to complete — save button becomes disabled again
    await expect(saveButton).toBeDisabled({ timeout: 10_000 });

    // Reload and re-select the first template. Sidebar labels show the
    // template DESCRIPTION (not the editable title) and the sort order is
    // stable across reloads, so the first item is the same template we
    // just renamed.
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    await expect(titleInput).toBeVisible({ timeout: 10_000 });
    await expect(titleInput).toHaveValue(newTitle);
  });

  test('revert unsaved changes', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    const titleInput = adminPage.locator('input#template-title');
    const modifiedTitle = `E2E Revert ${unique()}`;
    const cancelButton = adminPage.locator('[data-testid="cancel-button"]').first();

    // The editor's `templateForm` is a `computed` that builds a fresh
    // FormGroup whenever its `languageVersion` input changes — and that
    // input is fed by a Convex live query, so a re-emit (common under
    // load) recreates the form, RESETS `dirty`, and removes the cancel
    // button that renders only while dirty. A single fill-then-click
    // therefore races the re-emit. Re-dirty, click, and require the
    // revert to take effect all inside one retry, so a mid-sequence
    // rebuild just costs a retry instead of failing the test.
    await expect(async () => {
      await titleInput.clear();
      await titleInput.fill(modifiedTitle);
      await expect(cancelButton).toBeVisible({ timeout: 3_000 });
      await cancelButton.click();
      await expect(titleInput).not.toHaveValue(modifiedTitle, {
        timeout: 3_000,
      });
    }).toPass({ timeout: UI_SETTLE_TIMEOUT });

    // Verify the modified title is gone after revert. Asserting the
    // exact original title is brittle because the 'edit title' test in
    // this serial file modifies the same first template; the revert
    // reloads from the server which may now return the sibling's saved
    // value. The contract this test owns is that the *unsaved* change is
    // dropped — assert that, not the snapshot.
    await expect(titleInput).not.toHaveValue(modifiedTitle, { timeout: UI_SETTLE_TIMEOUT });
    // The reverted value should still be a non-empty string (form
    // reloaded a real template, not cleared).
    await expect(titleInput).toHaveValue(/.+/, { timeout: 5_000 });
  });

  test('unsaved edits survive a templates live-query re-emit', async ({
    adminPage,
  }) => {
    // Regression for the `templateForm` rebuild-on-re-emit bug. Dirty the
    // first template's title, then in a second tab (same session) edit +
    // save a DIFFERENT template. That mutation makes `api.templates.list`
    // re-emit to THIS tab; the fix must not let that re-emit rebuild the
    // editor form and drop the unsaved edit. Before the fix the form was
    // recreated (dirty reset, cancel button gone), which is the flake seen
    // in `revert unsaved changes` and a real user-facing data-loss bug.
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);
    const titleInput = adminPage.locator('input#template-title');
    const dirtyValue = `E2E Unsaved ${unique()}`;
    await titleInput.clear();
    await titleInput.fill(dirtyValue);
    const cancelButton = adminPage.locator('[data-testid="cancel-button"]').first();
    await expect(cancelButton).toBeVisible({ timeout: 5_000 });

    const page2 = await adminPage.context().newPage();
    try {
      await page2.goto('/admin/settings/templates');
      const items2 = page2.locator('button:has(span.truncate)');
      await expect(items2.nth(1)).toBeVisible({ timeout: 10_000 });
      await items2.nth(1).click();
      const title2 = page2.locator('input#template-title');
      await expect(title2).toBeVisible({ timeout: 10_000 });
      await title2.clear();
      await title2.fill(`E2E Reemit ${unique()}`);
      const save2 = page2
        .locator('p-button button')
        .filter({ hasText: /save|speichern/i })
        .first();
      await expect(save2).toBeEnabled({ timeout: 5_000 });
      await save2.click();
      await expect(save2).toBeDisabled({ timeout: 10_000 }); // save committed
    } finally {
      await page2.close();
    }

    // Give this tab time to receive and process the live-query re-emit,
    // then assert the unsaved edit is intact (form NOT rebuilt).
    await adminPage.waitForTimeout(2_500);
    await expect(titleInput).toHaveValue(dirtyValue, { timeout: 5_000 });
    await expect(cancelButton).toBeVisible({ timeout: 5_000 });
  });

  test('language tab switching', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    // Every deploy-seeded template ships de+en versions, so at least two
    // language tabs must render (button[role="tab"], not navigation
    // a[role="tab"]). Fewer means the seed or the tab rendering broke.
    const langTabs = adminPage.locator('button[role="tab"]');
    await expect(async () => {
      expect(await langTabs.count()).toBeGreaterThanOrEqual(2);
    }).toPass({ timeout: 10_000 });

    const secondTab = langTabs.nth(1);
    await expect(secondTab).toBeVisible({ timeout: 5_000 });

    const wasSelected = await secondTab.getAttribute('aria-selected');
    if (wasSelected === 'true') {
      // Second tab already selected, click first instead
      await langTabs.first().click();
      await expect(langTabs.first()).toHaveAttribute('aria-selected', 'true', { timeout: 5_000 });
    } else {
      await secondTab.click();
      await expect(secondTab).toHaveAttribute('aria-selected', 'true', { timeout: 5_000 });
    }
  });

  test('placeholder dropdown exists for mail templates', async ({ adminPage, customerConfig }) => {
    test.skip(
      !customerConfig.hasMailTemplates,
      'Customer has no mail templates by configuration (no process templates, students feature off)'
    );

    await adminPage.goto('/admin/settings/templates');
    await expectSidebarItems(adminPage);

    // Find and click a mail template (blue icon). Mail templates are
    // deploy-seeded for this customer, so absence is a failure.
    const mailTemplate = sidebarItems(adminPage).filter({ has: adminPage.locator('.text-blue-500') }).first();
    await expect(mailTemplate, 'deploy-seeded mail template must be listed').toBeVisible({ timeout: 5_000 });

    await mailTemplate.click();
    await expect(adminPage.locator('input#template-title')).toBeVisible({ timeout: 10_000 });

    // Verify placeholder dropdown exists in the Quill toolbar
    // Quill hides native <select> and renders a styled replacement (.ql-picker)
    await expect(adminPage.locator('.ql-placeholder.ql-picker')).toBeVisible({ timeout: 5_000 });
  });

  test('quill editor body: type text, save, verify persistence', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    const quillEditor = adminPage.locator('.ql-editor').first();
    await expect(quillEditor).toBeVisible({ timeout: 10_000 });

    // Type unique text into the editor
    const marker = `E2E-BODY-${unique()}`;
    await quillEditor.click();
    await adminPage.keyboard.press('End');
    await adminPage.keyboard.type(marker);

    // Save
    const saveButton = adminPage.locator('[data-testid="save-button"]').first();
    await expect(saveButton).toBeEnabled({ timeout: 5_000 });
    await saveButton.click();
    // Wait for save to complete (button may not disable — fall back to a short
    // propagation wait; the reload assertion below is the authoritative check)
    const disabled = await expect(saveButton).toBeDisabled({ timeout: 5_000 }).then(() => true).catch(() => false);
    if (!disabled) {
      await adminPage.waitForTimeout(2_000);
    }

    // Reload and re-select the same template
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    const quillAfter = adminPage.locator('.ql-editor').first();
    await expect(quillAfter).toBeVisible({ timeout: 10_000 });
    const contentAfter = await quillAfter.innerHTML();
    expect(contentAfter).toContain(marker);

    // Note: Quill uses Delta (object) format — restoring original content via innerHTML
    // doesn't properly sync with Angular form control. The marker text will remain
    // until the template is manually edited. This is acceptable for E2E test data.
  });

  test('quill toolbar bold button works', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await selectFirstTemplate(adminPage);

    // The Quill instance attaches lazily (@defer on viewport) and staging can
    // be slow; give it a generous settle budget. An editor that never attaches
    // is a rendering regression and fails the test.
    const quillEditor = adminPage.locator('.ql-editor').first();
    await expect(quillEditor).toBeVisible({ timeout: UI_SETTLE_TIMEOUT });

    // Click into editor and type some text
    await quillEditor.click();
    await adminPage.keyboard.press('End');

    // Use the platform's bold shortcut (avoids focus-loss from clicking the
    // toolbar button). Quill binds Cmd+B on macOS and Ctrl+B elsewhere — a
    // hardcoded Control+b silently does nothing on a local macOS run.
    const boldShortcut = process.platform === 'darwin' ? 'Meta+b' : 'Control+b';
    await adminPage.keyboard.press(boldShortcut);

    // Type bold text
    await adminPage.keyboard.type('bold-test');

    // Toggle bold off
    await adminPage.keyboard.press(boldShortcut);

    // Verify the bold text was inserted (check for <strong> tag). Poll: Quill
    // applies the format asynchronously. Bold never appearing means the Quill
    // keyboard module is broken — fail, don't skip.
    await expect(async () => {
      expect(await quillEditor.innerHTML()).toContain('<strong>bold-test</strong>');
    }).toPass({ timeout: 5_000 });

    // Revert changes (best-effort cleanup, don't fail if page is already closing)
    try {
      const cancelButton = adminPage.locator('[data-testid="cancel-button"]').first();
      const canCancel = await cancelButton.waitFor({ state: 'visible', timeout: 3_000 }).then(() => true).catch(() => false);
      if (canCancel) await cancelButton.click();
    } catch { /* page may already be closing during teardown */ }
  });

  test('placeholder dropdown inserts placeholder into editor', async ({ adminPage, customerConfig }) => {
    test.skip(
      !customerConfig.hasMailTemplates,
      'Customer has no mail templates by configuration (no process templates, students feature off)'
    );

    await adminPage.goto('/admin/settings/templates');
    await expectSidebarItems(adminPage);

    // Find a mail template (has placeholder dropdown); deploy-seeded here.
    const mailTemplate = sidebarItems(adminPage).filter({ has: adminPage.locator('.text-blue-500') }).first();
    await expect(mailTemplate, 'deploy-seeded mail template must be listed').toBeVisible({ timeout: 5_000 });

    await mailTemplate.click();
    await expect(adminPage.locator('input#template-title')).toBeVisible({ timeout: 10_000 });

    // The placeholder picker always renders once the editor modules load
    // (the global MASTEREV_URL placeholder exists for every template).
    const placeholderPicker = adminPage.locator('.ql-placeholder.ql-picker').first();
    await expect(placeholderPicker).toBeVisible({ timeout: 10_000 });

    // Click into editor first
    const quillEditor = adminPage.locator('.ql-editor').first();
    await quillEditor.click();
    await adminPage.keyboard.press('End');

    // Open placeholder dropdown and select first option
    await placeholderPicker.click();
    const pickerItem = adminPage.locator('.ql-placeholder .ql-picker-item').first();
    await expect(pickerItem).toBeVisible({ timeout: 3_000 });
    await pickerItem.click();

    // Verify a placeholder span was inserted
    const editorHtml = await quillEditor.innerHTML();
    expect(editorHtml).toMatch(/placeholder|ql-placeholder/i);

    // Revert (best-effort cleanup, don't fail if page is already closing)
    try {
      const cancelButton = adminPage.locator('[data-testid="cancel-button"]').first();
      const canCancel = await cancelButton.waitFor({ state: 'visible', timeout: 3_000 }).then(() => true).catch(() => false);
      if (canCancel) await cancelButton.click();
    } catch { /* page may already be closing during teardown */ }
  });

  test('page template loads with editor', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    await expectSidebarItems(adminPage);

    // Find a page template (amber icon). The privacy/sitenotice page
    // templates are deploy-seeded for EVERY customer, so absence is a failure.
    const pageTemplate = sidebarItems(adminPage).filter({ has: adminPage.locator('.text-amber-500, .text-amber-600') }).first();
    await expect(pageTemplate, 'deploy-seeded page template must be listed').toBeVisible({ timeout: 5_000 });

    await pageTemplate.click();
    await expect(adminPage.locator('input#template-title')).toBeVisible({ timeout: 10_000 });

    // Quill editor should be visible
    await expect(adminPage.locator('.ql-editor').first()).toBeVisible({ timeout: 10_000 });

    // Verify save and cancel buttons exist
    await expect(adminPage.locator('[data-testid="save-button"]').first()).toBeVisible();
  });

  test('switching templates loads different content', async ({ adminPage }) => {
    await adminPage.goto('/admin/settings/templates');
    const items = await expectSidebarItems(adminPage);

    // Every customer has at least the two seeded page templates; wait for the
    // sidebar to render at least 2 items instead of skipping.
    await expect(async () => {
      expect(await items.count()).toBeGreaterThanOrEqual(2);
    }).toPass({ timeout: 10_000 });

    // Select first template
    await items.first().click();
    const titleInput = adminPage.locator('input#template-title');
    await expect(titleInput).toBeVisible({ timeout: 10_000 });

    // Select second template
    await items.nth(1).click();

    // Verify the editor loaded for the newly selected template without error
    await expect(titleInput).toBeVisible({ timeout: 10_000 });
    await expect(adminPage.locator('.ql-editor').first()).toBeVisible({ timeout: 10_000 });
  });
});
