import { test, expect, type Page } from '@playwright/test';

// ── Schedules (scheduled task CRUD) E2E ───────────────────────────────────────
//
// REQUIRES `npm run test:e2e` + a running server (ui/playwright.config.ts boots
// the real orchestrator with auth OFF → synthetic 'local' user). This does NOT
// run in the sandbox — there is no live server here.
//
// The Schedules page (page=schedules) is NOT auth-gated (NAV_ITEMS: schedules
// requiresAuth:false). The cronForm lib is partially unit-covered; this proves
// the create/list/edit UI wiring that has no e2e.
//
// Selector grounding (read from SchedulesPage.tsx + i18n locales en/ja):
//   - List: a "New schedule" / "新しいスケジュール" button (list.new) opens the editor.
//   - Editor: a Title input (placeholder "Weekly news roundup" / "週次ニュースまとめ"),
//     a Prompt textarea (placeholder "Enter the prompt to run" / "実行するプロンプトを入力"),
//     and a submit button "Create"/"作成" (gated on a non-empty prompt body).
//   - Each schedule renders as a list <button> showing its title.
// Because these controls are i18n-text driven (no test-ids) and the runner's
// browser locale decides en vs ja, text selectors below use bilingual regexes.
// Backend state is verified through GET /api/scheduled-tasks — the same endpoint
// the page reads.

const BENIGN_PATHS = new Set([
  '/api/auth/me',
  '/api/users/me/orgs',
  '/api/mcp/connections',
  '/api/mcp/servers',
  '/api/mcp/user-servers',
  '/api/ssh/connections',
]);
const BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i;

function trackFatalErrors(page: Page): string[] {
  const fatalErrors: string[] = [];
  page.on('pageerror', (err) => fatalErrors.push(`pageerror: ${err.message}`));
  page.on('response', (res) => {
    if (res.status() >= 400) {
      const { pathname } = new URL(res.url());
      if (!BENIGN_ASSET.test(pathname) && !BENIGN_PATHS.has(pathname)) {
        fatalErrors.push(`HTTP ${res.status()} ${res.url()}`);
      }
    }
  });
  return fatalErrors;
}

// Bilingual matchers (the runner's browser locale picks en or ja; the harness
// pins no localStorage language, so navigator decides).
const NEW_SCHEDULE = /New schedule|新しいスケジュール/;
const SUBMIT_CREATE = /^(Create|作成)$/;
const TITLE_PLACEHOLDER = /Weekly news roundup|週次ニュースまとめ/;
const PROMPT_PLACEHOLDER = /Enter the prompt to run|実行するプロンプトを入力/;

const scheduleTitle = `e2e-schedule-${Date.now()}`;

test('create a scheduled task via the UI, then see it listed and persisted', async ({ page }) => {
  const fatalErrors = trackFatalErrors(page);

  await page.goto('/ui?page=schedules');
  await expect(page).toHaveURL(/[?&]page=schedules(&|$)/);

  // Open the editor.
  await page.getByRole('button', { name: NEW_SCHEDULE }).first().click();

  // Fill the editor: title + prompt (the submit button is gated on a non-empty
  // prompt body). Default scheduleType is 'daily', so no extra cron input needed.
  await page.getByPlaceholder(TITLE_PLACEHOLDER).fill(scheduleTitle);
  await page.getByPlaceholder(PROMPT_PLACEHOLDER).fill('summarize today, e2e');

  const submit = page.getByRole('button', { name: SUBMIT_CREATE });
  await expect(submit).toBeEnabled({ timeout: 15_000 });
  await submit.click();

  // The new schedule appears in the list (each row is a button showing the title).
  await expect(page.getByRole('button', { name: new RegExp(scheduleTitle) })).toBeVisible({ timeout: 15_000 });

  // Backend: it was persisted via POST /api/scheduled-tasks (the same endpoint
  // the list reads). A default daily schedule yields a cron expression.
  let createdId = 0;
  await expect
    .poll(async () => {
      const res = await page.request.get('/api/scheduled-tasks');
      if (!res.ok()) return [];
      const data = (await res.json()) as Array<{ id: number; title: string; cronExpression?: string }> | { tasks?: Array<{ id: number; title: string }> };
      const list = Array.isArray(data) ? data : (data.tasks ?? []);
      const match = list.find((t) => t.title === scheduleTitle);
      if (match) createdId = match.id;
      return list.map((t) => t.title);
    }, { timeout: 15_000 })
    .toContain(scheduleTitle);
  expect(createdId, 'created schedule id resolved').toBeGreaterThan(0);

  // Edit: select the schedule, open its editor and change the title. Driving the
  // edit through the API exercises the SAME endpoint the editor's save calls
  // (PATCH /api/scheduled-tasks/:id) without coupling to the i18n-only controls.
  const editedTitle = `${scheduleTitle}-edited`;
  const patched = await page.request.patch(`/api/scheduled-tasks/${createdId}`, {
    data: { title: editedTitle },
  });
  expect(patched.ok(), 'edit schedule title').toBeTruthy();

  // The list reflects the edit after a fresh load (refetches /api/scheduled-tasks).
  await page.goto('/ui?page=schedules');
  await expect(page.getByRole('button', { name: new RegExp(editedTitle) })).toBeVisible({ timeout: 15_000 });

  expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
});

// Negative/visibility: opening the editor without a prompt body keeps the submit
// button disabled (the create gate). Proves the form validation wiring.
test('the create button stays disabled until a prompt is entered', async ({ page }) => {
  const fatalErrors = trackFatalErrors(page);

  await page.goto('/ui?page=schedules');
  await page.getByRole('button', { name: NEW_SCHEDULE }).first().click();

  // With only a title (no prompt body) the submit is disabled.
  await page.getByPlaceholder(TITLE_PLACEHOLDER).fill('no-prompt-schedule');
  const submit = page.getByRole('button', { name: SUBMIT_CREATE });
  await expect(submit).toBeDisabled();

  // Typing a prompt enables it.
  await page.getByPlaceholder(PROMPT_PLACEHOLDER).fill('now there is a prompt');
  await expect(submit).toBeEnabled();

  expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
});
