/**
 * Workflow Verification: Review Queue (Review/Queue.tsx)
 * Session: ed72dafd-2ec0-4e39-bee4-40b570b4fc96
 *
 * Golden path and sad-path exercise for the cohort-scoped Review Queue page.
 * Scope derived from diff (no SUCCESS_CRITERIA / WORKFLOW_BLAST_RADIUS files present).
 *
 * === Changed features verified ===
 * R8UX-001: "Publish kept (N) →" sticky CTA after Keep action
 * R8UX-006: Metadata-only initial payload; content fetched on-demand
 * R8UX-103: Persistent convert-arrival banner (converted_count > 0)
 * R8UX-108: Cap banner copy explains triage→reload mechanism
 * R8VER-005: "Showing 200 of N" cap banner
 * R9UX-104: "Back to Recovery Run" back-link label (not "Back to Batch")
 *
 * === States exercised ===
 * A. Golden path: queue with one pending draft renders correctly
 * B. Empty state (no pending drafts): "You're all caught up" renders
 * C. Permission denied: unauthenticated access redirects to /login
 *
 * === public_id routing ===
 * All user-facing routes bind via HasPublicId (ULID). Site public_id is resolved
 * from the E2E seed data via artisan tinker before the suite runs.
 *
 * === PHP in temp file / bash expansion note ===
 * PHP variables in template strings use `$var` (no backslash) — bash does NOT
 * re-expand variables inside $(cat file) results, so $q,$ids,$site are safe.
 * FQCN use \\App\\... (JS double-backslash → single \ in file → valid PHP FQCN).
 */

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

// ---------------------------------------------------------------------------
// Known environmental noise (419 CSRF from background pql-signal fetch,
// occasional profile redirect — pre-existing, not introduced by this session)
// ---------------------------------------------------------------------------
const KNOWN_NOISE = [
  '/api/lead-score/pql-signal',
  '/profile',
  'ERR_TOO_MANY_REDIRECTS',
  'favicon',
  'chrome-extension',
  'extension',
];

// ---------------------------------------------------------------------------
// PHP execution helper (avoids shell-quoting issues)
// Uses temp file + $(cat file) pattern from existing E2E conventions.
// ---------------------------------------------------------------------------
function runPhp(phpCode: string): string {
  const tmpFile = path.join('/tmp', `e2e_rq_${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;
}

/** Ensures the seeded E2E AiDraft is in "pending" state (unrated, not published). */
function resetDraftToPending(aiDraftId: number): void {
  runPhp(`
\\App\\Models\\AiDraft::where('id', ${aiDraftId})->update([
  'quality_rating' => null,
  'publish_status' => 'not_published',
  'published_at' => null,
]);
echo "reset_ok";
  `);
}

/** Hard-deletes extra AiDrafts for the site, keeping only the seeded one. */
function clearExtraDraftsForSite(siteId: number, keepDraftId: number): void {
  // Use whereIn with pluck to avoid fn() syntax issues in bash-interpolated PHP.
  runPhp(`
$jobIds = \\App\\Models\\AiJob::where('site_id', ${siteId})->pluck('id')->toArray();
if (!empty($jobIds)) {
  \\App\\Models\\AiDraft::whereIn('ai_job_id', $jobIds)
    ->where('id', '!=', ${keepDraftId})
    ->delete();
}
echo "cleared_extras";
  `);
}

/** Sets the seeded draft to published (excluded from default queue). */
function markDraftPublished(aiDraftId: number): void {
  runPhp(`
\\App\\Models\\AiDraft::where('id', ${aiDraftId})->update([
  'published_at' => now()->toDateTimeString(),
  'publish_status' => 'published',
]);
echo "published";
  `);
}

/** Restores draft to pending after a "published" test. */
function markDraftUnpublished(aiDraftId: number): void {
  runPhp(`
\\App\\Models\\AiDraft::where('id', ${aiDraftId})->update([
  'published_at' => null,
  'publish_status' => 'not_published',
  'quality_rating' => null,
]);
echo "unpublished";
  `);
}

const reviewQueueUrl = (spid: string) => `/sites/${spid}/review`;

// Shared state set by beforeAll
let sitePublicId = '';

// ---------------------------------------------------------------------------
// Suite A: Golden path — queue with one pending draft
// ---------------------------------------------------------------------------

test.describe('Review Queue — golden path (one pending draft)', () => {
  test.beforeAll(async ({ seedData }: { seedData: E2ESeedData }) => {
    sitePublicId = resolveSitePublicId(seedData.siteId);
    clearExtraDraftsForSite(seedData.siteId, seedData.aiDraftId);
    resetDraftToPending(seedData.aiDraftId);
  });

  test('renders "Draft 1 of 1" heading with tally counters', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));
    page.on('console', (msg) => {
      if (msg.type() === 'error') errors.push(msg.text());
    });

    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // R8UX-001: Page heading shows current draft position
    await expect(page.getByRole('heading', { name: /draft 1 of 1/i })).toBeVisible();

    // R8UX-001: Tally counters visible with action-lexicon labels
    await expect(page.getByText(/kept/i).first()).toBeVisible();
    await expect(page.getByText(/discarded/i).first()).toBeVisible();
    await expect(page.getByText(/remaining/i).first()).toBeVisible();

    // Pager navigation bar visible
    const prevBtn = page.getByRole('button', { name: /prev/i });
    const nextBtn = page.getByRole('button', { name: /next/i });
    await expect(prevBtn).toBeVisible();
    await expect(nextBtn).toBeVisible();

    // Prev/Next disabled when only one draft
    await expect(prevBtn).toBeDisabled();
    await expect(nextBtn).toBeDisabled();

    const appErrors = errors.filter((e) => !KNOWN_NOISE.some((n) => e.includes(n)));
    expect(appErrors, `Console errors:\n${appErrors.join('\n')}`).toHaveLength(0);
  });

  test('zero failed XHR requests on initial load (R8UX-006 content fetch)', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    const failedRequests: string[] = [];
    page.on('response', (r) => {
      if (r.status() >= 400 && !KNOWN_NOISE.some((n) => r.url().includes(n))) {
        failedRequests.push(`${r.status()} ${r.url()}`);
      }
    });

    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // R8UX-006: on-demand content fetch for the first draft should succeed
    expect(
      failedRequests,
      `Failed HTTP requests:\n${failedRequests.join('\n')}`,
    ).toHaveLength(0);
  });

  test('R8UX-001: "Publish kept" CTA hidden when no drafts are kept', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // CTA only appears when at least one draft is kept-and-unpublished
    await expect(page.getByText(/publish kept/i)).not.toBeVisible();
  });

  test('jump-to dropdown is present and shows draft count', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // Jump-to dropdown trigger shows position "1 / 1"
    const dropdownTrigger = page.getByRole('button', { name: /1 \/ 1/i });
    await expect(dropdownTrigger).toBeVisible();
  });

  test('DraftReviewPanel mounts and shows recommendation title', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));

    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // DraftReviewPanel should have mounted — look for the recommendation title
    // The seeded draft has recommendation.title = "Rewrite content to recover lost traffic"
    await expect(page.getByText(/rewrite content to recover lost traffic/i)).toBeVisible();

    const appErrors = errors.filter((e) => !KNOWN_NOISE.some((n) => e.includes(n)));
    expect(appErrors, `Console errors:\n${appErrors.join('\n')}`).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// Suite B: Empty state (default cohort — no pending drafts)
// ---------------------------------------------------------------------------

test.describe('Review Queue — empty state (no pending drafts)', () => {
  test.beforeAll(async ({ seedData }: { seedData: E2ESeedData }) => {
    sitePublicId = resolveSitePublicId(seedData.siteId);
    // Mark the only draft as published — excluded from the default cohort
    markDraftPublished(seedData.aiDraftId);
  });

  test.afterAll(async ({ seedData }: { seedData: E2ESeedData }) => {
    // Restore draft to pending so other suites work
    markDraftUnpublished(seedData.aiDraftId);
  });

  test('shows "You\'re all caught up" empty state', async ({
    page,
  }: { page: import('@playwright/test').Page }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));
    page.on('console', (msg) => {
      if (msg.type() === 'error') errors.push(msg.text());
    });

    await page.goto(reviewQueueUrl(sitePublicId), { waitUntil: 'networkidle' });

    // Empty-state: "You're all caught up" copy (no pending drafts)
    await expect(page.getByText(/you.re all caught up/i)).toBeVisible();

    // CTA to generate more rewrites (default cohort empty state)
    await expect(page.getByRole('link', { name: /go to content inventory/i })).toBeVisible();

    // No pager when empty
    await expect(page.getByRole('button', { name: /prev/i })).not.toBeVisible();

    const appErrors = errors.filter((e) => !KNOWN_NOISE.some((n) => e.includes(n)));
    expect(appErrors, `Console errors:\n${appErrors.join('\n')}`).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// Suite C: Permission denied — unauthenticated access
// ---------------------------------------------------------------------------

test.describe('Review Queue — permission denied (unauthenticated)', () => {
  test.beforeAll(async ({ seedData }: { seedData: E2ESeedData }) => {
    sitePublicId = resolveSitePublicId(seedData.siteId);
  });

  test('unauthenticated access redirects to /login', async ({
    browser,
  }: { browser: import('@playwright/test').Browser }) => {
    // Fresh context with no auth state
    const ctx = await browser.newContext({ storageState: { cookies: [], origins: [] } });
    const p = await ctx.newPage();
    await p.goto(reviewQueueUrl(sitePublicId));
    await expect(p).toHaveURL(/\/login/);
    await ctx.close();
  });
});
