/**
 * Journey: Content Editor — Auto-Save & Save-Status Indicator
 *
 * Covers the auto-save state machine and manual "Save Now" button introduced /
 * refined by:
 *   - useAutoSave.ts — F5A11Y-006 (ariaStatus), R6FE-012 (pending-resave on
 *     double-click), EDIT-P1-4 (content snapshot), EDIT-P2-7 (no silent no-op)
 *   - ContentEditor/Edit.tsx — aria-live save-status indicator, "Save Now" button,
 *     "Auto-saves every 30 seconds" footnote
 *
 * Golden path:
 *   1. Editor page loads with aria-live="polite" save-status region
 *   2. Initial state: no "Unsaved changes" text, no "Save Now" button
 *   3. Typing in TipTap → "Unsaved changes" indicator + "Save Now" button appear
 *   4. Clicking "Save Now" → PATCH fires to content-editor.update,
 *      indicator updates to "Saved just now"
 *
 * Sad paths:
 *   - Save network failure → destructive alert "Save Failed:" appears
 *   - Double-clicking "Save Now" while in-flight → no crash (pending-resave path)
 *   - Unauthenticated → redirects to /login
 *
 * NOTE on public_id routing: all URLs use ULID public_ids resolved via artisan
 * tinker (migration 2026_05_27_000001_add_public_id_to_route_bound_tables).
 */

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { expect, test } from '../fixtures/auth';

// ---------------------------------------------------------------------------
// PHP execution helper (avoids shell-quoting issues with artisan tinker)
// ---------------------------------------------------------------------------

function runPhp(phpCode: string): string {
  const tmpFile = path.join('/tmp', `e2e_autosave_${process.pid}_${Date.now()}.php`);
  try {
    fs.writeFileSync(tmpFile, phpCode, 'utf8');
    return execSync(`php artisan tinker --execute="$(cat ${tmpFile})" 2>&1`, {
      cwd: process.cwd(),
      encoding: 'utf8',
      timeout: 30_000,
      shell: '/bin/bash',
    });
  } finally {
    try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
  }
}

function resolveSitePublicId(siteId: number): string {
  const raw = runPhp(
    `$s = \\App\\Models\\Site::find(${siteId}); echo $s ? $s->public_id : 'NOT_FOUND';`,
  );
  const result = raw.trim().split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
  if (!result || result === 'NOT_FOUND') throw new Error(`resolveSitePublicId(${siteId}) failed. Raw:\n${raw}`);
  return result;
}

function resolveDraftPublicId(draftId: number): string {
  const raw = runPhp(
    `$d = \\App\\Models\\AiDraft::find(${draftId}); echo $d ? $d->public_id : 'NOT_FOUND';`,
  );
  const result = raw.trim().split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
  if (!result || result === 'NOT_FOUND') throw new Error(`resolveDraftPublicId(${draftId}) failed. Raw:\n${raw}`);
  return result;
}

// ---------------------------------------------------------------------------
// Known background noise (APP_URL domain/scheme mismatch)
// ---------------------------------------------------------------------------

const KNOWN_NOISE = ['/api/lead-score/pql-signal', '/profile', 'ERR_TOO_MANY_REDIRECTS'];

// ---------------------------------------------------------------------------
// Module-scope state (resolved once per file in beforeAll)
// ---------------------------------------------------------------------------

let sitePublicId = '';
let draftPublicId = '';

const editorUrl = () => `/sites/${sitePublicId}/drafts/${draftPublicId}/edit`;
/** PATCH route pattern for content-editor.update — used in route interception tests. */
const contentPatchUrl = () => `/sites/${sitePublicId}/drafts/${draftPublicId}/content`;

// Serialize all tests so beforeAll fires only once (parallel workers restart
// beforeAll per describe block when fullyParallel: true).
test.describe.configure({ mode: 'serial' });

test.describe('Journey: Content Editor — Auto-Save', () => {
  test.beforeAll(async () => {
    const seedDataPath = path.join(process.cwd(), 'tests/e2e/.seed-data.json');
    const seedData = JSON.parse(fs.readFileSync(seedDataPath, 'utf8')) as {
      siteId: number;
      aiDraftId: number;
    };
    sitePublicId = resolveSitePublicId(seedData.siteId);
    draftPublicId = resolveDraftPublicId(seedData.aiDraftId);
    console.log(`[AutoSave] site=${sitePublicId} draft=${draftPublicId}`);
  });

  // ── Golden path ──────────────────────────────────────────────────────────

  test('editor page loads without JS errors and has aria-live save-status region', async ({ page }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));
    page.on('console', (msg) => {
      if (msg.type() === 'error') errors.push(`console.error: ${msg.text()}`);
    });

    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });
    await expect(page).toHaveURL(new RegExp(sitePublicId));

    // aria-live save-status region must be present
    const saveStatus = page.locator('[aria-live="polite"][aria-atomic="true"]').first();
    await expect(saveStatus).toBeAttached();

    // Footer footnote mentioning auto-save interval
    await expect(page.locator('text=Auto-saves every 30 seconds')).toBeVisible();

    // No unexpected JS errors
    const appErrors = errors.filter(
      (e) => !e.includes('extension') && !e.includes('chrome-extension') && !KNOWN_NOISE.some((n) => e.includes(n)),
    );
    expect(appErrors, `Uncaught errors:\n${appErrors.join('\n')}`).toHaveLength(0);
  });

  test('initial state: no "Unsaved changes" visible, no "Save Now" button', async ({ page }) => {
    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });

    // Wait for the page to settle (TipTap lazy-loads)
    await page.waitForLoadState('networkidle');

    // Unsaved changes indicator should not be present initially
    await expect(page.getByText('Unsaved changes')).not.toBeVisible();

    // "Save Now" button should not be present initially
    await expect(page.getByRole('button', { name: /save now/i })).not.toBeVisible();
  });

  test('typing in the editor reveals "Unsaved changes" indicator and "Save Now" button', async ({ page }) => {
    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });

    // Wait for TipTap to fully mount (lazy Suspense — skeleton disappears)
    const editor = page.locator('.tiptap[contenteditable="true"]').first();
    await editor.waitFor({ state: 'visible', timeout: 15_000 });

    // Click inside the editor and type a unique string to ensure a change is detected
    await editor.click();
    await page.keyboard.type(' e2e-autosave-probe');

    // "Unsaved changes" warning should appear in the save-status region
    await expect(page.getByText('Unsaved changes')).toBeVisible({ timeout: 5_000 });

    // "Save Now" button should now be visible
    await expect(page.getByRole('button', { name: /save now/i })).toBeVisible({ timeout: 5_000 });
  });

  test('clicking "Save Now" sends PATCH and shows "Saved" confirmation', async ({ page }) => {
    // Track PATCH requests to the content-editor.update endpoint
    const patchFired: string[] = [];
    page.on('request', (req) => {
      if (req.method() === 'PATCH' && req.url().includes('/content')) {
        patchFired.push(req.url());
      }
    });

    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });

    const editor = page.locator('.tiptap[contenteditable="true"]').first();
    await editor.waitFor({ state: 'visible', timeout: 15_000 });

    // Type to mark dirty
    await editor.click();
    await page.keyboard.type(' e2e-save-now-probe');

    // Wait for "Save Now" button
    const saveNowBtn = page.getByRole('button', { name: /save now/i });
    await saveNowBtn.waitFor({ state: 'visible', timeout: 5_000 });

    // Click Save Now
    await saveNowBtn.click();

    // Should transition through "Saving" → "Saved just now"
    // We wait for the success state — "Saved" text must appear (with "just now" from formatLastSaved)
    await expect(page.locator('[aria-live="polite"]').filter({ hasText: /saved/i })).toBeVisible({ timeout: 15_000 });

    // PATCH must have fired
    expect(patchFired.length, 'Expected at least one PATCH to content-editor.update').toBeGreaterThanOrEqual(1);

    // "Save Now" button should disappear after save completes (no longer dirty)
    await expect(saveNowBtn).not.toBeVisible({ timeout: 5_000 });
  });

  // ── Double-submit protection ──────────────────────────────────────────────

  test('"Save Now" button is disabled while save is in-flight (no double-fire)', async ({ page }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));

    // Delay the PATCH so we can observe the disabled state mid-flight
    await page.route('**' + contentPatchUrl(), async (route) => {
      await new Promise((resolve) => setTimeout(resolve, 500));
      await route.continue();
    });

    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });

    const editor = page.locator('.tiptap[contenteditable="true"]').first();
    await editor.waitFor({ state: 'visible', timeout: 15_000 });

    await editor.click();
    await page.keyboard.type(' e2e-inflight-probe');

    const saveNowBtn = page.getByRole('button', { name: /save now/i });
    await saveNowBtn.waitFor({ state: 'visible', timeout: 5_000 });

    // Click — save goes in-flight, button becomes disabled (isSaving=true, disabled prop)
    await saveNowBtn.click();

    // While the delayed PATCH is in-flight the button must be disabled
    await expect(saveNowBtn).toBeDisabled({ timeout: 2_000 });

    // After PATCH resolves: "Saved" appears, button disappears (no crash)
    await expect(page.locator('[aria-live="polite"]').filter({ hasText: /saved/i })).toBeVisible({ timeout: 10_000 });

    // No uncaught pageerrors
    expect(errors, `Uncaught pageerrors: ${errors.join(', ')}`).toHaveLength(0);
  });

  // ── Save error (sad path) ────────────────────────────────────────────────

  test('save failure shows destructive alert with error message', async ({ page }) => {
    // Force the PATCH to 422 to simulate a server-side save error
    await page.route('**' + contentPatchUrl(), async (route) => {
      await route.fulfill({
        status: 422,
        contentType: 'application/json',
        body: JSON.stringify({ message: 'Draft content is required.' }),
      });
    });

    await page.goto(editorUrl(), { waitUntil: 'domcontentloaded' });

    const editor = page.locator('.tiptap[contenteditable="true"]').first();
    await editor.waitFor({ state: 'visible', timeout: 15_000 });

    await editor.click();
    await page.keyboard.type(' e2e-error-probe');

    const saveNowBtn = page.getByRole('button', { name: /save now/i });
    await saveNowBtn.waitFor({ state: 'visible', timeout: 5_000 });
    await saveNowBtn.click();

    // A destructive alert with "Save Failed:" prefix must appear
    await expect(page.getByText(/Save Failed:/i)).toBeVisible({ timeout: 10_000 });
  });

  // ── Permission / auth (sad path) ─────────────────────────────────────────

  test('unauthenticated access redirects to /login', async ({ browser }) => {
    const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
    try {
      const page = await context.newPage();
      await page.goto(`/sites/${sitePublicId}/drafts/${draftPublicId}/edit`);
      await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
    } finally {
      await context.close();
    }
  });
});
