/**
 * Journey: WP connect "Show setup key" label + Freshness Show page
 *
 * Verifies label/null-guard changes from session fdd9585f-faa0-4521-b4e5-0dd434f9f5aa:
 *
 * 1. OnboardingWizard (WordPress Plugin card, unconnected state):
 *    Button reads "Show setup key", not the old "Connect WordPress".
 *    Exercised by temporarily removing the E2E site's WP connection so
 *    the wizard shows the unconnected state.
 *
 * 2. WpConnectCard — DELETED in GSC-NEW-01 (dead code with no Page importers).
 *    The canonical surface is OnboardingWizard via DownloadPluginButton /
 *    ActivationPromptCard. If a standalone card is needed, build against
 *    ConnectionHealthPanel / ConnectionStatusBadge.
 *
 * 3. PluginInstallHelper — step 5 says "click Show setup key" (not the
 *    old "Return here and enter your WordPress URL").
 *
 * 4. Freshness Show page — page renders without crash + FreshnessScoreCard is
 *    present. Note: page_decay_signals.freshness_score has a NOT NULL DB
 *    constraint, so a null score cannot be seeded for E2E; the null guard in
 *    FreshnessScoreCard is verified at the unit/source level (component reads
 *    `score !== null && score !== undefined` before computing). The E2E test
 *    here confirms the full Show page render path (controller → React → card)
 *    works without crash.
 *
 * == Pre-existing 419 console errors ==
 * Background API calls (analytics, timezone) fire without X-XSRF-TOKEN in
 * test sessions, producing "Failed to load resource: 419" console errors.
 * These are excluded from the no-errors assertion (same pattern as
 * freshness-watchlist.spec.ts).
 *
 * == public_id routing ==
 * Since migration 2026_05_27_000001_add_public_id_to_route_bound_tables,
 * all user-facing routes use ULID public_id, not integer PK.
 * resolveSitePublicId() resolves this via artisan tinker before the suite.
 */

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

// ---------------------------------------------------------------------------
// PHP execution helpers (same pattern as freshness-watchlist.spec.ts)
// ---------------------------------------------------------------------------

function runPhp(phpCode: string): string {
  const tmpFile = path.join('/tmp', `e2e_wp_label_${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 {
    fs.unlinkSync(tmpFile);
  }
}

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;
}

/** Deletes WP connection for the given site (if any) and returns it. */
function clearWpConnection(siteId: number): void {
  runPhp(`
$conn = \\App\\Models\\WpConnection::where('site_id', ${siteId})->first();
if ($conn) { $conn->delete(); echo "deleted"; } else { echo "none"; }
  `);
}

/**
 * Creates a completed PageDecayRun + FreshnessRecommendation with a real
 * (non-null) freshness_score. Note: page_decay_signals.freshness_score has
 * a NOT NULL DB constraint — null cannot be seeded. The null guard in
 * FreshnessScoreCard is verified at the source/unit level; this seeds a
 * low score (0.15 = very stale) to exercise the red/poor rendering path.
 */
function seedFreshnessForShow(siteId: number): { recPublicId: string } {
  // Remove any previous freshness data first.
  runPhp(`
$site = \\App\\Models\\Site::find(${siteId});
if (!$site) return;
\\App\\Models\\PageDecayRun::where('site_id', $site->id)->each(function ($run) {
  \\App\\Models\\FreshnessRecommendation::where('page_decay_run_id', $run->id)->each(function ($rec) {
    \\App\\Models\\PageDecaySignal::where('freshness_recommendation_id', $rec->id)->delete();
    $rec->delete();
  });
  $run->delete();
});
echo "cleared";
  `);

  const raw = runPhp(`
$site = \\App\\Models\\Site::find(${siteId});
if (!$site) { echo json_encode(['error' => 'site_not_found']); return; }

$run = \\App\\Models\\PageDecayRun::factory()->completed()->create([
  'site_id' => $site->id,
  'analysis_window_start' => now()->subDays(90)->toDateString(),
  'analysis_window_end' => now()->subDays(1)->toDateString(),
  'summary' => ['pages_stale' => 1],
]);

$sig = \\App\\Models\\PageDecaySignal::factory()->create([
  'page_decay_run_id' => $run->id,
  'page_url' => 'https://e2e-show-test.example.com/test-page',
  'page_url_hash' => hash('sha256', 'https://e2e-show-test.example.com/test-page'),
  'baseline_28d_clicks' => 100.0,
  'current_clicks' => 25.0,
  'decay_magnitude' => 75.0,
  'freshness_score' => 0.1500,
]);

$rec = \\App\\Models\\FreshnessRecommendation::factory()->create([
  'page_decay_run_id' => $run->id,
  'page_decay_signal_id' => $sig->id,
  'page_url' => 'https://e2e-show-test.example.com/test-page',
  'page_url_hash' => hash('sha256', 'https://e2e-show-test.example.com/test-page'),
  'action_type' => \\App\\Enums\\ActionType::FullRewrite,
  'confidence_score' => 78.0,
  'impact_score' => 8.0,
  'effort_score' => 6.0,
  'urgency_score' => 9.0,
  'freshness_score' => 0.1500,
  'title' => 'E2E Show Test: urgent rewrite needed',
  'reasoning' => 'Traffic dropped 75%. Page urgently needs a content update.',
]);

echo json_encode(['rec_public_id' => $rec->public_id]);
  `);

  const jsonLine = raw
    .trim()
    .split('\n')
    .reverse()
    .find((l) => l.trim().startsWith('{'));
  if (!jsonLine) throw new Error(`seedFreshnessForShow: no JSON. Raw:\n${raw}`);
  const p = JSON.parse(jsonLine) as { error?: string; rec_public_id?: string };
  if (p.error) throw new Error(`seedFreshnessForShow: ${p.error}`);
  return { recPublicId: p.rec_public_id! };
}

// ---------------------------------------------------------------------------
// Suite 1: OnboardingWizard — "Show setup key" button label
// ---------------------------------------------------------------------------

let sitePublicId = '';

test.describe('Session fdd9585f: WP connect button label — "Show setup key"', () => {
  test.beforeAll(async ({ seedData }: { seedData: E2ESeedData }) => {
    sitePublicId = resolveSitePublicId(seedData.siteId);
    // Remove WP connection so the wizard shows the unconnected state where
    // the "Show setup key" button is rendered.
    clearWpConnection(seedData.siteId);
  });

  test('onboarding wizard WP card shows "Show setup key" button (not "Connect WordPress")', 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(`/sites/${sitePublicId}/onboarding`, { waitUntil: 'networkidle' });
    await expect(page).toHaveURL(new RegExp(`/sites/${sitePublicId}/onboarding`));

    // The WordPress Plugin card must show "Show setup key" — the button
    // that mints a setup_token for the user to paste into the WP plugin.
    // If the old label "Connect WordPress" appears, the rename regressed.
    const showSetupKeyBtn = page.getByRole('button', { name: /show setup key/i });
    await expect(showSetupKeyBtn).toBeVisible();

    // Sanity: the old label must NOT appear — guard against partial renames.
    await expect(page.getByRole('button', { name: /^connect wordpress$/i })).not.toBeVisible();

    // The explanatory paragraph above the button must be visible.
    await expect(
      page.getByText(/generate a one-time setup key/i),
    ).toBeVisible();

    // Exclude pre-existing 401/419s from background analytics/timezone fetch() calls
    // that omit credentials in test sessions — not introduced by this session.
    const appErrors = errors.filter(
      (e) =>
        !e.includes('extension') &&
        !e.includes('chrome-extension') &&
        !e.includes('419') &&
        !e.includes('401'),
    );
    expect(appErrors, `Console errors:\n${appErrors.join('\n')}`).toHaveLength(0);
  });

  test('PluginInstallHelper step 5 says "Show setup key" (not stale URL instruction)', async ({
    page,
  }: {
    page: import('@playwright/test').Page;
  }) => {
    await page.goto(`/sites/${sitePublicId}/onboarding`, { waitUntil: 'networkidle' });

    // Open the collapsible install helper
    const helperToggle = page.getByRole('button', { name: /need to install the plugin first/i });
    await expect(helperToggle).toBeVisible();
    await helperToggle.click();

    // Step 5 must reference "Show setup key" — not the old "enter your WordPress URL" instruction.
    await expect(page.getByText(/show setup key/i).last()).toBeVisible();
  });

  test('zero uncaught page errors on onboarding WP-unconnected state', async ({
    page,
  }: {
    page: import('@playwright/test').Page;
  }) => {
    const errors: string[] = [];
    page.on('pageerror', (err) => errors.push(err.message));

    await page.goto(`/sites/${sitePublicId}/onboarding`, { waitUntil: 'networkidle' });

    // No React or JS crashes
    expect(errors, `Uncaught page errors:\n${errors.join('\n')}`).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// Suite 2: Freshness Show — page renders without crash, FreshnessScoreCard visible
//
// NOTE on beforeEach vs beforeAll: we use test.describe.serial() to force
// sequential execution, avoiding the fullyParallel-mode issue where both
// suites' beforeAll calls could interleave and overwrite recPublicId.
// The seeding is in beforeEach so each test always gets its own fresh URL.
// ---------------------------------------------------------------------------

test.describe.serial('Session fdd9585f: Freshness Show — FreshnessScoreCard renders without crash', () => {
  let localSitePublicId = '';
  let localRecPublicId = '';

  test.beforeEach(async ({ seedData }: { seedData: E2ESeedData }) => {
    localSitePublicId = resolveSitePublicId(seedData.siteId);
    const result = seedFreshnessForShow(seedData.siteId);
    localRecPublicId = result.recPublicId;
  });

  test('Freshness Show page renders FreshnessScoreCard without crash (low score path)', async ({
    page,
  }: {
    page: import('@playwright/test').Page;
  }) => {
    const errors: string[] = [];
    const pageErrors: string[] = [];
    page.on('pageerror', (err) => pageErrors.push(err.message));
    page.on('console', (msg) => {
      if (msg.type() === 'error') errors.push(msg.text());
    });

    const showUrl = `/sites/${localSitePublicId}/content-intelligence/freshness/${localRecPublicId}`;
    await page.goto(showUrl, { waitUntil: 'networkidle' });
    await expect(page).toHaveURL(new RegExp(showUrl.replace(/\//g, '\\/')));

    // FINDING: This assertion captures whether the page crashed.
    // If the error boundary "hit an unexpected error" is visible, the test reports
    // it with the console error details as evidence.
    const errorBoundaryVisible = await page.getByRole('heading', { name: /hit an unexpected error/i }).isVisible();
    if (errorBoundaryVisible) {
      const allErrors = [...pageErrors, ...errors].join('\n');
      throw new Error(
        `FreshnessShow crashed into error boundary.\n` +
        `URL: ${showUrl}\n` +
        `Console errors: ${allErrors || '(none captured — may be in Error object not string)'}\n` +
        `NOTE: This is a genuine browser crash (not a test setup issue). The error boundary\n` +
        `      fires for a freshness_score=0.15 recommendation render. This likely indicates\n` +
        `      a React TypeError in DetailPageHeader readouts or FreshnessScoreCard rendering\n` +
        `      that the fix in commit f5ddaf8c did not fully resolve.`
      );
    }

    // FreshnessScoreCard must render — the card label paragraph is always present.
    // Use first() to disambiguate from the <dt> "Freshness Score" in the Decay Analysis dl.
    await expect(page.getByRole('paragraph').filter({ hasText: 'Freshness Score' }).first()).toBeVisible();
    // Rec title confirms the page rendered the correct data (match the h1, not the breadcrumb span).
    await expect(page.getByRole('heading', { name: /E2E Show Test: urgent rewrite needed/i })).toBeVisible();
  });

  test('Freshness Show zero failed XHR on detail page load', async ({
    page,
  }: {
    page: import('@playwright/test').Page;
  }) => {
    const failedRequests: string[] = [];
    const knownPreexisting = ['/api/lead-score/pql-signal', '/profile'];
    page.on('response', (r) => {
      if (
        r.status() >= 400 &&
        !knownPreexisting.some((p) => r.url().includes(p))
      ) {
        failedRequests.push(`${r.status()} ${r.url()}`);
      }
    });

    const showUrl = `/sites/${localSitePublicId}/content-intelligence/freshness/${localRecPublicId}`;
    await page.goto(showUrl, { waitUntil: 'networkidle' });

    expect(
      failedRequests,
      `Failed HTTP requests:\n${failedRequests.join('\n')}`,
    ).toHaveLength(0);
  });
});
