/**
 * Workflow Verification: TimeWindowPicker + Analysis Error Messages
 * Session: 585fcb23-15a3-4872-a68c-1ea7c05e62b7
 *
 * Golden path and sad-path exercise for:
 *   - resources/js/Components/Analysis/TimeWindowPicker.tsx
 *   - resources/js/lib/errorMessages.ts (consumed by Analyze/Index.tsx on failed runs)
 *
 * Scope derived from diff (no SUCCESS_CRITERIA / WORKFLOW_BLAST_RADIUS files present).
 *
 * === Workflows verified ===
 * A. Golden path (preset): TimeWindowPicker with preset (30d) fills dates and
 *    shows "Analyze this period" button correctly.
 * B. Golden path (custom): Custom mode exposes 4 date inputs; filling them
 *    shows the submit button with the date range summary.
 * C. Empty state — no GSC data: picker renders the "No GSC data" banner, not blank.
 * D. Error state — failed analysis run: friendly error message from errorMessages.ts
 *    is rendered (not a raw exception / empty screen).
 * E. Permission denied: unauthenticated access to analyze page redirects to /login.
 *
 * === public_id routing ===
 * Site model uses HasPublicId — URL uses ULID (public_id), not integer PK.
 * Resolved via artisan tinker before the suite.
 */

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

// ESM __dirname shim (Playwright tests run as ES modules)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ---------------------------------------------------------------------------
// Known environmental noise — pre-existing, not introduced by this session
// ---------------------------------------------------------------------------
const KNOWN_NOISE = [
  '/api/lead-score/pql-signal',
  '/profile',
  'ERR_TOO_MANY_REDIRECTS',
  'favicon',
  'chrome-extension',
  'extension',
  'analytics',
  'pql-signal',
];

// ---------------------------------------------------------------------------
// PHP execution helper (avoids shell-quoting issues)
// ---------------------------------------------------------------------------
function runPhp(phpCode: string): string {
  const tmpFile = path.join('/tmp', `e2e_twp_${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;
}

/** Force analysis run into 'failed' state with a given error string. */
function setAnalysisRunFailed(analysisRunId: number, errorMessage: string): void {
  runPhp(`
\\App\\Models\\AnalysisRun::where('id', ${analysisRunId})->update([
  'status' => 'failed',
  'error' => '${errorMessage.replace(/'/g, "\\'")}',
  'completed_at' => now()->toDateTimeString(),
]);
echo "ok";
  `);
}

/** Restore analysis run to completed state. */
function setAnalysisRunCompleted(analysisRunId: number): void {
  runPhp(`
\\App\\Models\\AnalysisRun::where('id', ${analysisRunId})->update([
  'status' => 'completed',
  'error' => null,
  'completed_at' => now()->toDateTimeString(),
]);
echo "ok";
  `);
}

/** Temporarily remove GSC daily metrics so gsc_data_range returns null. */
function clearGscMetrics(siteId: number): void {
  runPhp(`
DB::table('gsc_daily_metrics')->where('site_id', ${siteId})->delete();
DB::table('gsc_page_metrics')->where('site_id', ${siteId})->delete();
echo "cleared";
  `);
}

/** Re-insert 60 days of GSC daily metrics for the site. */
function restoreGscMetrics(siteId: number): void {
  runPhp(`
$rows = [];
$now = now()->toDateTimeString();
for ($d = 60; $d >= 1; $d--) {
  $date = now()->subDays($d)->toDateString();
  $rows[] = [
    'site_id' => ${siteId},
    'date' => $date,
    'dimension_type' => 'overall',
    'dimension_value' => '',
    'clicks' => 50,
    'impressions' => 700,
    'ctr' => 0.071429,
    'position' => 8.5,
    'created_at' => $now,
  ];
}
foreach (array_chunk($rows, 30) as $chunk) {
  DB::table('gsc_daily_metrics')->insert($chunk);
}
echo "restored";
  `);
}

// ---------------------------------------------------------------------------
// Suite-level state
// ---------------------------------------------------------------------------
let sitePublicId: string;
let seedData: E2ESeedData;

test.beforeAll(() => {
  const seedPath = path.join(__dirname, '..', '.seed-data.json');
  if (!fs.existsSync(seedPath)) {
    throw new Error('Seed data missing — run global setup first');
  }
  seedData = JSON.parse(fs.readFileSync(seedPath, 'utf8')) as E2ESeedData;
  sitePublicId = resolveSitePublicId(seedData.siteId);
});

// ─── A. Golden path — preset time window ────────────────────────────────────
test('A: preset 30d button fills dates and shows Analyze button', async ({ page }) => {
  const consoleErrors: string[] = [];
  const failedRequests: string[] = [];

  page.on('console', (msg) => {
    if (msg.type() === 'error' && !KNOWN_NOISE.some((n) => msg.text().includes(n))) {
      consoleErrors.push(msg.text());
    }
  });
  page.on('requestfailed', (req) => {
    const url = req.url();
    if (!KNOWN_NOISE.some((n) => url.includes(n))) {
      failedRequests.push(url);
    }
  });

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

  // TimeWindowPicker preset buttons must be visible
  await expect(page.getByRole('button', { name: 'Last 30d vs Prior 30d' })).toBeVisible();
  await expect(page.getByRole('button', { name: 'Last 90d vs Prior 90d' })).toBeVisible();
  await expect(page.getByRole('button', { name: 'Custom' })).toBeVisible();

  // Click preset → date summary + Analyze button appear
  await page.getByRole('button', { name: 'Last 30d vs Prior 30d' }).click();

  await expect(page.getByRole('button', { name: /Analyze this period/i })).toBeVisible({
    timeout: 5_000,
  });
  await expect(page.locator('text=Selected:')).toBeVisible();

  const analyzeBtn = page.getByRole('button', { name: /Analyze this period/i });
  await expect(analyzeBtn).toBeEnabled();

  expect(consoleErrors).toHaveLength(0);
  expect(failedRequests).toHaveLength(0);
});

// ─── B. Golden path — custom time window ────────────────────────────────────
test('B: custom mode shows 4 date inputs and submit button when all filled', async ({ page }) => {
  const consoleErrors: string[] = [];
  const failedRequests: string[] = [];

  page.on('console', (msg) => {
    if (msg.type() === 'error' && !KNOWN_NOISE.some((n) => msg.text().includes(n))) {
      consoleErrors.push(msg.text());
    }
  });
  page.on('requestfailed', (req) => {
    const url = req.url();
    if (!KNOWN_NOISE.some((n) => url.includes(n))) {
      failedRequests.push(url);
    }
  });

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

  await page.getByRole('button', { name: 'Custom' }).click();

  // 4 labeled date inputs should appear
  await expect(page.getByLabel('Before Start')).toBeVisible();
  await expect(page.getByLabel('Before End')).toBeVisible();
  await expect(page.getByLabel('After Start')).toBeVisible();
  await expect(page.getByLabel('After End')).toBeVisible();

  // Fill valid custom dates
  const today = new Date();
  const fmt = (d: Date) => d.toISOString().split('T')[0];
  const afterEnd    = fmt(new Date(today.getTime() - 1  * 86400000));
  const afterStart  = fmt(new Date(today.getTime() - 30 * 86400000));
  const beforeEnd   = fmt(new Date(today.getTime() - 31 * 86400000));
  const beforeStart = fmt(new Date(today.getTime() - 60 * 86400000));

  await page.getByLabel('Before Start').fill(beforeStart);
  await page.getByLabel('Before End').fill(beforeEnd);
  await page.getByLabel('After Start').fill(afterStart);
  await page.getByLabel('After End').fill(afterEnd);

  // Submit button appears once all 4 dates are set
  await expect(page.getByRole('button', { name: /Analyze this period/i })).toBeVisible({
    timeout: 5_000,
  });
  await expect(page.locator('text=Selected:')).toBeVisible();

  expect(consoleErrors).toHaveLength(0);
  expect(failedRequests).toHaveLength(0);
});

// ─── C. Empty state — no GSC data ────────────────────────────────────────────
test('C: no GSC data — renders "No GSC data" banner, not blank', async ({ page }) => {
  const consoleErrors: string[] = [];
  const failedRequests: string[] = [];

  page.on('console', (msg) => {
    if (msg.type() === 'error' && !KNOWN_NOISE.some((n) => msg.text().includes(n))) {
      consoleErrors.push(msg.text());
    }
  });
  page.on('requestfailed', (req) => {
    const url = req.url();
    if (!KNOWN_NOISE.some((n) => url.includes(n))) {
      failedRequests.push(url);
    }
  });

  clearGscMetrics(seedData.siteId);

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

    // TimeWindowPicker empty-state branch
    await expect(
      page.getByText('No GSC data available yet')
    ).toBeVisible({ timeout: 8_000 });

    // Preset buttons hidden in empty-state
    await expect(
      page.getByRole('button', { name: 'Last 30d vs Prior 30d' })
    ).not.toBeVisible();

    expect(consoleErrors).toHaveLength(0);
    expect(failedRequests).toHaveLength(0);
  } finally {
    restoreGscMetrics(seedData.siteId);
  }
});

// ─── D. Error state — failed analysis with friendly message ──────────────────
test('D: failed analysis run shows friendly error from errorMessages.ts', async ({ page }) => {
  const consoleErrors: string[] = [];
  const failedRequests: string[] = [];

  page.on('console', (msg) => {
    if (msg.type() === 'error' && !KNOWN_NOISE.some((n) => msg.text().includes(n))) {
      consoleErrors.push(msg.text());
    }
  });
  page.on('requestfailed', (req) => {
    const url = req.url();
    if (!KNOWN_NOISE.some((n) => url.includes(n))) {
      failedRequests.push(url);
    }
  });

  setAnalysisRunFailed(seedData.analysisRunId, 'GSC quota exceeded: 429 Too Many Requests');

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

    // "Analysis failed" section header (exact match avoids the sr-only aria-live variant)
    await expect(page.getByText('Analysis failed', { exact: true })).toBeVisible({
      timeout: 8_000,
    });

    // errorMessages.ts maps quota/429 → 'Google Search Console Limit Reached'
    await expect(
      page.getByText('Google Search Console Limit Reached')
    ).toBeVisible({ timeout: 5_000 });

    // User-friendly description
    await expect(page.getByText(/daily GSC quota has been reached/i)).toBeVisible();

    // Retry button present
    await expect(page.getByRole('button', { name: /Retry Analysis/i })).toBeVisible();

    // No raw exception / stack trace
    await expect(page.getByText(/Illuminate\\|stack trace/i)).not.toBeVisible();

    expect(consoleErrors).toHaveLength(0);
    expect(failedRequests).toHaveLength(0);
  } finally {
    setAnalysisRunCompleted(seedData.analysisRunId);
  }
});

// ─── E. Permission denied — unauthenticated ───────────────────────────────────
test('E: unauthenticated access redirects to /login', async ({ browser }) => {
  const ctx = await browser.newContext({ storageState: undefined });
  const page = await ctx.newPage();

  const failedRequests: string[] = [];
  page.on('requestfailed', (req) => {
    const url = req.url();
    if (!KNOWN_NOISE.some((n) => url.includes(n))) {
      failedRequests.push(url);
    }
  });

  try {
    await page.goto(`/sites/${sitePublicId}/analyze`, { waitUntil: 'networkidle' });
    await expect(page).toHaveURL(/\/login/, { timeout: 8_000 });
    await expect(page.getByLabel(/email/i)).toBeVisible();
    expect(failedRequests).toHaveLength(0);
  } finally {
    await ctx.close();
  }
});
