import { coloredString as cs } from '@push.rocks/consolecolor';
import * as plugins from './tstest.plugins.js';
import * as fs from 'fs';
import * as path from 'path';

export interface LogOptions {
  quiet?: boolean;
  verbose?: boolean;
  noColor?: boolean;
  json?: boolean;
  logFile?: boolean;
}

export interface TestFileResult {
  file: string;
  passed: number;
  failed: number;
  total: number;
  duration: number;
  tests: Array<{
    name: string;
    passed: boolean;
    duration: number;
    error?: string;
  }>;
}

export interface TestSummary {
  totalFiles: number;
  totalTests: number;
  totalPassed: number;
  totalFailed: number;
  totalSkipped: number;
  totalDuration: number;
  fileResults: TestFileResult[];
  skippedFiles: string[];
}

export class TsTestLogger {
  public readonly options: LogOptions;
  private startTime: number;
  private fileResults: TestFileResult[] = [];
  private currentFileResult: TestFileResult | null = null;
  private currentTestLogFile: string | null = null;
  private currentTestLogs: string[] = []; // Buffer for current test logs
  private currentTestFailed: boolean = false;
  
  constructor(options: LogOptions = {}) {
    this.options = options;
    this.startTime = Date.now();
  }
  
  private format(text: string, color?: string): string {
    if (this.options.noColor || !color) {
      return text;
    }
    return cs(text, color as any);
  }
  
  private log(message: string) {
    if (this.options.json) {
      // For JSON mode, skip console output
      // JSON output is handled by logJson method
      return;
    }
    
    console.log(message);
    
    // Log to the current test file log if we're in a test and --logfile is specified
    if (this.currentTestLogFile) {
      this.logToTestFile(message);
    }
  }
  
  private logToFile(message: string) {
    // This method is no longer used since we use logToTestFile for individual test logs
    // Keeping it for potential future use with a global log file
  }
  
  private logToTestFile(message: string) {
    try {
      // Remove ANSI color codes for file logging
      const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
      
      // Append to test log file
      fs.appendFileSync(this.currentTestLogFile, cleanMessage + '\n');
    } catch (error) {
      // Silently fail to avoid disrupting the test run
    }
  }
  
  private logJson(data: any) {
    const jsonString = JSON.stringify(data);
    console.log(jsonString);
    
    // Also log to test file if --logfile is specified
    if (this.currentTestLogFile) {
      this.logToTestFile(jsonString);
    }
  }
  
  // Section separators
  sectionStart(title: string) {
    if (this.options.quiet || this.options.json) return;
    this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
  }
  
  sectionEnd() {
    if (this.options.quiet || this.options.json) return;
    this.log(this.format('─'.repeat(50), 'dim'));
  }
  
  // Progress indication
  progress(current: number, total: number, message: string) {
    if (this.options.quiet || this.options.json) return;
    const percentage = Math.round((current / total) * 100);
    const filled = Math.round((current / total) * 20);
    const empty = 20 - filled;
    
    this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
    this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
  }
  
  // Test discovery
  testDiscovery(count: number, pattern: string, executionMode: string) {
    if (this.options.json) {
      this.logJson({ event: 'discovery', count, pattern, executionMode });
      return;
    }
    
    if (this.options.quiet) {
      this.log(`Found ${count} tests`);
    } else {
      this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
      this.log(this.format(`   Mode: ${executionMode}`, 'dim'));
      this.log(this.format(`   Pattern: ${pattern}`, 'dim'));
      this.log(this.format(`   Found: ${count} test file(s)`, 'green'));
    }
  }
  
  // Test execution
  testFileStart(filename: string, runtime: string, index: number, total: number) {
    this.currentFileResult = {
      file: filename,
      passed: 0,
      failed: 0,
      total: 0,
      duration: 0,
      tests: []
    };
    
    // Reset test-specific state
    this.currentTestLogs = [];
    this.currentTestFailed = false;
    
    // Only set up test log file if --logfile option is specified
    if (this.options.logFile) {
      // Create a safe filename that preserves directory structure
      // Convert relative path to a flat filename by replacing separators with __
      const relativeFilename = path.relative(process.cwd(), filename);
      const safeFilename = relativeFilename
        .replace(/\\/g, '/') // Normalize Windows paths
        .replace(/\//g, '__') // Replace path separators with double underscores
        .replace(/\.ts$/, '') // Remove .ts extension
        .replace(/^\.\.__|^\.__|^__/, ''); // Clean up leading separators from relative paths
      
      this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}.log`);
      
      // Ensure the directory exists
      const logDir = path.dirname(this.currentTestLogFile);
      if (!fs.existsSync(logDir)) {
        fs.mkdirSync(logDir, { recursive: true });
      }
      
      // Clear the log file for this test
      fs.writeFileSync(this.currentTestLogFile, '');
    }
    
    if (this.options.json) {
      this.logJson({ event: 'fileStart', filename, runtime, index, total });
      return;
    }
    
    if (this.options.quiet) return;
    
    this.log(this.format(`\n▶️  ${filename} (${index}/${total})`, 'blue'));
    this.log(this.format(`   Runtime: ${runtime}`, 'dim'));
  }
  
  testResult(testName: string, passed: boolean, duration: number, error?: string) {
    if (this.currentFileResult) {
      this.currentFileResult.tests.push({ name: testName, passed, duration, error });
      this.currentFileResult.total++;
      if (passed) {
        this.currentFileResult.passed++;
      } else {
        this.currentFileResult.failed++;
        this.currentTestFailed = true;
      }
      this.currentFileResult.duration += duration;
    }
    
    if (this.options.json) {
      this.logJson({ event: 'testResult', testName, passed, duration, error });
      return;
    }
    
    // If test failed and we have buffered logs, show them now
    if (!passed && this.currentTestLogs.length > 0 && !this.options.verbose) {
      this.log(this.format('   📋 Console output from failed test:', 'yellow'));
      this.currentTestLogs.forEach(logMessage => {
        this.log(this.format(`   ${logMessage}`, 'dim'));
      });
    }
    
    const icon = passed ? '✅' : '❌';
    const color = passed ? 'green' : 'red';
    
    if (this.options.quiet) {
      this.log(`${icon} ${testName}`);
    } else {
      this.log(this.format(`   ${icon} ${testName} (${duration}ms)`, color));
      if (error && !passed) {
        this.log(this.format(`      ${error}`, 'red'));
      }
    }
    
    // Clear logs after each test
    this.currentTestLogs = [];
  }
  
  testFileEnd(passed: number, failed: number, duration: number) {
    if (this.currentFileResult) {
      this.fileResults.push(this.currentFileResult);
      this.currentFileResult = null;
    }
    
    if (this.options.json) {
      this.logJson({ event: 'fileEnd', passed, failed, duration });
      return;
    }
    
    if (!this.options.quiet) {
      const total = passed + failed;
      const status = failed === 0 ? 'PASSED' : 'FAILED';
      const color = failed === 0 ? 'green' : 'red';
      this.log(this.format(`   Summary: ${passed}/${total} ${status}`, color));
    }
    
    // If using --logfile, handle error copy and diff detection
    if (this.options.logFile && this.currentTestLogFile) {
      try {
        const logContent = fs.readFileSync(this.currentTestLogFile, 'utf-8');
        const logDir = path.dirname(this.currentTestLogFile);
        const logBasename = path.basename(this.currentTestLogFile);
        
        // Create error copy if there were failures
        if (failed > 0) {
          const errorDir = path.join(logDir, '00err');
          if (!fs.existsSync(errorDir)) {
            fs.mkdirSync(errorDir, { recursive: true });
          }
          const errorLogPath = path.join(errorDir, logBasename);
          fs.writeFileSync(errorLogPath, logContent);
        }
        
        // Check for previous version and create diff if changed
        const previousLogPath = path.join(logDir, 'previous', logBasename);
        if (fs.existsSync(previousLogPath)) {
          const previousContent = fs.readFileSync(previousLogPath, 'utf-8');
          
          // Simple check if content differs
          if (previousContent !== logContent) {
            const diffDir = path.join(logDir, '00diff');
            if (!fs.existsSync(diffDir)) {
              fs.mkdirSync(diffDir, { recursive: true });
            }
            const diffLogPath = path.join(diffDir, logBasename);
            const diffContent = this.createDiff(previousContent, logContent, logBasename);
            fs.writeFileSync(diffLogPath, diffContent);
          }
        }
      } catch (error) {
        // Silently fail to avoid disrupting the test run
      }
    }
    
    // Clear the current test log file reference only if using --logfile
    if (this.options.logFile) {
      this.currentTestLogFile = null;
    }
  }
  
  // TAP output forwarding (for TAP protocol messages)
  tapOutput(message: string, _isError: boolean = false) {
    if (this.options.json) return;
    
    // Never show raw TAP protocol messages in console
    // They are already processed by TapParser and shown in our format
    
    // Always log to test file if --logfile is specified
    if (this.currentTestLogFile) {
      this.logToTestFile(`   ${message}`);
    }
  }
  
  // Console output from test files (non-TAP output)
  testConsoleOutput(message: string) {
    if (this.options.json) return;
    
    // In verbose mode, show console output immediately
    if (this.options.verbose) {
      this.log(this.format(`   ${message}`, 'dim'));
    } else {
      // In non-verbose mode, buffer the logs
      this.currentTestLogs.push(message);
    }
    
    // Always log to test file if --logfile is specified
    if (this.currentTestLogFile) {
      this.logToTestFile(`   ${message}`);
    }
  }
  
  // Skipped test file
  testFileSkipped(filename: string, index: number, total: number, reason: string) {
    if (this.options.json) {
      this.logJson({ event: 'fileSkipped', filename, index, total, reason });
      return;
    }
    
    if (this.options.quiet) return;
    
    this.log(this.format(`\n⏭️  ${filename} (${index}/${total})`, 'yellow'));
    this.log(this.format(`   Skipped: ${reason}`, 'dim'));
  }
  
  // Browser console
  browserConsole(message: string, level: string = 'log') {
    if (this.options.json) {
      this.logJson({ event: 'browserConsole', message, level });
      return;
    }
    
    if (!this.options.quiet) {
      const prefix = level === 'error' ? '🌐❌' : '🌐';
      const color = level === 'error' ? 'red' : 'magenta';
      this.log(this.format(`   ${prefix} ${message}`, color));
    }
  }
  
  // Test error details display
  testErrorDetails(errorMessage: string) {
    if (this.options.json) {
      this.logJson({ event: 'testError', error: errorMessage });
      return;
    }
    
    if (!this.options.quiet) {
      this.log(this.format('      Error details:', 'red'));
      errorMessage.split('\n').forEach(line => {
        this.log(this.format(`        ${line}`, 'red'));
      });
    }
    
    // Always log to test file if --logfile is specified
    if (this.currentTestLogFile) {
      this.logToTestFile(`   Error: ${errorMessage}`);
    }
  }
  
  // Final summary
  summary(skippedFiles: string[] = []) {
    const totalDuration = Date.now() - this.startTime;
    const summary: TestSummary = {
      totalFiles: this.fileResults.length + skippedFiles.length,
      totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
      totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
      totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
      totalSkipped: skippedFiles.length,
      totalDuration,
      fileResults: this.fileResults,
      skippedFiles
    };
    
    if (this.options.json) {
      this.logJson({ event: 'summary', summary });
      return;
    }
    
    if (this.options.quiet) {
      const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
      this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
      return;
    }
    
    // Detailed summary
    this.log(this.format('\n📊 Test Summary', 'bold'));
    this.log(this.format('┌────────────────────────────────┐', 'dim'));
    this.log(this.format(`│ Total Files:    ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
    this.log(this.format(`│ Total Tests:    ${summary.totalTests.toString().padStart(14)} │`, 'white'));
    this.log(this.format(`│ Passed:         ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
    this.log(this.format(`│ Failed:         ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
    if (summary.totalSkipped > 0) {
      this.log(this.format(`│ Skipped:        ${summary.totalSkipped.toString().padStart(14)} │`, 'yellow'));
    }
    this.log(this.format(`│ Duration:       ${totalDuration.toString().padStart(14)}ms │`, 'white'));
    this.log(this.format('└────────────────────────────────┘', 'dim'));
    
    // File results
    if (summary.totalFailed > 0) {
      this.log(this.format('\n❌ Failed Tests:', 'red'));
      this.fileResults.forEach(fileResult => {
        if (fileResult.failed > 0) {
          this.log(this.format(`\n   ${fileResult.file}`, 'yellow'));
          fileResult.tests.filter(t => !t.passed).forEach(test => {
            this.log(this.format(`      ❌ ${test.name}`, 'red'));
            if (test.error) {
              this.log(this.format(`         ${test.error}`, 'dim'));
            }
          });
        }
      });
    }
    
    // Performance metrics
    if (this.options.verbose) {
      const avgDuration = Math.round(totalDuration / summary.totalTests);
      const slowestTest = this.fileResults
        .flatMap(r => r.tests)
        .sort((a, b) => b.duration - a.duration)[0];
      
      this.log(this.format('\n⏱️  Performance Metrics:', 'cyan'));
      this.log(this.format(`   Average per test: ${avgDuration}ms`, 'white'));
      if (slowestTest) {
        this.log(this.format(`   Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
      }
    }
    
    // Final status
    const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
    const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
    this.log(this.format(`\n${status}`, statusColor));
  }
  
  // Warning display
  warning(message: string) {
    if (this.options.json) {
      this.logJson({ event: 'warning', message });
      return;
    }
    
    if (this.options.quiet) {
      console.log(`WARNING: ${message}`);
    } else {
      this.log(this.format(`   ⚠️  ${message}`, 'orange'));
    }
  }
  
  // Error display
  error(message: string, file?: string, stack?: string) {
    if (this.options.json) {
      this.logJson({ event: 'error', message, file, stack });
      return;
    }
    
    if (this.options.quiet) {
      console.error(`ERROR: ${message}`);
    } else {
      this.log(this.format('\n⚠️  Error', 'red'));
      if (file) this.log(this.format(`   File: ${file}`, 'yellow'));
      this.log(this.format(`   ${message}`, 'red'));
      if (stack && this.options.verbose) {
        this.log(this.format(`   Stack:`, 'dim'));
        this.log(this.format(stack.split('\n').map(line => `     ${line}`).join('\n'), 'dim'));
      }
    }
  }
  
  // Create a diff between two log contents
  private createDiff(previousContent: string, currentContent: string, filename: string): string {
    const previousLines = previousContent.split('\n');
    const currentLines = currentContent.split('\n');
    
    let diff = `DIFF REPORT: ${filename}\n`;
    diff += `Generated: ${new Date().toISOString()}\n`;
    diff += '='.repeat(80) + '\n\n';
    
    // Simple line-by-line comparison
    const maxLines = Math.max(previousLines.length, currentLines.length);
    let hasChanges = false;
    
    for (let i = 0; i < maxLines; i++) {
      const prevLine = previousLines[i] || '';
      const currLine = currentLines[i] || '';
      
      if (prevLine !== currLine) {
        hasChanges = true;
        if (i < previousLines.length && i >= currentLines.length) {
          // Line was removed
          diff += `- [Line ${i + 1}] ${prevLine}\n`;
        } else if (i >= previousLines.length && i < currentLines.length) {
          // Line was added
          diff += `+ [Line ${i + 1}] ${currLine}\n`;
        } else {
          // Line was modified
          diff += `- [Line ${i + 1}] ${prevLine}\n`;
          diff += `+ [Line ${i + 1}] ${currLine}\n`;
        }
      }
    }
    
    if (!hasChanges) {
      diff += 'No changes detected.\n';
    }
    
    diff += '\n' + '='.repeat(80) + '\n';
    diff += `Previous version had ${previousLines.length} lines\n`;
    diff += `Current version has ${currentLines.length} lines\n`;
    
    return diff;
  }
  
  // Watch mode methods
  watchModeStart() {
    if (this.options.json) {
      this.logJson({ event: 'watchModeStart' });
      return;
    }
    
    this.log(this.format('\n👀 Watch Mode', 'cyan'));
    this.log(this.format('   Running tests in watch mode...', 'dim'));
    this.log(this.format('   Press Ctrl+C to exit\n', 'dim'));
  }
  
  watchModeWaiting() {
    if (this.options.json) {
      this.logJson({ event: 'watchModeWaiting' });
      return;
    }
    
    this.log(this.format('\n   Waiting for file changes...', 'dim'));
  }
  
  watchModeRerun(changedFiles: string[]) {
    if (this.options.json) {
      this.logJson({ event: 'watchModeRerun', changedFiles });
      return;
    }
    
    this.log(this.format('\n🔄 File changes detected:', 'cyan'));
    changedFiles.forEach(file => {
      this.log(this.format(`   • ${file}`, 'yellow'));
    });
    this.log(this.format('\n   Re-running tests...\n', 'dim'));
  }
  
  watchModeStop() {
    if (this.options.json) {
      this.logJson({ event: 'watchModeStop' });
      return;
    }
    
    this.log(this.format('\n\n👋 Stopping watch mode...', 'cyan'));
  }
}