import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'path';
import { applyPlaywrightDatabaseEnv } from './src/__tests__/integration/utils/playwrightDatabaseConfig';

function loadPlaywrightEnv(): string | null {
  const candidates = ['.env', '.env.test', '.env.example']
    .map((filename) => path.resolve(__dirname, filename))
    .filter((candidate) => fs.existsSync(candidate));

  const selected = candidates[0] ?? null;
  if (!selected) {
    console.warn(`[Playwright] couldn't find env file: ${path.resolve(__dirname, '.env')}`);
    return null;
  }

  dotenv.config({ path: selected });
  return selected;
}

loadPlaywrightEnv();
const dockerComposeEnvFile = fs.existsSync(path.resolve(__dirname, '.env'))
  ? 'ee/server/.env'
  : 'ee/server/.env.test';

// Playwright runs should be self-contained and must not depend on developer filesystem secrets.
// Playwright runs should be self-contained. Use an isolated filesystem-backed tenant secret store
// (env provider is read-only and cannot persist per-tenant configuration like integrations).
process.env.SECRET_READ_CHAIN = process.env.SECRET_READ_CHAIN || 'filesystem,env';
process.env.SECRET_WRITE_PROVIDER = process.env.SECRET_WRITE_PROVIDER || 'filesystem';
process.env.SECRET_FS_BASE_PATH = process.env.SECRET_FS_BASE_PATH || 'secrets-playwright';
process.env.PLAYWRIGHT_FAKE_GOOGLE_OAUTH = process.env.PLAYWRIGHT_FAKE_GOOGLE_OAUTH || 'true';
process.env.NEXT_PUBLIC_PLAYWRIGHT_FAKE_GOOGLE_OAUTH =
  process.env.NEXT_PUBLIC_PLAYWRIGHT_FAKE_GOOGLE_OAUTH || 'true';
process.env.GOOGLE_OAUTH_CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || 'playwright-google-client-id';
process.env.GOOGLE_OAUTH_CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || 'playwright-google-client-secret';

// Don't set STORAGE_LOCAL_BASE_PATH - we want to use MinIO for tests
// const storageBasePath = path.resolve(__dirname, 'playwright-storage');
// if (!fs.existsSync(storageBasePath)) {
//   fs.mkdirSync(storageBasePath, { recursive: true });
// }
// process.env.STORAGE_LOCAL_BASE_PATH = process.env.STORAGE_LOCAL_BASE_PATH || storageBasePath;

// If Postgres runs in Docker and is published to a different host/port,
// override the Playwright DB connection to hit the direct Postgres port instead
// of PgBouncer.
//
// IMPORTANT: Do not fall back to DB_HOST/DB_PORT from `ee/server/.env` here.
// That file is a developer runtime env and may use non-default ports; Playwright
// should be self-contained and only deviate from defaults when explicitly configured.
const directDbHost =
  process.env.DB_DIRECT_HOST ||
  process.env.PLAYWRIGHT_DB_HOST ||
  process.env.EXPOSE_DB_HOST ||
  'localhost';
const directDbPort =
  process.env.DB_DIRECT_PORT ||
  process.env.PLAYWRIGHT_DB_PORT ||
  process.env.EXPOSE_DB_PORT ||
  '5432';

process.env.DB_DIRECT_HOST = process.env.DB_DIRECT_HOST || directDbHost;
process.env.DB_DIRECT_PORT = process.env.DB_DIRECT_PORT || directDbPort;

process.env.PLAYWRIGHT_DB_HOST = process.env.PLAYWRIGHT_DB_HOST || directDbHost;
process.env.PLAYWRIGHT_DB_PORT = process.env.PLAYWRIGHT_DB_PORT || directDbPort;

// Playwright runs should be deterministic and not inherit a developer's NODE_ENV,
// since it affects auth cookie naming and other runtime branches.
process.env.NODE_ENV = 'test';

function runPortProbe(start: number, span: number, strict: boolean): { success: boolean; port?: number; error?: string } {
const script = `const net = require('net');
const start = Number(process.argv[1]);
const attempts = Number(process.argv[2]);
const strictMode = process.argv[3] === 'strict';
if (!Number.isFinite(start)) {
  console.error('invalid-port');
  process.exit(2);
}
function tryPort(port, attemptsLeft) {
  const server = net.createServer();
  server.unref();
  server.once('error', (err) => {
    if (strictMode || attemptsLeft <= 0) {
      console.error(err && err.code ? err.code : 'EADDRINUSE');
      process.exit(1);
    }
    tryPort(port + 1, attemptsLeft - 1);
  });
  server.listen(port, () => {
    server.close(() => {
      process.stdout.write(String(port));
      process.exit(0);
    });
  });
}
tryPort(start, attempts);
`;

  const result = spawnSync(process.execPath, ['--input-type=commonjs', '-e', script, String(start), String(span), strict ? 'strict' : 'flex'], {
    encoding: 'utf-8',
  });

  if (result.status === 0 && result.stdout) {
    return { success: true, port: Number(result.stdout.trim()) };
  }

  const error = (result.stderr || result.error?.message || '').trim();
  return { success: false, error: error || 'port-probe-failed' };
}

function isPortProbePermissionError(error?: string): boolean {
  if (!error) return false;
  const normalized = error.trim().toUpperCase();
  // Some sandboxed environments forbid binding/listening, causing EPERM/EACCES.
  return normalized.includes('EPERM') || normalized.includes('EACCES');
}

function resolveWebPortSync(): number {
  const preferred = Number(process.env.PLAYWRIGHT_APP_PORT || process.env.APP_PORT || 3300);
  if (process.env.PLAYWRIGHT_APP_PORT_LOCKED === 'true' && process.env.PLAYWRIGHT_APP_PORT) {
    if (!Number.isFinite(preferred)) {
      throw new Error(`Invalid PLAYWRIGHT_APP_PORT value: ${process.env.PLAYWRIGHT_APP_PORT}`);
    }
    return preferred;
  }
  if (process.env.PLAYWRIGHT_APP_PORT) {
    if (!Number.isFinite(preferred)) {
      throw new Error(`Invalid PLAYWRIGHT_APP_PORT value: ${process.env.PLAYWRIGHT_APP_PORT}`);
    }
    const check = runPortProbe(preferred, 0, true);
    if (!check.success || typeof check.port !== 'number') {
      if (isPortProbePermissionError(check.error)) {
        console.warn(`[Playwright] Port probe blocked (${check.error}); using PLAYWRIGHT_APP_PORT=${preferred} without validation.`);
        return preferred;
      }
      // Even if a preferred port is provided, fall back to scanning for a nearby
      // available port to avoid spurious failures when developers have multiple
      // environments running locally.
      const fallback = runPortProbe(preferred, 25, false);
      if (!fallback.success || typeof fallback.port !== 'number') {
        throw new Error(`PLAYWRIGHT_APP_PORT=${preferred} is unavailable (${check.error || 'unknown error'}).`);
      }
      return fallback.port;
    }
    return check.port;
  }

  const probe = runPortProbe(preferred, 25, false);
  if (!probe.success || typeof probe.port !== 'number') {
    if (isPortProbePermissionError(probe.error)) {
      console.warn(`[Playwright] Port probe blocked (${probe.error}); defaulting to port ${preferred}.`);
      return preferred;
    }
    throw new Error(`Unable to find an available port for the Playwright dev server (${probe.error || 'probe failed'}).`);
  }
  return probe.port;
}

function resolveServicePortSync(envVarName: string, fallbackPort: number): number {
  const preferred = Number(process.env[envVarName] || fallbackPort);
  if (!Number.isFinite(preferred)) {
    throw new Error(`Invalid ${envVarName} value: ${process.env[envVarName]}`);
  }

  const lockedVarName = `${envVarName}_LOCKED`;
  if (process.env[lockedVarName] === 'true') {
    return preferred;
  }

  // If the preferred port is taken, scan forward for an available one (up to 25).
  const probe = runPortProbe(preferred, 25, false);
  if (!probe.success || typeof probe.port !== 'number') {
    if (isPortProbePermissionError(probe.error)) {
      console.warn(`[Playwright] Port probe blocked (${probe.error}); using ${envVarName}=${preferred} without validation.`);
      return preferred;
    }
    throw new Error(`Unable to find an available port for ${envVarName} starting at ${preferred} (${probe.error || 'probe failed'}).`);
  }

  return probe.port;
}

const PORT_CACHE_KEY = Symbol.for('__ALGA_PLAYWRIGHT_PORT__');

function getCachedWebPort(): number {
  const cached = globalThis[PORT_CACHE_KEY];
  if (typeof cached === 'number' && Number.isFinite(cached)) {
    return cached;
  }
  console.log('[Playwright] env before port detection', {
    PLAYWRIGHT_APP_PORT: process.env.PLAYWRIGHT_APP_PORT,
    APP_PORT: process.env.APP_PORT,
    PORT: process.env.PORT,
  });
  const resolved = resolveWebPortSync();
  globalThis[PORT_CACHE_KEY] = resolved;
  console.log(`[Playwright] using dev server port ${resolved}`);
  return resolved;
}

const resolvedWebPort = getCachedWebPort();
const webHost = process.env.PLAYWRIGHT_APP_HOST || 'localhost';
const resolvedBaseUrl = process.env.PLAYWRIGHT_BASE_URL || `http://${webHost}:${resolvedWebPort}`;

const resolvedHostForEnv = (() => {
  try {
    return new URL(resolvedBaseUrl).host;
  } catch {
    return `${webHost}:${resolvedWebPort}`;
  }
})();

process.env.PLAYWRIGHT_APP_PORT = String(resolvedWebPort);
process.env.PLAYWRIGHT_APP_PORT_LOCKED = 'true';
process.env.HOST = process.env.HOST || resolvedHostForEnv;
process.env.APP_PORT = process.env.APP_PORT || String(resolvedWebPort);
process.env.EXPOSE_SERVER_PORT = process.env.EXPOSE_SERVER_PORT || String(resolvedWebPort);
process.env.PORT = process.env.PORT || String(resolvedWebPort);

// Reserve ports for dockerized Playwright deps (postgres/redis/worker).
const resolvedPlaywrightDbPort = resolveServicePortSync('PLAYWRIGHT_DB_PORT', Number(directDbPort || 5439));
process.env.PLAYWRIGHT_DB_PORT = String(resolvedPlaywrightDbPort);
process.env.PLAYWRIGHT_DB_PORT_LOCKED = 'true';
process.env.DB_DIRECT_PORT = String(resolvedPlaywrightDbPort);
process.env.PLAYWRIGHT_DB_PORT = String(resolvedPlaywrightDbPort);

const resolvedPlaywrightRedisPort = resolveServicePortSync('REDIS_PORT', Number(process.env.REDIS_PORT || 16379));
process.env.REDIS_PORT = String(resolvedPlaywrightRedisPort);
process.env.REDIS_PORT_LOCKED = 'true';

const resolvedPlaywrightHocuspocusPort = resolveServicePortSync(
  'PLAYWRIGHT_HOCUSPOCUS_PORT',
  Number(process.env.PLAYWRIGHT_HOCUSPOCUS_PORT || process.env.EXPOSE_HOCUSPOCUS_PORT || 1234)
);
process.env.PLAYWRIGHT_HOCUSPOCUS_PORT = String(resolvedPlaywrightHocuspocusPort);
process.env.PLAYWRIGHT_HOCUSPOCUS_PORT_LOCKED = 'true';
process.env.EXPOSE_HOCUSPOCUS_PORT = String(resolvedPlaywrightHocuspocusPort);
process.env.NEXT_PUBLIC_HOCUSPOCUS_URL =
  process.env.NEXT_PUBLIC_HOCUSPOCUS_URL || `ws://localhost:${resolvedPlaywrightHocuspocusPort}`;
process.env.HOCUSPOCUS_JWT_SECRET =
  process.env.HOCUSPOCUS_JWT_SECRET || 'dev-hocuspocus-jwt-secret';
process.env.REDIS_PASSWORD = process.env.REDIS_PASSWORD || 'sebastian123';

const skipWorkflowWorker = parseTruthyEnv(process.env.PLAYWRIGHT_SKIP_WORKFLOW_WORKER);

// Apply Playwright-specific database configuration (after port resolution).
applyPlaywrightDatabaseEnv();

function parseTruthyEnv(value?: string): boolean {
  if (!value) return false;
  const normalized = value.trim().toLowerCase();
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y' || normalized === 'on';
}

// Decide whether Playwright should manage starting the dev server.
// - In CI, some pipelines start the server externally and only want tests to connect.
// - However, if no explicit base URL is provided, disabling webServer causes hard-to-debug ECONNREFUSED.
const explicitBaseUrlProvided = Boolean(
  process.env.PLAYWRIGHT_BASE_URL || process.env.EE_BASE_URL || process.env.NEXTAUTH_URL
);
const isCi = parseTruthyEnv(process.env.CI);
const shouldRunWebServer =
  process.env.PW_WEBSERVER === 'true'
    ? true
    : process.env.PW_WEBSERVER === 'false'
      ? false
      : !(isCi && explicitBaseUrlProvided);

// If Playwright is managing the dev server, make the computed base URL authoritative.
// This avoids a mismatch where an env file sets EE_BASE_URL/NEXTAUTH_URL to a different port.
if (shouldRunWebServer && !process.env.PLAYWRIGHT_BASE_URL) {
  process.env.EE_BASE_URL = resolvedBaseUrl;
  process.env.NEXTAUTH_URL = resolvedBaseUrl;
} else {
  process.env.EE_BASE_URL = process.env.EE_BASE_URL || resolvedBaseUrl;
  process.env.NEXTAUTH_URL = process.env.NEXTAUTH_URL || resolvedBaseUrl;
}

console.log('[Playwright] webServer', {
  CI: process.env.CI,
  isCi,
  explicitBaseUrlProvided,
  shouldRunWebServer,
  resolvedBaseUrl,
  EE_BASE_URL: process.env.EE_BASE_URL,
  NEXTAUTH_URL: process.env.NEXTAUTH_URL,
});

/**
 * Playwright configuration for EE server integration tests
 * @see https://playwright.dev/docs/test-configuration
 */
export default defineConfig({
  testDir: './src/__tests__/integration',
  // Run all Playwright integration tests in this folder
  testMatch: ['**/*.playwright.test.ts'],

  /* Global setup file */
  globalSetup: './playwright.global-setup.ts',

  /* Global teardown file */
  globalTeardown: './playwright.global-teardown.ts',

  /* Run tests in files in parallel */
  fullyParallel: false, // Disabled for database isolation

  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,

  /* Retry control: CI default 2, local default 0, override with PW_RETRIES */
  retries: process.env.PW_RETRIES !== undefined
    ? Number(process.env.PW_RETRIES)
    : (process.env.CI ? 2 : 0),

  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : 1, // Single worker for database isolation

  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: [
    ['list'],
    ['json', { outputFile: 'playwright-report/results.json' }],
    ['html', { open: 'never' }]
  ],

  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    baseURL: resolvedBaseUrl,

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry',

    /* Take screenshot on failure */
    screenshot: 'only-on-failure',

    /* Record video on failure */
    video: 'retain-on-failure',

    /* Global test timeout */
    actionTimeout: 15000,
    navigationTimeout: 30000,
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        launchOptions: {
          headless: false,
          args: [
            '--host-resolver-rules=MAP portal.acme.local 127.0.0.1,MAP canonical.localhost 127.0.0.1,MAP localhost 127.0.0.1'
          ],
        },
      },
    },

    // Uncomment for cross-browser testing
    // {
    //   name: 'firefox',
    //   use: { ...devices['Desktop Firefox'] },
    // },

    // {
    //   name: 'webkit',
    //   use: { ...devices['Desktop Safari'] },
    // },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },
  ],

  /* Run your local dev server before starting the tests */
  webServer: shouldRunWebServer ? {
    // Reset DB once per session before starting the dev server.
    command:
      'cd ../../' +
      ` && PLAYWRIGHT_DB_PORT=${resolvedPlaywrightDbPort} REDIS_PORT=${resolvedPlaywrightRedisPort} PLAYWRIGHT_HOCUSPOCUS_PORT=${resolvedPlaywrightHocuspocusPort} REDIS_PASSWORD=${process.env.REDIS_PASSWORD} HOCUSPOCUS_JWT_SECRET=${process.env.HOCUSPOCUS_JWT_SECRET} docker compose -f docker-compose.playwright-workflow-deps.yml -p alga-psa-playwright-workflow --env-file ${dockerComposeEnvFile} up -d --wait --wait-timeout 60 postgres-playwright redis-playwright` +
      ' && node --import tsx/esm scripts/bootstrap-playwright-db.ts' +
      (skipWorkflowWorker
        ? ` && PLAYWRIGHT_DB_PORT=${resolvedPlaywrightDbPort} REDIS_PORT=${resolvedPlaywrightRedisPort} PLAYWRIGHT_HOCUSPOCUS_PORT=${resolvedPlaywrightHocuspocusPort} REDIS_PASSWORD=${process.env.REDIS_PASSWORD} HOCUSPOCUS_JWT_SECRET=${process.env.HOCUSPOCUS_JWT_SECRET} docker compose -f docker-compose.playwright-workflow-deps.yml -p alga-psa-playwright-workflow --env-file ${dockerComposeEnvFile} up -d --build --wait --wait-timeout 120 hocuspocus-playwright`
        : ` && PLAYWRIGHT_DB_PORT=${resolvedPlaywrightDbPort} REDIS_PORT=${resolvedPlaywrightRedisPort} PLAYWRIGHT_HOCUSPOCUS_PORT=${resolvedPlaywrightHocuspocusPort} REDIS_PASSWORD=${process.env.REDIS_PASSWORD} HOCUSPOCUS_JWT_SECRET=${process.env.HOCUSPOCUS_JWT_SECRET} docker compose -f docker-compose.playwright-workflow-deps.yml -p alga-psa-playwright-workflow --env-file ${dockerComposeEnvFile} up -d --build --wait --wait-timeout 120 workflow-worker-playwright hocuspocus-playwright`) +
      ' && NEXT_PUBLIC_EDITION=enterprise npm run dev',
    url: resolvedBaseUrl,
    // Default to starting a fresh server (we also need to bring up dockerized deps + reset the DB).
    // Opt-in reuse with PW_REUSE=true for local iteration.
    reuseExistingServer: process.env.PW_REUSE === 'true',
    timeout: 300000,
    stdout: 'pipe',
    stderr: 'pipe',
    env: {
      ...process.env,
      // Use a writable filesystem secret store for tenant secrets; keep app secrets in env.
      SECRET_READ_CHAIN: process.env.SECRET_READ_CHAIN || 'filesystem,env',
      SECRET_WRITE_PROVIDER: process.env.SECRET_WRITE_PROVIDER || 'filesystem',
      SECRET_FS_BASE_PATH: process.env.SECRET_FS_BASE_PATH || 'secrets-playwright',
      NEXT_PUBLIC_EDITION: 'enterprise',
      E2E_AUTH_BYPASS: 'true',
      EE_BASE_URL: resolvedBaseUrl,
      NEXTAUTH_URL: resolvedBaseUrl,
      HOST: resolvedHostForEnv,
      HOSTNAME: webHost,
      PORT: String(resolvedWebPort),
      APP_PORT: String(resolvedWebPort),
      EXPOSE_SERVER_PORT: String(resolvedWebPort),
      NEXT_PUBLIC_APP_URL: resolvedBaseUrl,
      NEXT_PUBLIC_SITE_URL: resolvedBaseUrl,
      NEXT_PUBLIC_API_BASE_URL: resolvedBaseUrl,
      NEXT_PUBLIC_EXTERNAL_APP_URL: resolvedBaseUrl,
      NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET || 'test-nextauth-secret',
      NEXT_PUBLIC_DISABLE_FEATURE_FLAGS: process.env.NEXT_PUBLIC_DISABLE_FEATURE_FLAGS ?? 'true',
      NEXT_PUBLIC_HOCUSPOCUS_URL:
        process.env.NEXT_PUBLIC_HOCUSPOCUS_URL || `ws://localhost:${resolvedPlaywrightHocuspocusPort}`,
      HOCUSPOCUS_JWT_SECRET: process.env.HOCUSPOCUS_JWT_SECRET || 'dev-hocuspocus-jwt-secret',
      REDIS_PASSWORD: process.env.REDIS_PASSWORD || 'sebastian123',
      // Explicitly set DB config for Playwright - override server/.env settings
      DB_HOST: process.env.PLAYWRIGHT_DB_HOST || process.env.DB_HOST || 'localhost',
      DB_PORT: process.env.PLAYWRIGHT_DB_PORT || process.env.DB_PORT || '5439',
      DB_NAME: process.env.PLAYWRIGHT_DB_NAME || process.env.DB_NAME || 'alga_contract_wizard_test',
      DB_NAME_SERVER: process.env.PLAYWRIGHT_DB_NAME || process.env.DB_NAME_SERVER || 'alga_contract_wizard_test',
      DB_USER: process.env.PLAYWRIGHT_DB_ADMIN_USER || process.env.DB_USER || 'postgres',
      DB_PASSWORD: process.env.PLAYWRIGHT_DB_ADMIN_PASSWORD || process.env.DB_PASSWORD || '',
      DB_USER_SERVER: process.env.PLAYWRIGHT_DB_ADMIN_USER || process.env.DB_USER_SERVER || 'postgres',
      DB_PASSWORD_SERVER: process.env.PLAYWRIGHT_DB_ADMIN_PASSWORD || process.env.DB_PASSWORD_SERVER || '',
      DB_PASSWORD_ADMIN: process.env.PLAYWRIGHT_DB_ADMIN_PASSWORD || process.env.DB_PASSWORD_ADMIN || '',
      // Use S3/MinIO for file uploads (not local storage)
      // MinIO test instance runs on port 9002 (separate from Payload MinIO on 9000)
      STORAGE_DEFAULT_PROVIDER: 's3', // Use S3/MinIO instead of local storage
      STORAGE_S3_ENDPOINT: process.env.STORAGE_S3_ENDPOINT || 'http://localhost:9002',
      STORAGE_S3_ACCESS_KEY: process.env.STORAGE_S3_ACCESS_KEY || 'minioadmin',
      STORAGE_S3_SECRET_KEY: process.env.STORAGE_S3_SECRET_KEY || 'minioadmin',
      STORAGE_S3_BUCKET: process.env.STORAGE_S3_BUCKET || 'alga-test',
      STORAGE_S3_REGION: process.env.STORAGE_S3_REGION || 'us-east-1',
      STORAGE_S3_FORCE_PATH_STYLE: process.env.STORAGE_S3_FORCE_PATH_STYLE || 'true',
      // Redis is required for the event bus; prefer a local instance for Playwright.
      REDIS_HOST: process.env.REDIS_HOST || 'localhost',
      REDIS_PORT: process.env.REDIS_PORT || '6379',
    }
  } : undefined,

  /* Global test timeout */
  timeout: 60000,

  /* Test output directory */
  outputDir: 'playwright-test-results/',
});
