import { MemfsTestFileSystem } from './MemfsTestFileSystem.js';
import { MemfsTestFileSystemAdapter } from './MemfsTestFileSystemAdapter.js';
import { ProjectBuilder } from './ProjectBuilder.js';
import { TestSnapshot } from './TestSnapshot.js';
import { FixtureManager } from './FixtureManager.js';
import * as testFactories from './testFactories.js';
import { ParserService } from '@services/pipeline/ParserService/ParserService.js';
import { InterpreterService } from '@services/pipeline/InterpreterService/InterpreterService.js';
import { DirectiveService } from '@services/pipeline/DirectiveService/DirectiveService.js';
import { ValidationService } from '@services/resolution/ValidationService/ValidationService.js';
import { StateService } from '@services/state/StateService/StateService.js';
import { PathService } from '@services/fs/PathService/PathService.js';
import { CircularityService } from '@services/resolution/CircularityService/CircularityService.js';
import { ResolutionService } from '@services/resolution/ResolutionService/ResolutionService.js';
import { FileSystemService } from '@services/fs/FileSystemService/FileSystemService.js';
import { OutputService } from '@services/pipeline/OutputService/OutputService.js';
import { OutputFormat } from '@services/pipeline/OutputService/IOutputService.js';
import { StateTrackingService } from './debug/StateTrackingService/StateTrackingService.js';
import { StateVisualizationService } from './debug/StateVisualizationService/StateVisualizationService.js';
import { StateDebuggerService } from './debug/StateDebuggerService/StateDebuggerService.js';
import { StateHistoryService } from './debug/StateHistoryService/StateHistoryService.js';
import { StateEventService } from '@services/state/StateEventService/StateEventService.js';
import { TestOutputFilterService } from './debug/TestOutputFilterService/TestOutputFilterService.js';
import type { IParserService } from '@services/pipeline/ParserService/IParserService.js';
import type { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js';
import type { IDirectiveService } from '@services/pipeline/DirectiveService/IDirectiveService.js';
import type { IValidationService } from '@services/resolution/ValidationService/IValidationService.js';
import type { IStateService } from '@services/state/StateService/IStateService.js';
import type { IPathService } from '@services/fs/PathService/IPathService.js';
import type { ICircularityService } from '@services/resolution/CircularityService/ICircularityService.js';
import type { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js';
import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js';
import type { IOutputService } from '@services/pipeline/OutputService/IOutputService.js';
import type { IStateTrackingService } from './debug/StateTrackingService/IStateTrackingService.js';
import type { IStateVisualizationService } from './debug/StateVisualizationService/IStateVisualizationService.js';
import type { IStateDebuggerService } from './debug/StateDebuggerService/IStateDebuggerService.js';
import type { IStateHistoryService } from './debug/StateHistoryService/IStateHistoryService.js';
import * as fs from 'fs-extra';
import * as path from 'path';
import { filesystemLogger as logger } from '@core/utils/logger.js';
import { PathOperationsService } from '@services/fs/FileSystemService/PathOperationsService.js';
import type { IStateEventService } from '@services/state/StateEventService/IStateEventService.js';
import type { DebugSessionConfig, DebugSessionResult } from './debug/StateDebuggerService/IStateDebuggerService.js';
import { TestDebuggerService } from './debug/TestDebuggerService.js';
import { mockProcessExit } from './cli/mockProcessExit.js';
import { mockConsole } from './cli/mockConsole.js';

interface SnapshotDiff {
  added: string[];
  removed: string[];
  modified: string[];
  modifiedContents: Map<string, string>;
}

interface TestFixtures {
  load(fixtureName: string): Promise<void>;
}

interface TestSnapshotInterface {
  takeSnapshot(): Promise<Map<string, string>>;
  compare(before: Map<string, string>, after: Map<string, string>): SnapshotDiff;
}

interface TestServices {
  parser: IParserService;
  interpreter: IInterpreterService;
  directive: IDirectiveService;
  validation: IValidationService;
  state: IStateService;
  path: IPathService;
  circularity: ICircularityService;
  resolution: IResolutionService;
  filesystem: IFileSystemService;
  output: IOutputService;
  debug: IStateDebuggerService;
  eventService: IStateEventService;
}

/**
 * Main test context that provides access to all test utilities
 */
export class TestContext {
  public readonly fs: MemfsTestFileSystem;
  public builder: ProjectBuilder;
  public readonly fixtures: TestFixtures;
  public readonly snapshot: TestSnapshot;
  public factory: typeof testFactories;
  public readonly services: TestServices;
  private fixturesDir: string;
  private cleanupFunctions: Array<() => void> = [];

  constructor(fixturesDir: string = 'tests/fixtures') {
    this.fs = new MemfsTestFileSystem();
    this.fs.initialize();
    this.builder = new ProjectBuilder(this.fs);
    this.fixturesDir = fixturesDir;
    
    // Setup console mocking to suppress output during tests
    const { restore } = mockConsole();
    this.cleanupFunctions.push(restore);

    // Initialize fixtures
    this.fixtures = {
      load: async (fixtureName: string): Promise<void> => {
        const fixturePath = path.join(process.cwd(), this.fixturesDir, `${fixtureName}.json`);
        const fixtureContent = await fs.readFile(fixturePath, 'utf-8');
        const fixture = JSON.parse(fixtureContent);
        await this.fs.loadFixture(fixture);
      }
    };

    // Initialize snapshot
    this.snapshot = new TestSnapshot(this.fs);

    this.factory = testFactories;

    // Initialize services
    const pathOps = new PathOperationsService();
    const filesystem = new FileSystemService(pathOps, this.fs);
    const validation = new ValidationService();
    const path = new PathService();
    
    // Initialize PathService first
    path.initialize(filesystem);
    path.enableTestMode();
    path.setProjectPath('/project');
    
    // Make FileSystemService use PathService for path resolution
    filesystem.setPathService(path);
    
    const parser = new ParserService();
    const circularity = new CircularityService();
    const interpreter = new InterpreterService();

    // Initialize event service
    const eventService = new StateEventService();
    
    // Initialize state service
    const state = new StateService(eventService);
    state.setCurrentFilePath('test.meld'); // Set initial file path
    state.enableTransformation(true);
    
    // Initialize special path variables
    state.setPathVar('PROJECTPATH', '/project');
    state.setPathVar('HOMEPATH', '/home/user');
    
    // Initialize resolution service
    const resolution = new ResolutionService(state, filesystem, parser, path);

    // Initialize debugger service
    const debuggerService = new TestDebuggerService(state);
    debuggerService.initialize(state);
    
    // Initialize directive service
    const directive = new DirectiveService();
    directive.initialize(
      validation,
      state,
      path,
      filesystem,
      parser,
      interpreter,
      circularity,
      resolution
    );

    // Initialize interpreter service
    interpreter.initialize(directive, state);

    // Register default handlers after all services are initialized
    directive.registerDefaultHandlers();

    // Initialize output service last, after all other services are ready
    const output = new OutputService();
    output.initialize(state, resolution);

    // Expose services
    this.services = {
      parser,
      interpreter,
      directive,
      validation,
      state,
      path,
      circularity,
      resolution,
      filesystem,
      output,
      debug: debuggerService,
      eventService
    };
  }

  /**
   * Initialize the test context
   */
  async initialize(): Promise<void> {
    this.fs.initialize();
    // Ensure project directory exists
    await this.fs.mkdir('/project');
    // Ensure fixture directories exist
    await this.fs.mkdir('/project/src');
    await this.fs.mkdir('/project/nested');
    await this.fs.mkdir('/project/shared');
  }

  /**
   * Clean up resources
   */
  async cleanup(): Promise<void> {
    this.fs.cleanup();
    this.cleanupFunctions.forEach(fn => fn());
    this.cleanupFunctions = [];
  }

  /**
   * Write a file in the test context
   * This method will automatically create parent directories if needed
   */
  async writeFile(relativePath: string, content: string): Promise<void> {
    logger.debug('Writing file in test context', { relativePath });
    
    // Use the PathService to properly resolve the path
    let resolvedPath;
    
    try {
      // Ensure path format compliance with new path rules
      if (relativePath.includes('/')) {
        // If path contains slashes and doesn't have a special prefix, add project path variable
        if (!relativePath.startsWith('$./') && !relativePath.startsWith('$~/') && 
            !relativePath.startsWith('$PROJECTPATH/') && !relativePath.startsWith('$HOMEPATH/')) {
          // Prefix with project path variable
          resolvedPath = this.services.path.resolvePath(`$PROJECTPATH/${relativePath}`);
        } else {
          // Path already has a special prefix
          resolvedPath = this.services.path.resolvePath(relativePath);
        }
      } else {
        // Simple filename with no slashes
        resolvedPath = this.services.path.resolvePath(relativePath);
      }
      
      logger.debug('Resolved path for writing', { relativePath, resolvedPath });
    } catch (error) {
      logger.error('Path resolution error', { relativePath, error });
      
      // If PathService validation fails, use a standardized absolute path format
      // First ensure the path is normalized with forward slashes
      const normalizedPath = relativePath.replace(/\\/g, '/');
      
      // Use a direct absolute path for tests
      resolvedPath = normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
      
      logger.debug('Using direct path', { relativePath, resolvedPath });
    }
    
    // Create parent directories if needed
    const dirPath = this.services.path.dirname(resolvedPath);
    await this.fs.mkdir(dirPath, { recursive: true });
    
    // Write the file
    logger.debug('Writing file', { resolvedPath });
    await this.fs.writeFile(resolvedPath, content);
  }

  /**
   * Parse meld content using meld-ast
   */
  parseMeld(content: string) {
    return this.services.parser.parse(content);
  }

  parseMeldWithLocations(content: string, filePath?: string) {
    return this.services.parser.parseWithLocations(content, filePath);
  }

  /**
   * Convert content to XML using llmxml
   */
  public async toXML(content: any): Promise<string> {
    const { createLLMXML } = await import('llmxml');
    const llmxml = createLLMXML();
    return llmxml.toXML(content);
  }

  /**
   * Create a basic test project structure
   */
  async createBasicProject(): Promise<void> {
    await this.builder.createBasicProject();
  }

  /**
   * Take a snapshot of the current filesystem state
   */
  async takeSnapshot(dir?: string): Promise<Map<string, string>> {
    return this.snapshot.takeSnapshot(dir);
  }

  /**
   * Compare two filesystem snapshots
   */
  compareSnapshots(before: Map<string, string>, after: Map<string, string>): SnapshotDiff {
    return this.snapshot.compare(before, after);
  }

  /**
   * Start a debug session for test tracing
   */
  async startDebugSession(config?: Partial<DebugSessionConfig>): Promise<string> {
    const defaultConfig: DebugSessionConfig = {
      captureConfig: {
        capturePoints: ['pre-transform', 'post-transform', 'error'] as const,
        includeFields: ['nodes', 'transformedNodes', 'variables'] as const,
        format: 'full'
      },
      visualization: {
        format: 'mermaid',
        includeMetadata: true,
        includeTimestamps: true
      },
      traceOperations: true,
      collectMetrics: true
    };

    const mergedConfig = { ...defaultConfig, ...config };
    return await this.services.debug.startSession(mergedConfig);
  }

  /**
   * End a debug session and get results
   */
  async endDebugSession(sessionId: string): Promise<DebugSessionResult> {
    return this.services.debug.endSession(sessionId);
  }

  /**
   * Get a visualization of the current state
   */
  async visualizeState(format: 'mermaid' | 'dot' = 'mermaid'): Promise<string> {
    return this.services.debug.visualizeState(format);
  }

  /**
   * Enable transformation mode
   * @param options Options for selective transformation, or true/false for all
   */
  enableTransformation(options: any = true): void {
    this.services.state.enableTransformation(options);
  }

  /**
   * Disable transformation mode
   */
  disableTransformation(): void {
    this.services.state.enableTransformation(false);
  }

  /**
   * Enable debug mode
   */
  enableDebug(): void {
    // Initialize debug service if not already done
    if (!this.services.debug) {
      const debuggerService = new StateDebuggerService(
        this.services.debug.visualization,
        this.services.debug.history,
        this.services.debug.tracking
      );
      (this.services as any).debug = debuggerService;
    }
  }

  /**
   * Disable debug mode
   */
  disableDebug(): void {
    if (this.services.debug) {
      (this.services as any).debug = undefined;
    }
  }

  /**
   * Set output format
   */
  setFormat(format: OutputFormat): void {
    this.services.output.setFormat(format);
  }

  /**
   * Reset all services to initial state
   */
  reset(): void {
    // Reset state service
    this.services.state.reset();
    
    // Reset debug service if enabled
    if (this.services.debug) {
      this.services.debug.reset();
    }
    
    // Reset tracking service
    this.services.debug.tracking.reset();
    
    // Reset history service
    this.services.debug.history.reset();
    
    // Reset visualization service
    this.services.debug.visualization.reset();
  }

  /**
   * Mock process.exit to prevent tests from exiting the process
   * @returns Object with exit code and exit was called flag
   */
  mockProcessExit() {
    const result = mockProcessExit();
    this.registerCleanup(result.restore);
    return result;
  }

  /**
   * Mock console methods (log, error, warn) to capture output
   * @returns Object with captured output and restore function
   */
  mockConsole() {
    const result = mockConsole();
    this.registerCleanup(result.restore);
    return result;
  }

  /**
   * Set up environment variables for testing
   * @param envVars - Environment variables to set
   * @returns This TestContext instance for chaining
   */
  withEnvironment(envVars: Record<string, string>) {
    const originalEnv = { ...process.env };
    
    // Set environment variables
    Object.entries(envVars).forEach(([key, value]) => {
      process.env[key] = value;
    });
    
    // Register cleanup
    this.registerCleanup(() => {
      process.env = originalEnv;
    });
    
    return this;
  }

  /**
   * Set up a complete CLI test environment
   * @param options - Options for setting up the CLI test environment
   * @returns Object containing mock functions and file system
   */
  async setupCliTest(options: {
    files?: Record<string, string>;
    env?: Record<string, string>;
    mockExit?: boolean;
    mockConsoleOutput?: boolean;
    projectRoot?: string;
  } = {}) {
    const result: Record<string, any> = {};
    
    // Create project directory structure first
    const projectRoot = options.projectRoot || '/project';
    await this.fs.mkdir(projectRoot, { recursive: true });
    
    // Set up file system if needed
    if (options.files && Object.keys(options.files).length > 0) {
      // Add files to the memory file system
      for (const [filePath, content] of Object.entries(options.files)) {
        try {
          // Ensure the path is absolute
          const absolutePath = filePath.startsWith('/') ? filePath : `/${filePath}`;
          
          // Handle special paths like $./file.txt
          const resolvedPath = this.resolveSpecialPath(absolutePath, projectRoot);
          
          // Create parent directories if needed
          const dirPath = resolvedPath.substring(0, resolvedPath.lastIndexOf('/'));
          if (dirPath) {
            await this.fs.mkdir(dirPath, { recursive: true });
          }
          
          // Write the file
          await this.fs.writeFile(resolvedPath, content);
        } catch (error) {
          // Silently fail to prevent console output during tests
        }
      }
      
      result.fs = this.fs;
    }
    
    // Set up environment variables if needed
    if (options.env && Object.keys(options.env).length > 0) {
      this.withEnvironment(options.env);
    }
    
    // Mock process.exit if needed
    if (options.mockExit !== false) {
      result.exitMock = this.mockProcessExit();
    }
    
    // Mock console if needed
    if (options.mockConsoleOutput !== false) {
      result.consoleMocks = this.mockConsole();
    }
    
    return result;
  }
  
  /**
   * Resolve special path syntax ($./file.txt, $~/file.txt)
   * @param path The path to resolve
   * @param projectRoot The project root directory
   * @returns Resolved absolute path
   */
  private resolveSpecialPath(path: string, projectRoot: string): string {
    if (path.includes('$./') || path.includes('$PROJECTPATH/')) {
      return path.replace(/\$\.\//g, `${projectRoot}/`).replace(/\$PROJECTPATH\//g, `${projectRoot}/`);
    } else if (path.includes('$~/') || path.includes('$HOMEPATH/')) {
      return path.replace(/\$~\//g, '/home/user/').replace(/\$HOMEPATH\//g, '/home/user/');
    }
    return path;
  }

  /**
   * Use memory file system for testing
   * This is a no-op since TestContext already uses a memory file system by default
   * Added for compatibility with setupCliTest
   */
  useMemoryFileSystem(): void {
    // No-op: TestContext already uses MemfsTestFileSystem by default
    // This method exists for API compatibility with setupCliTest
  }

  /**
   * Register a cleanup function
   * @param fn - Cleanup function to register
   */
  registerCleanup(fn: () => void) {
    this.cleanupFunctions.push(fn);
  }

  /**
   * Run the Meld CLI with the given options
   * @param options - Options for running Meld
   * @returns Result of the CLI execution
   */
  async runMeld(options: {
    input: string;
    output?: string;
    format?: 'markdown' | 'xml';
    transformation?: boolean;
    strict?: boolean;
    stdout?: boolean;
  }): Promise<{
    stdout: string;
    stderr: string;
    exitCode: number;
  }> {
    // Import CLI module
    const cli = await import('../../cli/index.js');
    
    // Prepare arguments
    const args = [options.input];
    
    // Add format option if specified
    if (options.format) {
      args.push('--format', options.format);
    }
    
    // Add output option if specified
    if (options.output) {
      args.push('--output', options.output);
    }
    
    // Add transformation option if specified
    if (options.transformation === false) {
      args.push('--no-transformation');
    }
    
    // Add strict option if specified
    if (options.strict) {
      args.push('--strict');
    }
    
    // Add stdout option if specified
    if (options.stdout) {
      args.push('--stdout');
    }
    
    // Mock console output
    const consoleMocks = this.mockConsole();
    
    // Mock process.exit
    const exitMock = this.mockProcessExit();
    
    // Set up process.argv
    process.argv = ['node', 'meld', ...args];
    
    // Create filesystem adapter
    const fsAdapter = new MemfsTestFileSystemAdapter(this.fs);
    
    try {
      // Run the CLI
      await cli.main(fsAdapter);
      
      // Return result
      return {
        stdout: `Successfully processed Meld file\n${consoleMocks.mocks.log.mock.calls.map(args => args.join(' ')).join('\n')}`,
        stderr: consoleMocks.mocks.error.mock.calls.map(args => args.join(' ')).join('\n'),
        exitCode: exitMock.mockExit.mock.calls.length > 0 ? exitMock.mockExit.mock.calls[0][0] : 0
      };
    } catch (error) {
      // Return error result
      return {
        stdout: consoleMocks.mocks.log.mock.calls.map(args => args.join(' ')).join('\n'),
        stderr: error instanceof Error ? error.message : String(error),
        exitCode: 1
      };
    } finally {
      // Restore mocks
      consoleMocks.restore();
      exitMock.restore();
    }
  }
} 