import type { Mock, MockedClass, MockedFunction } from 'vitest';
import { vi } from 'vitest';
import { EventEmitter } from 'events';
import { ProcessEngine } from '../process-engine.js';
import { ProcessStore } from '../process-store.js';
import { AgentOrchestrator } from '../../agent-orchestration/orchestrator.js';
import { 
  ProcessDefinition, 
  ProcessExecution, 
  Activity,
  ExecutionStatus 
} from '../types.js';

// Mock the dependencies
vi.mock('../process-store.js');

// Mock ActivityExecutor
const mockActivityExecutor = {
  execute: vi.fn().mockResolvedValue({ 
    success: true,
    output: 'Activity completed successfully',
    executedAt: new Date().toISOString()
  }),
  setServer: vi.fn()
};

vi.mock('../activity-executor.js', () => ({
  ActivityExecutor: vi.fn(() => mockActivityExecutor)
}));

// Mock VariableResolver
const mockVariableResolver = {
  resolveVariables: vi.fn().mockImplementation((vars) => Promise.resolve(vars)),
  evaluateCondition: vi.fn().mockResolvedValue(true),
  interpolateString: vi.fn().mockImplementation((str) => str)
};

vi.mock('../variable-resolver.js', () => ({
  VariableResolver: vi.fn(() => mockVariableResolver)
}));
vi.mock('../../agent-orchestration/orchestrator.js');

describe('ProcessEngine', () => {
  describe('Constructor and basic methods', () => {
    it('should create ProcessEngine instance', () => {
      const store = {} as any;
      const orchestrator = {} as any;
      const engine = new ProcessEngine(store, orchestrator);
      
      expect(engine).toBeInstanceOf(ProcessEngine);
      expect(engine).toBeInstanceOf(EventEmitter);
    });
  });
  
  let engine: ProcessEngine;
  let mockStore: vi.Mocked<ProcessStore>;
  let mockAgentOrchestrator: vi.Mocked<AgentOrchestrator>;
  let mockExecution: ProcessExecution;
  
  beforeEach(() => {
    mockStore = {
      saveExecution: vi.fn().mockResolvedValue(undefined),
      getProcess: vi.fn(),
      saveProcess: vi.fn()
    } as any;
    
    mockAgentOrchestrator = {
      createSession: vi.fn(),
      createAgent: vi.fn(),
      createTask: vi.fn(),
      executeTask: vi.fn()
    } as any;
    
    // Reset and configure mocks
    mockActivityExecutor.execute.mockReset();
    mockActivityExecutor.execute.mockResolvedValue({ 
      success: true,
      output: 'Activity completed successfully',
      executedAt: new Date().toISOString()
    });
    
    mockVariableResolver.resolveVariables.mockReset();
    mockVariableResolver.resolveVariables.mockImplementation((vars) => Promise.resolve(vars));
    mockVariableResolver.evaluateCondition.mockReset();
    mockVariableResolver.evaluateCondition.mockResolvedValue(true);
    
    engine = new ProcessEngine(mockStore, mockAgentOrchestrator);
    
    mockExecution = {
      id: 'exec-123',
      processId: 'process-123',
      processVersion: '1.0.0',
      status: 'pending',
      triggeredBy: 'manual',
      startedAt: new Date().toISOString(),
      variables: {},
      activityResults: [],
      logs: []
    };
  });
  
  afterEach(() => {
    vi.clearAllMocks();
  });
  
  describe('executeProcess', () => {
    it('should create and execute a process successfully', async () => {
      const process: ProcessDefinition = createTestProcess();
      
      const execution = await engine.executeProcess(process);
      
      // Debug output
      if (execution.status === 'failed') {
        console.error('Execution failed with error:', execution.error);
        console.error('Activity results:', execution.activityResults);
      }
      
      expect(execution.status).toBe('completed');
      expect(execution.processId).toBe(process.id);
      expect(execution.activityResults).toHaveLength(2);
      expect(mockStore.saveExecution).toHaveBeenCalled();
    });
    
    it('should handle empty activities', async () => {
      const process: ProcessDefinition = createTestProcess({ activities: [] });
      
      const execution = await engine.executeProcess(process);
      
      expect(execution.status).toBe('completed');
      expect(execution.activityResults).toHaveLength(0);
    });
    
    it('should pass initial variables to execution', async () => {
      const process = createTestProcess();
      const initialVars = { foo: 'bar', count: 42 };
      
      const execution = await engine.executeProcess(process, 'manual', initialVars);
      
      expect(execution.variables).toMatchObject(initialVars);
    });
    
    it('should execute activities in sequence', async () => {
      const activities: Activity[] = [
        createTestActivity('act-1', 'First'),
        createTestActivity('act-2', 'Second'),
        createTestActivity('act-3', 'Third')
      ];
      
      const process = createTestProcess({ activities });
      const execution = await engine.executeProcess(process);
      
      // Verify activities executed in order
      expect(execution.activityResults[0].activityId).toBe('act-1');
      expect(execution.activityResults[1].activityId).toBe('act-2');
      expect(execution.activityResults[2].activityId).toBe('act-3');
    });
    
    it('should skip activities when condition is false', async () => {
      const activities: Activity[] = [
        createTestActivity('act-1', 'Always runs'),
        createTestActivity('act-2', 'Should skip', { condition: 'false' }),
        createTestActivity('act-3', 'Also runs')
      ];
      
      // Configure mock to return false for the second activity's condition
      mockVariableResolver.evaluateCondition.mockImplementation((condition) => {
        return Promise.resolve(condition !== 'false');
      });
      
      const process = createTestProcess({ activities });
      const execution = await engine.executeProcess(process);
      
      expect(execution.activityResults).toHaveLength(3);
      expect(execution.activityResults[1].status).toBe('skipped');
    });
    
    it('should handle activity failures', async () => {
      const failingActivity = createTestActivity('fail-1', 'Will fail');
      
      // Mock the activity executor to throw an error for this activity
      mockActivityExecutor.execute.mockRejectedValueOnce(new Error('Activity failed'));
      
      const process = createTestProcess({ 
        activities: [failingActivity] 
      });
      
      const execution = await engine.executeProcess(process);
      
      expect(execution.status).toBe('failed');
      expect(execution.error).toBeDefined();
      expect(execution.error?.message).toBe('Activity failed');
    });
    
    it('should execute success handlers on completion', async () => {
      const successActivity = createTestActivity('success-1', 'Success handler');
      const process = createTestProcess({
        activities: [createTestActivity('main-1', 'Main')],
        onSuccess: [successActivity]
      });
      
      const execution = await engine.executeProcess(process);
      
      expect(execution.status).toBe('completed');
      // Should have main activity + success handler
      expect(execution.activityResults).toHaveLength(2);
      expect(execution.activityResults[1].activityId).toBe('success-1');
    });
    
    it('should execute failure handlers on error', async () => {
      const failureActivity = createTestActivity('failure-1', 'Failure handler');
      const process = createTestProcess({
        activities: [createTestActivity('fail-1', 'Will fail')],
        onFailure: [failureActivity]
      });
      
      // Mock activity executor to fail for the main activity, succeed for handler
      mockActivityExecutor.execute
        .mockRejectedValueOnce(new Error('Activity failed'))
        .mockResolvedValueOnce({ 
          success: true, 
          output: 'Failure handled' 
        });
      
      const execution = await engine.executeProcess(process);
      
      // Process should be failed
      expect(execution.status).toBe('failed');
      expect(execution.error).toBeDefined();
      
      // Should have executed both the main activity (failed) and failure handler
      expect(mockActivityExecutor.execute).toHaveBeenCalledTimes(2);
      
      // Verify failure handler was called
      const secondCall = mockActivityExecutor.execute.mock.calls[1];
      expect(secondCall[0].id).toBe('failure-1');
    });
    
    it('should emit lifecycle events', async () => {
      const process = createTestProcess();
      const events: string[] = [];
      
      engine.on('execution:started', () => events.push('started'));
      engine.on('activity:started', () => events.push('activity:started'));
      engine.on('activity:completed', () => events.push('activity:completed'));
      engine.on('execution:completed', () => events.push('completed'));
      
      await engine.executeProcess(process);
      
      expect(events).toContain('started');
      expect(events).toContain('completed');
      expect(events.filter(e => e === 'activity:started')).toHaveLength(2);
      expect(events.filter(e => e === 'activity:completed')).toHaveLength(2);
    });
  });
  
  describe('pauseExecution', () => {
    it('should pause a running execution', async () => {
      const process = createTestProcess();
      
      // Create a slow activity to ensure we can pause during execution
      const slowActivity = createTestActivity('slow-1', 'Slow Activity');
      process.activities = [slowActivity];
      
      // Mock the activity executor to take some time
      let resolveFn: any;
      const slowPromise = new Promise((resolve) => {
        resolveFn = resolve;
      });
      mockActivityExecutor.execute.mockReturnValueOnce(slowPromise);
      
      // Start execution in background
      const executionPromise = engine.executeProcess(process);
      
      // Wait a bit to ensure execution starts
      await new Promise(resolve => setTimeout(resolve, 10));
      
      // Get the execution ID from the engine's internal map
      const executions = Array.from((engine as any).executions.values());
      expect(executions.length).toBe(1);
      const executionId = executions[0].id;
      
      // Pause the execution
      await engine.pauseExecution(executionId);
      
      const execution = engine.getExecution(executionId);
      expect(execution?.status).toBe('paused');
      
      // Clean up by resolving the slow activity
      resolveFn({ success: true });
      await executionPromise;
    });
  });
  
  describe('cancelExecution', () => {
    it('should cancel a running execution', async () => {
      const process = createTestProcess();
      
      // Create a slow activity
      const slowActivity = createTestActivity('slow-1', 'Slow Activity');
      process.activities = [slowActivity];
      
      // Mock the activity executor to take some time
      let rejectFn: any;
      const slowPromise = new Promise((resolve, reject) => {
        rejectFn = reject;
      });
      mockActivityExecutor.execute.mockReturnValueOnce(slowPromise);
      
      // Start execution in background
      const executionPromise = engine.executeProcess(process).catch(() => {
        // Expect this to fail due to cancellation
      });
      
      // Wait a bit to ensure execution starts
      await new Promise(resolve => setTimeout(resolve, 10));
      
      // Get the execution ID
      const executions = Array.from((engine as any).executions.values());
      const executionId = executions[0].id;
      
      // Cancel the execution
      await engine.cancelExecution(executionId);
      
      const execution = engine.getExecution(executionId);
      expect(execution?.status).toBe('cancelled');
      expect(mockStore.saveExecution).toHaveBeenCalled();
      
      // Clean up
      rejectFn(new Error('Cancelled'));
      await executionPromise;
    });
  });
});

// Test helper functions
function createTestProcess(overrides: Partial<ProcessDefinition> = {}): ProcessDefinition {
  return {
    id: 'process-123',
    name: 'Test Process',
    version: '1.0.0',
    triggers: [],
    activities: [
      createTestActivity('act-1', 'Activity 1'),
      createTestActivity('act-2', 'Activity 2')
    ],
    variables: {},
    metadata: {
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      executionCount: 0
    },
    ...overrides
  };
}

function createTestActivity(
  id: string, 
  name: string,
  overrides: Partial<Activity> = {}
): Activity {
  return {
    id,
    type: 'tool',
    name,
    config: {
      toolName: 'test_tool',
      toolArgs: {}
    },
    ...overrides
  };
}