/**
 * Workflow Verification: Connection State UI (INTUX-101)
 * Session: 02e525ef-473e-419b-8df8-66f855c75876
 *
 * Changed UI files:
 *   - ConnectionStatusBadge.tsx  (auth_failed/unreachable/expired → destructive)
 *   - WpConnectCard.tsx          (WP-001: auth_failed/unreachable → destructive card) [DELETED in GSC-NEW-01]
 *   - GscConnectCard.tsx         (GSC-02: failed/expired → re-auth recovery UI) [DELETED in GSC-NEW-01]
 *   - OnboardingWizard.tsx       (wizard branches on degraded WP states)
 *   - DashboardBanners.tsx       (new wp-degraded banner at connection priority)
 *   - Dashboard.tsx              (accepts wp_degraded_connections prop)
 *
 * Scope derived from diff (no SUCCESS_CRITERIA / WORKFLOW_BLAST_RADIUS files present).
 *
 * States exercised:
 *   A. Golden path: onboarding page renders; WP "Show setup key" CTA visible
 *   B. WP auth_failed: destructive badge + "Reconnect WordPress" CTA (NEVER green)
 *   C. WP unreachable: destructive badge + "Reconnect WordPress" CTA (NEVER green)
 *   D. GSC expired: "Token Expired" badge + inline re-auth recovery visible
 *   E. Dashboard WP degraded banner: "WordPress connection broken" alert appears
 *   F. Permission denied: unauthenticated → redirected to login
 *
 * Self-contained: handles its own login (no global-setup dependency to avoid
 * the known 419 CSRF issue when APP_URL ≠ baseURL).
 *
 * === public_id routing ===
 * User-facing routes use HasPublicId (ULID). Site public_id is resolved
 * via PHP tinker after E2E seed runs.
 */

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { expect, test } from '@playwright/test';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ---------------------------------------------------------------------------
// Known background noise (pre-existing, not introduced by INTUX-101)
// ---------------------------------------------------------------------------
const KNOWN_NOISE = [
  '/api/lead-score/pql-signal',
  '/profile',
  'ERR_TOO_MANY_REDIRECTS',
  'favicon',
  'chrome-extension',
  'extension',
  '/_debugbar',
  '/sanctum/csrf-cookie',
];

function isKnownNoise(url: string, errorText: string): boolean {
  return KNOWN_NOISE.some((n) => url.includes(n) || errorText.includes(n));
}

// ---------------------------------------------------------------------------
// PHP execution helper (temp-file pattern from existing E2E conventions)
// ---------------------------------------------------------------------------

// Prefer Herd's PHP 8.4 binary (required by this project). Fall back to the
// system `php` if Herd's binary is absent (CI / non-macOS Herd environments).
const HERD_PHP84 = '/Users/sood/Library/Application Support/Herd/bin/php84';
const PHP_BIN: string = (() => {
  try {
    if (fs.existsSync(HERD_PHP84)) return HERD_PHP84;
  } catch { /* ignore */ }
  return 'php';
})();

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

// ---------------------------------------------------------------------------
// Resolve site public_id from seed data
// ---------------------------------------------------------------------------
let SITE_PUBLIC_ID: string;

function getSitePublicId(): string {
  if (SITE_PUBLIC_ID) return SITE_PUBLIC_ID;
  const result = runPhp(
    `$site = \\App\\Models\\Site::first(); echo $site ? $site->public_id : 'NOT_FOUND';`,
  );
  const pid = result.trim().split('\n').pop()?.trim() ?? '';
  if (!pid || pid === 'NOT_FOUND') {
    throw new Error(`Could not resolve site public_id. Tinker output: ${result}`);
  }
  SITE_PUBLIC_ID = pid;
  return SITE_PUBLIC_ID;
}

// ---------------------------------------------------------------------------
// Login helper (self-contained — no global-setup dependency)
// ---------------------------------------------------------------------------
async function loginAsE2eUser(page: import('@playwright/test').Page, baseURL: string) {
  await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' });
  await page.locator('input[name="email"]').fill('e2e-test@rankwiz.test');
  await page.locator('input[name="password"]').fill('E2ePassword123!');
  await page.locator('button[type="submit"]').click();
  // Accept any post-login destination that is NOT the login page
  await page.waitForURL((url) => !url.pathname.startsWith('/login'), { timeout: 15_000 });
  // Dismiss cookie consent so the banner never blocks role queries
  await page.context().addCookies([
    {
      name: 'cookie_consent',
      value: 'accepted',
      domain: new URL(baseURL).hostname,
      path: '/',
      sameSite: 'Lax',
      httpOnly: false,
      secure: false,
    },
  ]);
}

// ---------------------------------------------------------------------------
// DB helpers: inject / clear degraded WP states
// ---------------------------------------------------------------------------
function setWpConnectionStatus(siteId: number, status: string) {
  // The E2E seed always creates a WpConnection for the site; use update() to avoid
  // the shared_secret NOT NULL constraint when re-creating from scratch.
  runPhp(`
    \\App\\Models\\WpConnection::where('site_id', ${siteId})
      ->update(['status' => '${status}', 'setup_token' => null]);
  `);
}

function restoreWpConnectionToConnected(siteId: number) {
  runPhp(`
    $conn = \\App\\Models\\WpConnection::where('site_id', ${siteId})->first();
    if ($conn) {
      $conn->status = 'connected';
      $conn->setup_token = null;
      $conn->save();
    }
  `);
}

function _deleteWpConnection(siteId: number) {
  runPhp(`\\App\\Models\\WpConnection::where('site_id', ${siteId})->delete();`);
}

function setGscConnectionStatus(siteId: number, status: string) {
  // The E2E seed always creates a GscConnection for the site; use update() to avoid
  // constraints when re-creating from scratch.
  runPhp(`
    \\App\\Models\\GscConnection::where('site_id', ${siteId})
      ->update(['sync_status' => '${status}']);
  `);
}

// ============================================================================
// Tests
// ============================================================================

test.describe('INTUX-101 — Connection State UI', () => {
  let baseURL: string;
  let sitePublicId: string;
  let siteId: number;
  const consoleErrors: string[] = [];
  const failedRequests: string[] = [];

  test.beforeAll(() => {
    sitePublicId = getSitePublicId();
    // Get integer siteId for DB manipulation
    const result = runPhp(`$site = \\App\\Models\\Site::where('public_id', '${sitePublicId}')->first(); echo $site ? $site->id : '0';`);
    siteId = parseInt(result.trim().split('\n').pop()?.trim() ?? '0', 10);
    if (!siteId) throw new Error('Could not resolve siteId integer');
  });

  test.beforeEach(async ({ page }, testInfo) => {
    baseURL = testInfo.project.use.baseURL ?? 'http://127.0.0.1:8212';
    consoleErrors.length = 0;
    failedRequests.length = 0;

    // Capture console errors
    page.on('console', (msg) => {
      if (msg.type() === 'error' && !isKnownNoise(msg.location().url ?? '', msg.text())) {
        consoleErrors.push(`[console.error] ${msg.text()}`);
      }
    });

    // Capture failed XHR/fetch
    page.on('requestfailed', (req) => {
      const url = req.url();
      if (!isKnownNoise(url, req.failure()?.errorText ?? '')) {
        failedRequests.push(`${req.method()} ${url} — ${req.failure()?.errorText}`);
      }
    });

    page.on('response', (res) => {
      if (res.status() >= 400 && res.url().includes('/api/') && !isKnownNoise(res.url(), '')) {
        failedRequests.push(`${res.request().method()} ${res.url()} → ${res.status()}`);
      }
    });

    await loginAsE2eUser(page, baseURL);
  });

  // ── A: Golden path ─────────────────────────────────────────────────────────
  // The E2E seed creates a completed analysis run + recommendation AND a WP
  // connection with status='connected'. In step3Complete=true + wpConnection='connected'
  // state, the wizard shows the default full WP card with "Connected: <url>".
  test('A — golden path: onboarding page renders with connected WP and GSC states', async ({ page }) => {
    // WP connection is 'connected' from the seed (restored by afterEach for subsequent tests)
    await page.goto(`${baseURL}/sites/${sitePublicId}/onboarding`, { waitUntil: 'networkidle' });

    // Page title / heading visible
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();

    // WordPress Plugin card is visible
    await expect(page.getByRole('heading', { name: 'WordPress Plugin', level: 2 })).toBeVisible();

    // Connected WP shows "Connected: <url>" — not a destructive error card
    await expect(page.getByText(/Connected: https:\/\/e2e-test\.example\.com/)).toBeVisible();

    // Google Search Console card visible and shows "Connected" (GSC-specific URL)
    await expect(page.getByRole('heading', { name: 'Google Search Console', level: 2 })).toBeVisible();
    await expect(page.getByText(/Connected: sc-domain:/)).toBeVisible();

    // No uncaught console errors
    expect(consoleErrors, `Console errors: ${consoleErrors.join(', ')}`).toHaveLength(0);
    // No failed XHR (excluding known background noise)
    expect(failedRequests, `Failed requests: ${failedRequests.join(', ')}`).toHaveLength(0);
  });

  // ── B: WP auth_failed — must show destructive, never green ────────────────
  // OnboardingWizard's degraded WP branch renders "WordPress authentication failed"
  // inline (NOT via ConnectionStatusBadge which shows "Auth Failed"). The degraded
  // branch is triggered when wpConnection.status === 'auth_failed'.
  test('B — WP auth_failed: destructive error copy + Reconnect CTA visible (not green)', async ({ page }) => {
    setWpConnectionStatus(siteId, 'auth_failed');

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

    // INTUX-101 WP-001: OnboardingWizard degraded branch shows this copy
    await expect(page.getByText('WordPress authentication failed')).toBeVisible();
    await expect(page.getByText('AI draft publishing is disabled')).toBeVisible();

    // "Reconnect WordPress" must be visible as the recovery CTA
    const reconnectBtn = page.getByRole('button', { name: 'Reconnect WordPress' });
    await expect(reconnectBtn).toBeVisible();

    // "Connected: https://e2e-test.example.com" must NOT be visible for WP — a broken
    // connection must not read as healthy. Note: GSC may still show "Connected: sc-domain:..."
    // so we check for the WP-specific URL pattern.
    await expect(page.getByText('Connected: https://e2e-test.example.com')).not.toBeVisible();

    expect(consoleErrors, `Console errors: ${consoleErrors.join(', ')}`).toHaveLength(0);
    expect(failedRequests, `Failed requests: ${failedRequests.join(', ')}`).toHaveLength(0);
  });

  // ── C: WP unreachable — must show destructive, never green ─────────────────
  // OnboardingWizard's degraded WP branch also covers 'unreachable'. The degraded
  // branch text differs slightly from auth_failed ("WordPress site is unreachable").
  test('C — WP unreachable: destructive error copy + Reconnect CTA visible (not green)', async ({ page }) => {
    setWpConnectionStatus(siteId, 'unreachable');

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

    // Unreachable-specific copy rendered by OnboardingWizard degraded branch
    await expect(page.getByText('WordPress site is unreachable')).toBeVisible();

    // Recovery CTA visible
    const reconnectBtn = page.getByRole('button', { name: 'Reconnect WordPress' });
    await expect(reconnectBtn).toBeVisible();

    // "Connected: https://e2e-test.example.com" must NOT be visible for WP
    // (GSC still shows "Connected: sc-domain:..." which is correct — we only
    // check the WP-specific URL)
    await expect(page.getByText('Connected: https://e2e-test.example.com')).not.toBeVisible();

    expect(consoleErrors, `Console errors: ${consoleErrors.join(', ')}`).toHaveLength(0);
    expect(failedRequests, `Failed requests: ${failedRequests.join(', ')}`).toHaveLength(0);
  });

  // ── D: GSC expired — re-auth recovery visible (not syncing UI) ─────────────
  // GscFailureCard renders for expired state. The OnboardingWizard routes expired
  // status to GscFailureCard with forceReconnect=true, showing "Reconnect Google"
  // link and the expired-specific copy about reconnecting to resume syncing.
  test('D — GSC expired: inline re-auth recovery visible (not syncing spinner)', async ({ page }) => {
    setGscConnectionStatus(siteId, 'expired');

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

    // INTUX-101 GSC-02: expired must show GscFailureCard recovery UI, not syncing spinner
    // GscFailureCard renders "We couldn't sync your Search Console data." and
    // expired-specific copy ("Your Google connection expired").
    await expect(page.getByText("We couldn't sync your Search Console data.")).toBeVisible();

    // Expired-specific error description
    await expect(page.getByText(/Your Google connection expired/i)).toBeVisible();

    // "Reconnect Google" CTA (forceReconnect=true for expired state)
    await expect(page.getByRole('link', { name: /reconnect google/i })).toBeVisible();

    // Syncing copy must NOT appear (expired ≠ syncing)
    await expect(page.getByText(/Syncing your search data/i)).not.toBeVisible();

    expect(consoleErrors, `Console errors: ${consoleErrors.join(', ')}`).toHaveLength(0);
    expect(failedRequests, `Failed requests: ${failedRequests.join(', ')}`).toHaveLength(0);
  });

  // ── E: Dashboard WP degraded banner ────────────────────────────────────────
  // FINDING (INTUX-101-E2E-001): The `wp_degraded_connections` Inertia prop is
  // absent from the initial Dashboard page load even when the DashboardController
  // code correctly computes it. The DB manipulation succeeds (WP status set to
  // auth_failed) and the PHP controller query returns the correct data, but the
  // prop does not appear in the `data-page` attribute in the HTML response.
  // DashboardBanners unit tests pass (component correctly renders when the prop
  // is supplied), so the bug is in the prop not reaching the component.
  // This test documents the DESIRED behavior as a regression gate for when the
  // root cause is fixed. It is currently expected to fail.
  test('E — Dashboard: WP degraded banner appears for auth_failed connection', async ({ page }) => {
    setWpConnectionStatus(siteId, 'auth_failed');

    await page.goto(`${baseURL}/dashboard`, { waitUntil: 'networkidle' });

    // Verify the DB state is correct (smoke check for the test setup itself)
    const wpStatusAfterSet = runPhp(
      `$w = \\App\\Models\\WpConnection::where('site_id', ${siteId})->first(); echo $w ? $w->status : 'NOT_FOUND';`,
    ).trim().split('\n').pop()?.trim();
    expect(wpStatusAfterSet, 'WP connection must be auth_failed in DB before testing banner').toBe('auth_failed');

    // INTUX-101 DashboardBanners: "WordPress connection broken" alert must appear.
    // FINDING: This assertion fails because wp_degraded_connections is absent from
    // the Inertia initial page props even though DashboardController emits it.
    // The banner IS implemented in DashboardBanners.tsx and tested in unit tests.
    await expect(page.getByText(/WordPress connection broken/i)).toBeVisible();

    // "Reconnect WordPress" CTA in the banner
    await expect(page.getByRole('link', { name: 'Reconnect WordPress' })).toBeVisible();

    expect(consoleErrors, `Console errors: ${consoleErrors.join(', ')}`).toHaveLength(0);
    expect(failedRequests, `Failed requests: ${failedRequests.join(', ')}`).toHaveLength(0);
  });

  // ── F: Permission denied — unauthenticated access ──────────────────────────
  test('F — permission denied: unauthenticated access to onboarding redirects to login', async ({ browser }) => {
    // Fresh context with NO auth state
    const freshCtx = await browser.newContext();
    const freshPage = await freshCtx.newPage();

    try {
      await freshPage.goto(
        `http://127.0.0.1:8212/sites/${sitePublicId}/onboarding`,
        { waitUntil: 'domcontentloaded' },
      );

      // Should redirect to /login (no data leak, no 500)
      const finalUrl = freshPage.url();
      expect(finalUrl).toContain('/login');

      // No 500 error
      // The response chain ends at /login (200) after redirect
    } finally {
      await freshCtx.close();
    }
  });

  // Cleanup after each test: restore both connections to healthy seeded state
  test.afterEach(() => {
    try {
      restoreWpConnectionToConnected(siteId);
    } catch { /* best-effort */ }
    try {
      setGscConnectionStatus(siteId, 'synced');
    } catch { /* best-effort */ }
  });
});
