const fs = require('fs/promises');
const fsSync = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
const { randomUUID } = require('crypto');

const DEFAULT_CONFIG = {
  wpHost: process.env.TEST_WP_HOST || process.env.EXTERNAL_WP_HOST || '',
  wpSshUser: process.env.TEST_WP_SSH_USER || process.env.EXTERNAL_WP_SSH_USER || 'wordpress',
  wpSshKey: process.env.TEST_WP_SSH_KEY || process.env.TEST_WP_SSH_KEY_PATH || process.env.EXTERNAL_WP_SSH_KEY || '',
  wpPath: process.env.TEST_WP_PATH || process.env.EXTERNAL_WP_PATH || '/var/www/html',
  wpBaseUrl: process.env.TEST_WP_BASE_URL || process.env.EXTERNAL_WP_BASE_URL || '',
  enableMultisite: (process.env.TEST_WP_MULTISITE || process.env.EXTERNAL_WP_MULTISITE || 'true') !== 'false',
  subsitePrefix: process.env.TEST_WP_SUBSITE_PREFIX || process.env.EXTERNAL_WP_SUBSITE_PREFIX || 'test',
  subsiteDomain: process.env.TEST_WP_SUBSITE_DOMAIN || process.env.EXTERNAL_WP_SUBSITE_DOMAIN || '',
  subsiteMode: (process.env.TEST_WP_SUBSITE_MODE || process.env.EXTERNAL_WP_SUBSITE_MODE || 'subdirectory').toLowerCase(),
  maxConcurrentTests: Number(process.env.TEST_MAX_CONCURRENT || process.env.EXTERNAL_WP_MAX_CONCURRENT || 20),
  queueTimeoutMs: Number(process.env.TEST_QUEUE_TIMEOUT || process.env.EXTERNAL_WP_QUEUE_TIMEOUT || 300000),
  testTimeoutMs: Number(process.env.TEST_TIMEOUT || process.env.EXTERNAL_WP_TEST_TIMEOUT || 600000),
  autoCleanup: (process.env.TEST_AUTO_CLEANUP || process.env.EXTERNAL_WP_AUTO_CLEANUP || 'true') !== 'false',
  cleanupDelayMs: Number(process.env.TEST_CLEANUP_DELAY || process.env.EXTERNAL_WP_CLEANUP_DELAY || 3600000),
  errorLogPath: process.env.TEST_WP_ERROR_LOG || process.env.EXTERNAL_WP_ERROR_LOG || '/var/log/wp-errors.log',
  strictHostKeyChecking: (process.env.TEST_SSH_STRICT || process.env.EXTERNAL_WP_SSH_STRICT || 'false') === 'true',
};

function expandHome(p) {
  if (!p || typeof p !== 'string') return p;
  if (p.startsWith('~')) {
    return path.join(os.homedir(), p.slice(1));
  }
  return p;
}

function getExternalTestingConfig(overrides = {}) {
  const config = { ...DEFAULT_CONFIG, ...(overrides || {}) };
  config.wpSshKey = expandHome(config.wpSshKey);
  config.wpBaseUrl = (config.wpBaseUrl || '').trim();
  if (!config.wpBaseUrl && config.wpHost) {
    config.wpBaseUrl = `https://${config.wpHost}`;
  }
  config.maxConcurrentTests = Number.isFinite(config.maxConcurrentTests) && config.maxConcurrentTests > 0
    ? Math.floor(config.maxConcurrentTests)
    : 1;
  config.queueTimeoutMs = Number.isFinite(config.queueTimeoutMs) && config.queueTimeoutMs > 0
    ? Math.floor(config.queueTimeoutMs)
    : 300000;
  config.testTimeoutMs = Number.isFinite(config.testTimeoutMs) && config.testTimeoutMs > 0
    ? Math.floor(config.testTimeoutMs)
    : 600000;
  config.cleanupDelayMs = Number.isFinite(config.cleanupDelayMs) && config.cleanupDelayMs >= 0
    ? Math.floor(config.cleanupDelayMs)
    : 3600000;
  config.subsiteMode = config.subsiteMode === 'subdomain' ? 'subdomain' : 'subdirectory';
  return config;
}

function spawnCommand(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    const child = spawn(command, args, { ...options, windowsHide: true });
    let stdout = '';
    let stderr = '';

    child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
    child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });

    child.on('error', (error) => {
      reject(error);
    });

    child.on('close', (code) => {
      resolve({ stdout, stderr, code: code ?? 0, durationMs: Date.now() - start });
    });
  });
}

function buildSshBaseArgs(config) {
  const args = [];
  if (config.wpSshKey) {
    args.push('-i', config.wpSshKey);
  }
  if (!config.strictHostKeyChecking) {
    args.push('-o', 'StrictHostKeyChecking=no');
  }
  args.push('-o', 'BatchMode=yes');
  return args;
}

async function runSshCommand(config, command, options = {}) {
  const target = `${config.wpSshUser}@${config.wpHost}`;
  const args = [...buildSshBaseArgs(config), target, command];
  return spawnCommand('ssh', args, { timeout: options.timeout || config.testTimeoutMs });
}

async function runScpUpload(config, localPath, remotePath) {
  const args = [...buildSshBaseArgs(config), '-r', localPath, `${config.wpSshUser}@${config.wpHost}:${remotePath}`];
  return spawnCommand('scp', args, { timeout: config.testTimeoutMs });
}

function buildWpCliCommand(config, url, command) {
  return `wp --path=${config.wpPath} --url=${url} ${command}`;
}

function resolveSubsiteUrl(config, slug) {
  if (!config.enableMultisite) return config.wpBaseUrl;
  const domain = config.subsiteDomain || config.wpHost;
  if (config.subsiteMode === 'subdomain') {
    return `https://${slug}.${domain}`;
  }
  return `${config.wpBaseUrl.replace(/\/$/, '')}/${slug}`;
}

async function createSubsite(config, sessionId) {
  const slug = `${config.subsitePrefix}-${sessionId.slice(0, 8)}`;
  const title = `Test Environment ${sessionId}`;
  const adminEmail = 'test@example.com';

  const createCmd = buildWpCliCommand(
    config,
    config.wpBaseUrl,
    `site create --slug=${slug} --title="${title}" --email=${adminEmail}`
  );
  const createRes = await runSshCommand(config, createCmd, { timeout: config.testTimeoutMs });

  if (createRes.code !== 0) {
    throw new Error(`Failed to create subsite: ${createRes.stderr || createRes.stdout}`);
  }

  const url = resolveSubsiteUrl(config, slug);
  
  // Configure permalink structure
  const permalinkCmd = buildWpCliCommand(
    config,
    url,
    'option update permalink_structure "/%postname%/"'
  );
  await runSshCommand(config, permalinkCmd, { timeout: config.testTimeoutMs });
  
  // Flush rewrite rules
  const rewriteCmd = buildWpCliCommand(
    config,
    url,
    'rewrite flush'
  );
  await runSshCommand(config, rewriteCmd, { timeout: config.testTimeoutMs });

  return { slug, url, adminUrl: `${url.replace(/\/$/, '')}/wp-admin` };
}

async function deleteSubsite(config, slug) {
  if (!slug) return;
  const deleteCmd = buildWpCliCommand(
    config,
    config.wpBaseUrl,
    `site delete --slug=${slug} --yes`
  );
  await runSshCommand(config, deleteCmd, { timeout: config.testTimeoutMs });
}

async function installPluginFromLocal(config, pluginPath, pluginSlug, sessionId) {
  if (!pluginPath || !pluginSlug) throw new Error('Plugin path and slug are required');
  const safeSlug = pluginSlug.replace(/[^a-zA-Z0-9\-_]/g, '');
  const remoteBase = path.posix.join(config.wpPath, 'wp-content', 'plugins');
  const tmpDir = path.posix.join(remoteBase, `${safeSlug}__tmp_${sessionId}`);
  const finalDir = path.posix.join(remoteBase, safeSlug);

  const cleanupTmp = async () => {
    const cmd = `rm -rf ${tmpDir}`;
    await runSshCommand(config, cmd, { timeout: config.testTimeoutMs }).catch(() => {});
  };

  await cleanupTmp();
  const scpResult = await runScpUpload(config, pluginPath, tmpDir);
  if (scpResult.code !== 0) {
    throw new Error(`Failed to upload plugin: ${scpResult.stderr || scpResult.stdout}`);
  }

  const moveCmd = `rm -rf ${finalDir} && mv ${tmpDir} ${finalDir}`;
  const moveRes = await runSshCommand(config, moveCmd, { timeout: config.testTimeoutMs });
  if (moveRes.code !== 0) {
    throw new Error(`Failed to install plugin: ${moveRes.stderr || moveRes.stdout}`);
  }
}

async function installRequiredPlugins(config, subsiteUrl, plugins) {
  if (!Array.isArray(plugins)) return [];
  const installed = [];
  for (const spec of plugins) {
    if (!spec || !spec.plugin_slug) continue;
    const slug = spec.plugin_slug;
    const source = (spec.source || 'wordpress.org').toLowerCase();
    const activate = spec.activate !== false;
    let command;
    if (source === 'url' && spec.source_url) {
      command = `plugin install ${spec.source_url} ${activate ? '--activate' : ''}`;
    } else if (source === 'local' && spec.source_url) {
      await installPluginFromLocal(config, spec.source_url, slug, randomUUID());
      command = activate ? `plugin activate ${slug}` : `plugin deactivate ${slug}`;
    } else {
      command = `plugin install ${slug} ${activate ? '--activate' : ''}`;
    }

    const res = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
    installed.push({ slug, status: res.code === 0 ? (activate ? 'active' : 'installed') : 'error', output: res.stdout || res.stderr });
  }
  return installed;
}

function normalizeScenarioAssertions(assertions = {}) {
  return {
    status_code: Number.isFinite(assertions.status_code) ? assertions.status_code : null,
    contains: Array.isArray(assertions.contains) ? assertions.contains : [],
    not_contains: Array.isArray(assertions.not_contains) ? assertions.not_contains : [],
    wp_cli_success: assertions.wp_cli_success === true,
  };
}

function normalizeTestScenarios(spec, pluginSlug, config) {
  const scenarios = Array.isArray(spec?.test_scenarios) ? spec.test_scenarios : [];
  const normalized = scenarios.map((s) => ({
    name: String(s?.name || 'Unnamed test'),
    type: String(s?.type || 'custom'),
    url: s?.url || '',
    selector: s?.selector || '',
    expected_text: s?.expected_text || '',
    wp_cli_command: s?.wp_cli_command || '',
    shortcode: s?.shortcode || '',
    hook: s?.hook || '',
    ajax_action: s?.ajax_action || '',
    method: s?.method || '',
    body: s?.body || '',
    assertions: normalizeScenarioAssertions(s?.assertions || {})
  }));

  if (!normalized.length && pluginSlug) {
    normalized.push({
      name: 'Plugin activates without errors',
      type: 'custom',
      wp_cli_command: `plugin activate ${pluginSlug}`,
      assertions: { wp_cli_success: true, contains: [], not_contains: ['Fatal error', 'Parse error'] }
    });
    normalized.push({
      name: 'Plugin is listed as active',
      type: 'custom',
      wp_cli_command: `plugin status ${pluginSlug}`,
      assertions: { contains: ['Active'] }
    });
  }

  if (spec?.auto_error_log_check !== false && config?.errorLogPath) {
    const safePath = config.errorLogPath.replace(/"/g, '').trim();
    const grepSlug = pluginSlug ? ` | grep -i "${pluginSlug}"` : '';
    normalized.push({
      name: 'Scan error log for PHP errors',
      type: 'custom',
      wp_cli_command: `eval "echo shell_exec('tail -n 200 ${safePath.replace(/'/g, "'\\''")}${grepSlug} || echo No errors found');"`,
      assertions: { not_contains: ['Fatal error', 'Parse error'] }
    });
  }

  return normalized;
}

async function runHttpScenario(config, subsiteUrl, scenario, baseFn) {
  const url = scenario.url || '/';
  const method = String(scenario.method || '').toUpperCase() || 'GET';
  const body = scenario.body || '';
  const bodyArg = body ? `, ["body" => ${JSON.stringify(body)}]` : '';
  const requestFn = method === 'POST' ? 'wp_remote_post' : 'wp_remote_get';
  const phpUrl = `${baseFn}("${url}")`;
  const command = buildWpCliCommand(
    config,
    subsiteUrl,
    `eval ' $res = ${requestFn}(${phpUrl}${bodyArg}); echo json_encode(["status" => wp_remote_retrieve_response_code($res), "body" => wp_remote_retrieve_body($res)]);'`
  );
  const result = await runSshCommand(config, command, { timeout: config.testTimeoutMs });
  let statusCode = null;
  let bodyOut = result.stdout || '';
  try {
    const parsed = JSON.parse((result.stdout || '').trim());
    statusCode = parsed.status;
    bodyOut = parsed.body || '';
  } catch (_) {
    bodyOut = result.stdout || result.stderr || '';
  }
  return { result, statusCode, body: bodyOut };
}

function evaluateAssertions(output, statusCode, assertions) {
  const failures = [];
  if (Number.isFinite(assertions.status_code) && statusCode !== assertions.status_code) {
    failures.push(`status_code expected ${assertions.status_code} but got ${statusCode}`);
  }
  for (const needle of assertions.contains || []) {
    if (!output.includes(needle)) {
      failures.push(`missing text: ${needle}`);
    }
  }
  for (const needle of assertions.not_contains || []) {
    if (output.includes(needle)) {
      failures.push(`unexpected text: ${needle}`);
    }
  }
  return failures;
}

async function runTestScenario(config, subsiteUrl, scenario) {
  const assertions = normalizeScenarioAssertions(scenario.assertions || {});
  let output = '';
  let statusCode = null;
  let command = scenario.wp_cli_command || '';
  let result;

  if (scenario.type === 'endpoint' && !command) {
    const endpointRes = await runHttpScenario(config, subsiteUrl, scenario, 'home_url');
    result = endpointRes.result;
    statusCode = endpointRes.statusCode;
    output = endpointRes.body || '';
  } else if (scenario.type === 'admin_page' && !command) {
    const adminUrl = scenario.url || 'admin.php';
    const adminScenario = { ...scenario, url: adminUrl };
    const adminRes = await runHttpScenario(config, subsiteUrl, adminScenario, 'admin_url');
    result = adminRes.result;
    statusCode = adminRes.statusCode;
    output = adminRes.body || '';
  } else if (scenario.type === 'ajax' && !command) {
    const action = scenario.ajax_action || (scenario.url || '').replace(/^\/?/, '');
    const ajaxUrl = action.includes('admin-ajax.php') || action.includes('action=')
      ? action
      : `admin-ajax.php?action=${action || 'ping'}`;
    const ajaxScenario = { ...scenario, url: ajaxUrl, method: scenario.method || 'POST' };
    const ajaxRes = await runHttpScenario(config, subsiteUrl, ajaxScenario, 'admin_url');
    result = ajaxRes.result;
    statusCode = ajaxRes.statusCode;
    output = ajaxRes.body || '';
  } else if (scenario.type === 'shortcode' && !command) {
    const shortcode = scenario.shortcode || scenario.url || '';
    const shortcodeInput = shortcode.includes('[') ? shortcode : `[${shortcode}]`;
    command = `eval "echo do_shortcode('${shortcodeInput.replace(/'/g, "'\\''")}');"`;
    result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
    output = (result.stdout || result.stderr || '').trim();
  } else if (scenario.type === 'hook' && !command) {
    const hook = scenario.hook || scenario.url || '';
    command = `eval "echo json_encode([\"action\" => has_action('${hook.replace(/'/g, "'\\''")}') ? 1 : 0, \"filter\" => has_filter('${hook.replace(/'/g, "'\\''")}') ? 1 : 0]);"`;
    result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
    output = (result.stdout || result.stderr || '').trim();
  } else {
    if (!command) {
      command = 'eval "echo \'No command provided\';"';
    }
    result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
    output = (result.stdout || result.stderr || '').trim();
  }

  const failures = evaluateAssertions(output, statusCode, assertions);
  if (assertions.wp_cli_success && result.code !== 0) {
    failures.push(`wp_cli_command exited with ${result.code}`);
  }

  return {
    name: scenario.name,
    status: failures.length ? 'failed' : 'passed',
    command,
    output,
    statusCode,
    duration: result.durationMs || 0,
    failures,
  };
}

async function runTestScenarios(config, subsiteUrl, scenarios) {
  const results = [];
  let passed = 0;
  let failed = 0;

  for (const scenario of scenarios) {
    const result = await runTestScenario(config, subsiteUrl, scenario);
    results.push(result);
    if (result.status === 'passed') {
      passed += 1;
    } else {
      failed += 1;
    }
  }

  return { passed, failed, results };
}

class ExternalTestQueue {
  constructor(maxConcurrent, queueTimeoutMs) {
    this.maxConcurrent = maxConcurrent;
    this.queueTimeoutMs = queueTimeoutMs;
    this.active = 0;
    this.queue = [];
  }

  enqueue(task) {
    return new Promise((resolve) => {
      const entry = { task, resolve, enqueuedAt: Date.now() };
      this.queue.push(entry);
      this.process();
    });
  }

  process() {
    if (this.active >= this.maxConcurrent) return;
    const entry = this.queue.shift();
    if (!entry) return;

    if (Date.now() - entry.enqueuedAt > this.queueTimeoutMs) {
      entry.resolve({ ok: false, error: 'Test queue timeout - too many concurrent tests' });
      this.process();
      return;
    }

    this.active += 1;
    Promise.resolve()
      .then(entry.task)
      .then((result) => entry.resolve(result))
      .catch((error) => entry.resolve({ ok: false, error: error.message || String(error) }))
      .finally(() => {
        this.active -= 1;
        this.process();
      });
  }
}

function createExternalWpTester(options = {}) {
  const logger = typeof options.logger === 'function' ? options.logger : null;
  const queue = new ExternalTestQueue(
    DEFAULT_CONFIG.maxConcurrentTests,
    DEFAULT_CONFIG.queueTimeoutMs
  );

  const log = (msg, meta) => {
    if (logger) logger(msg, meta || {});
  };

  async function runTest(input = {}, opts = {}) {
    const config = getExternalTestingConfig(opts.configOverrides);
    if (!config.wpHost || !config.wpSshUser) {
      return { ok: false, error: 'External WordPress test host is not configured' };
    }
    if (!config.wpSshKey || !fsSync.existsSync(config.wpSshKey)) {
      return { ok: false, error: 'SSH key for external WordPress testing is missing or invalid' };
    }

    const sessionId = input.session_id || randomUUID();
    const pluginPath = input.plugin_path;
    const pluginSlug = input.plugin_slug || path.basename(pluginPath || '').replace(/\.zip$/i, '');
    const requiredPlugins = input.required_plugins || [];

    return queue.enqueue(async () => {
      const start = Date.now();
      let subsite = { slug: null, url: config.wpBaseUrl, adminUrl: `${config.wpBaseUrl.replace(/\/$/, '')}/wp-admin` };
      const warnings = [];
      const errors = [];

      try {
        if (config.enableMultisite) {
          subsite = await createSubsite(config, sessionId);
        }

        if (pluginPath && pluginSlug) {
          await installPluginFromLocal(config, pluginPath, pluginSlug, sessionId);
        } else {
          warnings.push('Plugin path or slug missing; skipping plugin upload');
        }

        const installedPlugins = await installRequiredPlugins(config, subsite.url, requiredPlugins);
        if (pluginSlug) {
          const activateCmd = buildWpCliCommand(config, subsite.url, `plugin activate ${pluginSlug}`);
          const activateRes = await runSshCommand(config, activateCmd, { timeout: config.testTimeoutMs });
          if (activateRes.code !== 0) {
            errors.push(`Failed to activate plugin ${pluginSlug}: ${activateRes.stderr || activateRes.stdout}`);
          }
        }

        const scenarios = normalizeTestScenarios(input, pluginSlug, config);
        const cliTests = await runTestScenarios(config, subsite.url, scenarios);

        const result = {
          ok: cliTests.failed === 0,
          session_id: sessionId,
          subsite_url: subsite.url,
          test_results: {
            mode: 'cli',
            cli_tests: cliTests,
          },
          installed_plugins: installedPlugins,
          errors,
          warnings,
          duration: Date.now() - start,
        };

        if (config.autoCleanup && subsite.slug) {
          setTimeout(() => {
            deleteSubsite(config, subsite.slug).catch(() => {});
          }, config.cleanupDelayMs);
          result.cleanup_scheduled = new Date(Date.now() + config.cleanupDelayMs).toISOString();
        }

        return result;
      } catch (error) {
        errors.push(error.message || String(error));
        if (config.autoCleanup && subsite.slug) {
          setTimeout(() => {
            deleteSubsite(config, subsite.slug).catch(() => {});
          }, config.cleanupDelayMs);
        }
        return {
          ok: false,
          session_id: sessionId,
          subsite_url: subsite.url,
          test_results: { mode: 'cli', cli_tests: { passed: 0, failed: 1, results: [] } },
          installed_plugins: [],
          errors,
          warnings,
          duration: Date.now() - start,
        };
      }
    });
  }

  return { runTest, getConfig: () => getExternalTestingConfig() };
}

module.exports = {
  createExternalWpTester,
  getExternalTestingConfig,
};
