#!/usr/bin/env node

/* eslint-env node */
/* eslint-disable no-console, no-undef, no-unused-vars */

/**
 * Memory-safe test runner
 * Automatically handles test batching and memory management
 */

const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

// Test batch configuration
const TEST_BATCHES = [
  {
    name: 'Security & Cache',
    paths: ['tests/security/', 'tests/cache/', 'tests/server/'],
    timeout: 60000,
  },
  {
    name: 'Client & Config & Utils',
    paths: ['tests/client/', 'tests/config/', 'tests/utils/'],
    timeout: 90000,
  },
  {
    name: 'Tools & Performance',
    paths: ['tests/tools/', 'tests/performance/'],
    timeout: 120000,
  },
  {
    name: 'Root & Docs',
    paths: ['tests/*.test.js', 'tests/docs/'],
    timeout: 60000,
  },
];

// Node.js memory options (only heap-size flags are allowed in NODE_OPTIONS)
const NODE_OPTIONS = [
  '--max-old-space-size=4096',
  '--max-semi-space-size=256',
].join(' ');

// Expand a simple glob like "tests/*.test.js" into real file paths.
// Handles only single-level wildcards (no **). Passes non-glob paths through.
function expandGlob(pattern) {
  if (!pattern.includes('*')) return [pattern];
  const dir = path.dirname(pattern);
  const ext = path.extname(pattern);
  const prefix = path.basename(pattern).replace(/\*.*/, '');
  try {
    return fs
      .readdirSync(dir)
      .filter((f) => f.startsWith(prefix) && f.endsWith(ext))
      .map((f) => path.join(dir, f));
  } catch {
    return [];
  }
}

class TestRunner {
  constructor() {
    this.results = [];
    this.totalTests = 0;
    this.totalPassed = 0;
    this.totalFailed = 0;
  }

  async runBatch(batch) {
    console.log(`\n🧪 Running ${batch.name}...`);
    console.log(`   Paths: ${batch.paths.join(', ')}`);

    return new Promise((resolve, reject) => {
      // Build first
      try {
        console.log('   Building...');
        execSync('npm run build', { stdio: 'pipe' });
      } catch (buildError) {
        console.error(`   ❌ Build failed: ${buildError.message}`);
        resolve({ success: false, error: 'Build failed', tests: 0, passed: 0, failed: 0 });
        return;
      }

      // Expand any glob patterns (spawn doesn't go through a shell)
      const expandedPaths = batch.paths.flatMap(expandGlob).filter(Boolean);

      // Run tests with memory limits
      const vitestCmd = [
        'vitest', 'run',
        '--config', 'vitest.memory-safe.config.ts',
        '--no-coverage',
        ...expandedPaths
      ];

      const child = spawn('npx', vitestCmd, {
        env: {
          ...process.env,
          NODE_OPTIONS,
        },
        stdio: ['inherit', 'pipe', 'pipe'],
        timeout: batch.timeout,
      });

      let stdout = '';
      let stderr = '';

      child.stdout.on('data', (data) => {
        const output = data.toString();
        stdout += output;
        // Show real-time output for important messages
        if (output.includes('✓') || output.includes('×') || output.includes('Test Files')) {
          process.stdout.write(output);
        }
      });

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

      child.on('close', (code) => {
        // Parse results from stdout - handle multiple formats
        let testFiles = 0;
        let tests = 0;
        let passed = 0;
        let failed = 0;

        // Try different patterns for test results
        const testFilesPassedMatch = stdout.match(/Test Files\s+(\d+)\s+passed/);
        const testFilesTotalMatch = stdout.match(/Test Files\s+(\d+)\s+/);

        if (testFilesPassedMatch) {
          testFiles = parseInt(testFilesPassedMatch[1]) || 0;
          passed = testFiles; // If no individual test count, use test files count
        } else if (testFilesTotalMatch) {
          testFiles = parseInt(testFilesTotalMatch[1]) || 0;
          passed = testFiles;
        }

        // Match patterns like "Tests  248 passed" or "Tests  110 passed (110)"
        const testsPassedMatch = stdout.match(/Tests\s+(\d+)\s+passed/);
        const testsFailedMatch = stdout.match(/Tests\s+(\d+)\s+failed/);
        const testsInParensMatch = stdout.match(/Tests\s+(\d+)\s+passed\s+\((\d+)\)/);

        if (testsInParensMatch) {
          // Use the number in parentheses as it's more accurate
          passed = parseInt(testsInParensMatch[2]) || 0;
          tests = passed;
        } else if (testsPassedMatch) {
          passed = parseInt(testsPassedMatch[1]) || 0;
          tests = passed;
        }

        if (testsFailedMatch) {
          failed = parseInt(testsFailedMatch[1]) || 0;
          tests = passed + failed;
        }

        // If we still have no test count but have test files, use a reasonable estimate
        if (tests === 0 && testFiles > 0) {
          tests = passed = testFiles * 10; // Rough estimate
        }

        const success = code === 0;
        const result = {
          batch: batch.name,
          success,
          code,
          testFiles,
          tests,
          passed,
          failed,
          stdout: stdout.slice(-1000), // Keep last 1000 chars for debugging
          stderr: stderr.slice(-1000),
        };

        if (success) {
          console.log(`   ✅ ${batch.name}: ${tests} tests passed`);
        } else {
          console.log(`   ❌ ${batch.name}: ${failed} failed, ${passed} passed (${tests} total)`);
        }

        this.totalTests += tests;
        this.totalPassed += passed;
        this.totalFailed += failed;

        resolve(result);
      });

      child.on('error', (error) => {
        console.log(`   ❌ ${batch.name}: Process error - ${error.message}`);
        resolve({
          batch: batch.name,
          success: false,
          error: error.message,
          tests: 0,
          passed: 0,
          failed: 0,
        });
      });
    });
  }

  async runAllBatches() {
    console.log('🚀 Starting memory-safe test runner...\n');

    for (const batch of TEST_BATCHES) {
      const result = await this.runBatch(batch);
      this.results.push(result);

      // Force garbage collection between batches
      if (global.gc) {
        global.gc();
      }

      // Small delay between batches
      await new Promise((resolve) => setTimeout(resolve, 2000));
    }

    this.printSummary();
    return this.totalFailed === 0;
  }

  printSummary() {
    console.log('\n📊 Test Summary:');
    console.log('================');

    let successfulBatches = 0;

    for (const result of this.results) {
      const status = result.success ? '✅' : '❌';
      console.log(`${status} ${result.batch}: ${result.passed || 0} passed, ${result.failed || 0} failed`);
      if (result.success) successfulBatches++;
    }

    console.log('\n📈 Overall Results:');
    console.log(`   Batches: ${successfulBatches}/${this.results.length} successful`);
    console.log(`   Tests: ${this.totalPassed} passed, ${this.totalFailed} failed`);
    console.log(`   Total: ${this.totalTests} tests`);

    const anyBatchFailed = this.results.some((r) => !r.success);
    if (this.totalFailed > 0 || anyBatchFailed) {
      console.log('\n❌ Some tests failed. Check individual batch output above.');
      process.exit(1);
    } else {
      console.log('\n🎉 All tests passed!');
    }
  }
}

// Run if called directly
if (require.main === module) {
  const runner = new TestRunner();
  runner.runAllBatches().catch((error) => {
    console.error('Test runner failed:', error);
    process.exit(1);
  });
}

module.exports = TestRunner;
