// src/tests/setup/integration-global-setup.ts
import { execSync } from 'child_process';
import fs from 'node:fs/promises';
import path from 'path';
import os from 'os';
import type { Server } from 'http';
import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db';

// --- DEBUG: Log when this file is first loaded/parsed ---
const SETUP_LOAD_TIME = new Date().toISOString();
console.error(`\n[GLOBAL-SETUP-DEBUG] Module loaded at ${SETUP_LOAD_TIME}`);
console.error(`[GLOBAL-SETUP-DEBUG] Current working directory: ${process.cwd()}`);
console.error(`[GLOBAL-SETUP-DEBUG] NODE_ENV: ${process.env.NODE_ENV}`);
console.error(`[GLOBAL-SETUP-DEBUG] __filename: ${import.meta.url}`);

// --- Centralized State for Integration Test Lifecycle ---
let server: Server;
// This will hold the single database pool instance for the entire test run.
let globalPool: ReturnType<typeof getPool> | null = null;
// Temporary directory for test file storage (to avoid modifying committed fixtures)
let tempStorageDir: string | null = null;

/**
 * Cleans all BullMQ queues to ensure no stale jobs from previous test runs.
 * This is critical because old jobs with outdated error messages can pollute test results.
 */
async function cleanAllQueues() {
  // Use console.error for visibility in CI logs (stderr is often more reliable)
  console.error(`[PID:${process.pid}] [QUEUE CLEANUP] Starting BullMQ queue cleanup...`);

  try {
    const {
      flyerQueue,
      cleanupQueue,
      emailQueue,
      analyticsQueue,
      weeklyAnalyticsQueue,
      tokenCleanupQueue,
    } = await import('../../services/queues.server');
    console.error(`[QUEUE CLEANUP] Successfully imported queue modules`);

    const queues = [
      flyerQueue,
      cleanupQueue,
      emailQueue,
      analyticsQueue,
      weeklyAnalyticsQueue,
      tokenCleanupQueue,
    ];

    for (const queue of queues) {
      try {
        // Log queue state before cleanup
        const jobCounts = await queue.getJobCounts();
        console.error(
          `[QUEUE CLEANUP] Queue "${queue.name}" before cleanup: ${JSON.stringify(jobCounts)}`,
        );

        // obliterate() removes ALL data associated with the queue from Redis
        await queue.obliterate({ force: true });
        console.error(`  ✅ [QUEUE CLEANUP] Cleaned queue: ${queue.name}`);
      } catch (error) {
        // Log but don't fail - the queue might not exist yet
        console.error(
          `  ⚠️ [QUEUE CLEANUP] Could not clean queue ${queue.name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
        );
      }
    }
    console.error(`✅ [PID:${process.pid}] [QUEUE CLEANUP] All queues cleaned successfully.`);
  } catch (error) {
    console.error(
      `❌ [PID:${process.pid}] [QUEUE CLEANUP] CRITICAL ERROR during queue cleanup:`,
      error,
    );
    // Don't throw - we want the tests to continue even if cleanup fails
  }
}

export async function setup() {
  console.error(`\n[GLOBAL-SETUP-DEBUG] ========================================`);
  console.error(`[GLOBAL-SETUP-DEBUG] setup() function STARTED at ${new Date().toISOString()}`);
  console.error(`[GLOBAL-SETUP-DEBUG] ========================================`);

  // Ensure we are in the correct environment for these tests.
  process.env.NODE_ENV = 'test';
  // Fix: Set the FRONTEND_URL globally for the test server instance
  process.env.FRONTEND_URL = 'https://example.com';

  // CRITICAL: Create a temporary directory for test file storage.
  // This prevents tests from modifying or deleting committed fixture files.
  // The temp directory is cleaned up in teardown().
  tempStorageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flyer-crawler-test-'));
  const tempFlyerImagesDir = path.join(tempStorageDir, 'flyer-images');
  await fs.mkdir(path.join(tempFlyerImagesDir, 'icons'), { recursive: true });
  console.error(`[SETUP] Created temporary storage directory: ${tempFlyerImagesDir}`);

  // CRITICAL: Set STORAGE_PATH before importing the server.
  // The multer middleware runs an IIFE on import that creates directories based on this path.
  // Using a temp directory ensures test file operations don't affect committed files.
  process.env.STORAGE_PATH = tempFlyerImagesDir;
  console.error(`[SETUP] Set STORAGE_PATH to temporary directory: ${process.env.STORAGE_PATH}`);

  console.error(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Setup ---`);
  console.error(`[SETUP] STORAGE_PATH: ${process.env.STORAGE_PATH}`);
  console.error(`[SETUP] REDIS_URL: ${process.env.REDIS_URL}`);
  console.error(`[SETUP] REDIS_PASSWORD is set: ${!!process.env.REDIS_PASSWORD}`);

  // CRITICAL: Clean all queues BEFORE running any tests to remove stale jobs
  // from previous test runs that may have outdated error messages.
  console.error(`[SETUP] About to call cleanAllQueues()...`);
  await cleanAllQueues();
  console.error(`[SETUP] cleanAllQueues() completed.`);

  // The integration setup is now the single source of truth for preparing the test DB.
  // It runs the same seed script that `npm run db:reset:test` used.
  try {
    console.log(`\n[PID:${process.pid}] Running database seed script...`);
    // Use npx cross-env for Windows compatibility
    execSync('npx cross-env NODE_ENV=test npx tsx src/db/seed.ts', { stdio: 'inherit' });
    console.log(`✅ [PID:${process.pid}] Database seed script finished.`);
  } catch (error) {
    console.error('🔴 Failed to reset and seed the test database. Aborting tests.', error);
    process.exit(1);
  }

  // Initialize the global pool instance once.
  console.log(`[PID:${process.pid}] Initializing global database pool...`);
  globalPool = getPool();

  // Fix: Dynamic import AFTER env vars are set
  console.error(`[GLOBAL-SETUP-DEBUG] About to import server module...`);
  const appModule = await import('../../../server');
  console.error(`[GLOBAL-SETUP-DEBUG] Server module imported successfully`);
  const app = appModule.default;
  console.error(`[GLOBAL-SETUP-DEBUG] App object type: ${typeof app}`);

  // Programmatically start the server within the same process.
  // Use a dedicated test port to avoid conflicts with production servers.
  const port = process.env.TEST_PORT || process.env.PORT || 3099;
  console.error(`[GLOBAL-SETUP-DEBUG] Attempting to start server on port ${port}...`);

  await new Promise<void>((resolve, reject) => {
    let settled = false; // Prevent double-resolution race condition
    try {
      server = app.listen(port, () => {
        if (settled) return;
        settled = true;
        console.log(`✅ In-process test server started on port ${port}`);
        console.error(
          `[GLOBAL-SETUP-DEBUG] Server listen callback invoked at ${new Date().toISOString()}`,
        );
        resolve();
      });

      server.on('error', (err: NodeJS.ErrnoException) => {
        if (settled) return;
        settled = true;
        console.error(`[GLOBAL-SETUP-DEBUG] Server error event:`, err.message);
        if (err.code === 'EADDRINUSE') {
          console.error(
            `[GLOBAL-SETUP-DEBUG] Port ${port} is already in use! ` +
              `Set TEST_PORT env var to use a different port.`,
          );
        }
        reject(err);
      });
    } catch (err) {
      if (settled) return;
      settled = true;
      console.error(`[GLOBAL-SETUP-DEBUG] Error during app.listen:`, err);
      reject(err);
    }
  });

  /**
   * A local ping function that pings the test server we just started.
   * Uses the same port that the server was started on to avoid hitting
   * a different server that might be running on the default port.
   */
  const pingTestBackend = async (): Promise<boolean> => {
    // Always ping the port we started on, not what's in env vars
    const pingUrl = `http://localhost:${port}/api/health/ping`;
    console.error(`[GLOBAL-SETUP-DEBUG] Pinging: ${pingUrl}`);
    try {
      const response = await fetch(pingUrl);
      console.error(`[GLOBAL-SETUP-DEBUG] Ping response status: ${response.status}`);
      if (!response.ok) {
        console.error(`[GLOBAL-SETUP-DEBUG] Ping response not OK: ${response.statusText}`);
        return false;
      }
      // The ping endpoint returns JSON: { status: 'success', data: { message: 'pong' } }
      const json = await response.json();
      console.error(`[GLOBAL-SETUP-DEBUG] Ping response JSON:`, JSON.stringify(json));
      return json?.data?.message === 'pong';
    } catch (e) {
      const errMsg = e instanceof Error ? e.message : String(e);
      console.error(`[GLOBAL-SETUP-DEBUG] Ping exception: ${errMsg}`);
      logger.debug({ error: e }, 'Ping failed while waiting for server, this is expected.');
      return false;
    }
  };

  console.error(
    `[GLOBAL-SETUP-DEBUG] Server started, beginning ping loop at ${new Date().toISOString()}`,
  );
  console.error(`[GLOBAL-SETUP-DEBUG] Server address info:`, server.address());

  const maxRetries = 15;
  const retryDelay = 1000;
  for (let i = 0; i < maxRetries; i++) {
    console.error(`[GLOBAL-SETUP-DEBUG] Ping attempt ${i + 1}/${maxRetries}`);
    if (await pingTestBackend()) {
      console.log('✅ Backend server is running and responsive.');
      console.error(
        `[GLOBAL-SETUP-DEBUG] setup() function COMPLETED SUCCESSFULLY at ${new Date().toISOString()}`,
      );
      return;
    }
    console.log(
      `[PID:${process.pid}] Waiting for backend server... (attempt ${i + 1}/${maxRetries})`,
    );
    await new Promise((resolve) => setTimeout(resolve, retryDelay));
  }

  console.error(`[GLOBAL-SETUP-DEBUG] All ${maxRetries} ping attempts failed!`);
  console.error(`[GLOBAL-SETUP-DEBUG] Server listening status: ${server.listening}`);
  console.error(`[GLOBAL-SETUP-DEBUG] Server address: ${JSON.stringify(server.address())}`);

  throw new Error('Backend server failed to start.');
}

export async function teardown() {
  console.log(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Teardown ---`);

  // 1. CRITICAL: Close any workers that might still be running from tests.
  // This ensures all background jobs are stopped before we tear down the server/db.
  // Individual test files should close their own workers, but this is a safety net
  // for cases where tests fail/crash before their afterAll hooks run.
  //
  // NOTE: Importing workers.server.ts creates workers as a side effect.
  // If workers were already imported by a test, this just gets the cached module.
  // If not, we'll create and immediately close them - which is fine.
  try {
    console.log('[TEARDOWN] Attempting to close any running workers...');
    const { closeWorkers } = await import('../../services/workers.server');
    await closeWorkers();
    // Give workers a moment to fully release their Redis connections
    await new Promise((resolve) => setTimeout(resolve, 100));
    console.log('✅ [TEARDOWN] Workers closed successfully.');
  } catch (error) {
    // Workers might not have been imported/started, or already closed
    console.log(
      `[TEARDOWN] Workers cleanup note: ${error instanceof Error ? error.message : 'Not initialized or already closed'}`,
    );
  }

  // 2. Close all queues and the Redis connection to prevent orphaned connections.
  try {
    console.log('[TEARDOWN] Closing queues and Redis connection...');
    const {
      flyerQueue,
      cleanupQueue,
      emailQueue,
      analyticsQueue,
      weeklyAnalyticsQueue,
      tokenCleanupQueue,
    } = await import('../../services/queues.server');
    const { connection } = await import('../../services/redis.server');

    await Promise.all([
      flyerQueue.close(),
      cleanupQueue.close(),
      emailQueue.close(),
      analyticsQueue.close(),
      weeklyAnalyticsQueue.close(),
      tokenCleanupQueue.close(),
    ]);
    await connection.quit();
    console.log('✅ [TEARDOWN] Queues and Redis connection closed.');
  } catch (error) {
    console.error(
      `⚠️ [TEARDOWN] Error closing queues/Redis: ${error instanceof Error ? error.message : String(error)}`,
    );
  }

  // 3. Stop the server to release any resources it's holding.
  if (server) {
    await new Promise<void>((resolve) => server.close(() => resolve()));
    console.log('✅ In-process test server stopped.');
  }

  // 4. Close the single, shared database pool.
  if (globalPool) {
    await globalPool.end();
    console.log('✅ Global database pool teardown complete.');
  }

  // 5. Clean up the temporary storage directory.
  if (tempStorageDir) {
    try {
      await fs.rm(tempStorageDir, { recursive: true, force: true });
      console.log(`✅ Cleaned up temporary storage directory: ${tempStorageDir}`);
    } catch (error) {
      console.error(`⚠️ Warning: Could not clean up temp directory ${tempStorageDir}:`, error);
    }
  }

  // 6. Give async operations a moment to fully settle before Vitest exits.
  await new Promise((resolve) => setTimeout(resolve, 100));
  console.log('✅ [TEARDOWN] Integration test teardown complete.');
}
