#!/usr/bin/env node
/**
 * Compile all TypeScript source + test files to dist-test/ using esbuild.
 * Run compiled JS directly with node --test (no per-file TS overhead).
 *
 * Usage: node scripts/compile-tests.mjs
 */

import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
import { existsSync, symlinkSync } from 'node:fs';
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const ROOT = join(__dirname, '..');

const require = createRequire(import.meta.url);
const esbuild = require(join(ROOT, 'node_modules/esbuild'));

// Recursively collect files by extension (skip node_modules, templates, etc.)
// Directories to skip during file collection
const SKIP_DIRS = new Set(['node_modules', 'templates', '__tests__', 'integration']);

async function collectFiles(dir, exts = ['.ts', '.mjs']) {
  const results = [];
  let entries;
  try {
    entries = await readdir(dir, { withFileTypes: true });
  } catch {
    return results;
  }
  for (const entry of entries) {
    if (SKIP_DIRS.has(entry.name)) continue;
    const full = join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...await collectFiles(full, exts));
    } else if (
      exts.some(ext => entry.name.endsWith(ext)) &&
      !entry.name.endsWith('.d.ts')
    ) {
      results.push(full);
    }
  }
  return results;
}

// Dirs to skip when copying assets (node_modules are never useful in dist-test)
const ASSET_SKIP_DIRS = new Set(['node_modules', '__tests__', 'integration']);

/**
 * Recursively copy files from srcDir to destDir.
 * Skips node_modules only. Copies everything: .ts/.tsx originals (for jiti),
 * .mjs helpers, .md/.yaml/.json assets, etc.
 * esbuild compiled .js output already lands in dist-test, so we just
 * overlay the asset files on top.
 */
async function copyAssets(srcDir, destDir) {
  let entries;
  try {
    entries = await readdir(srcDir, { withFileTypes: true });
  } catch {
    return; // directory doesn't exist, nothing to copy
  }
  for (const entry of entries) {
    if (ASSET_SKIP_DIRS.has(entry.name)) continue;
    const srcPath = join(srcDir, entry.name);
    const destPath = join(destDir, entry.name);
    if (entry.isDirectory()) {
      await copyAssets(srcPath, destPath);
    } else {
      await mkdir(destDir, { recursive: true });
      await cp(srcPath, destPath, { force: true });
    }
  }
}

async function main() {
  const start = Date.now();

  // Collect entry points from src/ and packages/*/src/
  const srcFiles = await collectFiles(join(ROOT, 'src'));

  const packagesDir = join(ROOT, 'packages');
  const pkgEntries = await readdir(packagesDir, { withFileTypes: true });
  const packageFiles = [];
  for (const entry of pkgEntries) {
    if (!entry.isDirectory()) continue;
    const pkgSrc = join(packagesDir, entry.name, 'src');
    packageFiles.push(...await collectFiles(pkgSrc));
  }

  // Also compile web/lib/ — some tests import from ../../web/lib/
  const webLibFiles = await collectFiles(join(ROOT, 'web', 'lib'));

  const entryPoints = [...srcFiles, ...packageFiles, ...webLibFiles];
  console.log(`Compiling ${entryPoints.length} files to dist-test/...`);

  // bundle:false transforms TypeScript but keeps import specifiers verbatim.
  // We post-process the output to rewrite .ts → .js in import strings.
  await esbuild.build({
    entryPoints,
    outdir: join(ROOT, 'dist-test'),
    outbase: ROOT,
    bundle: false,
    format: 'esm',
    platform: 'node',
    target: 'node22',
    sourcemap: 'inline',
    packages: 'external',
    logLevel: 'warning',
  });

  // Copy non-compiled assets from src/ to dist-test/src/ maintaining structure.
  // Tests use import.meta.url to resolve sibling .md, .yaml, .json, .ts etc.
  // Also copy original .ts files — jiti-based imports load .ts source directly.
  const srcDir = join(ROOT, 'src');
  const distSrcDir = join(ROOT, 'dist-test', 'src');
  await copyAssets(srcDir, distSrcDir);
  console.log('Copied non-TS assets and .ts source files to dist-test/src/');

  // Copy packages/*/src/ assets as well
  for (const entry of pkgEntries) {
    if (!entry.isDirectory()) continue;
    const pkgSrc = join(packagesDir, entry.name, 'src');
    const pkgDistSrc = join(ROOT, 'dist-test', 'packages', entry.name, 'src');
    await copyAssets(pkgSrc, pkgDistSrc);
  }

  // Copy web/lib/ assets (tests import from ../../web/lib/ relative to dist-test/src/tests/)
  await copyAssets(join(ROOT, 'web', 'lib'), join(ROOT, 'dist-test', 'web', 'lib'));

  // Copy web/components/ assets (xterm-theme test reads shell-terminal.tsx via import.meta.dirname)
  await copyAssets(join(ROOT, 'web', 'components'), join(ROOT, 'dist-test', 'web', 'components'));

  // Copy scripts/ non-TS files (.cjs etc) — some tests require() scripts directly
  await copyAssets(join(ROOT, 'scripts'), join(ROOT, 'dist-test', 'scripts'));

  // Copy root package.json — some tests read it to check version/engines fields
  await cp(join(ROOT, 'package.json'), join(ROOT, 'dist-test', 'package.json'), { force: true });

  // Copy root dist/ into dist-test/dist/ — some tests compute projectRoot as
  // 3 levels up from dist-test/src/tests/ which lands at dist-test/, then
  // import from dist/mcp-server.js etc.
  const rootDistDir = join(ROOT, 'dist');
  const distTestDistDir = join(ROOT, 'dist-test', 'dist');
  await copyAssets(rootDistDir, distTestDistDir);

  // Post-process: rewrite .ts import specifiers to .js in all compiled JS files.
  // esbuild with bundle:false preserves original specifiers; Node can't load .ts.
  const compiledJsFiles = await collectFiles(join(ROOT, 'dist-test'), ['.js']);
  // Regex matches .ts in from/import() strings but not sourceMappingURL comments
  const tsImportRe = /(from\s+["'])(\.\.?\/[^"']*?)\.ts(["'])/g;
  const tsDynImportRe = /(import\(["'])(\.\.?\/[^"']*?)\.ts(["'])\)/g;

  let rewritten = 0;
  await Promise.all(compiledJsFiles.map(async (file) => {
    const src = await readFile(file, 'utf-8');
    const out = src
      .replace(tsImportRe,   (_, a, b, c) => `${a}${b}.js${c}`)
      .replace(tsDynImportRe, (_, a, b, c) => `${a}${b}.js${c})`);
    if (out !== src) {
      await writeFile(file, out, 'utf-8');
      rewritten++;
    }
  }));
  if (rewritten > 0) {
    console.log(`Rewrote .ts → .js imports in ${rewritten} files`);
  }

  // Remove stale compiled test files: dist-test entries whose source no longer exists
  // in a non-integration source directory (e.g. test moved to integration/).
  // Only cleans *.test.js and *.test.ts files to avoid touching non-test outputs.
  const { rm } = await import('node:fs/promises');
  const { existsSync } = await import('node:fs');
  const testDirsToClean = [
    [join(ROOT, 'dist-test', 'src', 'tests'), join(ROOT, 'src', 'tests')],
    [join(ROOT, 'dist-test', 'src', 'resources', 'extensions', 'sf', 'tests'),
     join(ROOT, 'src', 'resources', 'extensions', 'sf', 'tests')],
  ];
  let staleCleaned = 0;
  for (const [distDir, srcDir] of testDirsToClean) {
    let distEntries;
    try { distEntries = await readdir(distDir, { withFileTypes: true }); } catch { continue; }
    for (const entry of distEntries) {
      if (!entry.isFile()) continue;
      if (!entry.name.match(/\.test\.(js|ts)$/)) continue;
      const stem = entry.name.replace(/\.(js|ts)$/, '');
      // Source could be .ts or .mjs (esbuild compiles both to .js)
      const hasTsSrc = existsSync(join(srcDir, stem + '.ts'));
      const hasMjsSrc = existsSync(join(srcDir, stem + '.mjs'));
      if (!hasTsSrc && !hasMjsSrc) {
        await rm(join(distDir, entry.name));
        staleCleaned++;
      }
    }
  }
  if (staleCleaned > 0) {
    console.log(`Removed ${staleCleaned} stale compiled test files from dist-test/`);
  }

  // Ensure dist-test/node_modules exists so resource-loader.ts (which computes
  // packageRoot from import.meta.url) resolves gsdNodeModules to a real path.
  // Without this, initResources creates dangling symlinks in test environments.
  const distNodeModules = join(ROOT, 'dist-test', 'node_modules');
  if (!existsSync(distNodeModules)) {
    symlinkSync(join(ROOT, 'node_modules'), distNodeModules);
  }

  const elapsed = ((Date.now() - start) / 1000).toFixed(2);
  console.log(`Done in ${elapsed}s`);
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});
