import type { Mock, MockedClass, MockedFunction } from 'vitest';
import { vi } from 'vitest';
import { ActivityExecutor } from '../activity-executor.js';
import { AgentOrchestrator } from '../../agent-orchestration/orchestrator.js';
import { 
  Activity, 
  ProcessExecution,
  ConditionalBranch,
  ActivityConfig
} from '../types.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';

// Mock dependencies
vi.mock('@modelcontextprotocol/sdk/server/index.js');

describe('ActivityExecutor', () => {
  let executor: ActivityExecutor;
  let mockProcessEngine: any;
  let mockServer: vi.Mocked<Server>;
  let mockAgentOrchestrator: AgentOrchestrator;
  let mockExecution: ProcessExecution;
  
  beforeEach(() => {
    mockProcessEngine = {
      emit: vi.fn()
    };
    
    mockServer = {
      // Add any server methods that might be called
    } as any;
    
    mockAgentOrchestrator = new AgentOrchestrator();
    
    executor = new ActivityExecutor(mockProcessEngine, mockAgentOrchestrator);
    executor.setServer(mockServer);
    
    mockExecution = createMockExecution();
  });
  
  afterEach(() => {
    vi.clearAllMocks();
  });
  
  describe('execute', () => {
    describe('tool activities', () => {
      it('should execute a tool activity successfully', async () => {
        const activity: Activity = {
          id: 'tool-1',
          type: 'tool',
          name: 'Run Tool',
          config: {
            toolName: 'test_tool',
            toolArgs: { param: 'value' }
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          success: true,
          toolName: 'test_tool',
          executedAt: expect.any(String)
        });
      });
      
      it('should throw error if toolName is missing', async () => {
        const activity: Activity = {
          id: 'tool-1',
          type: 'tool',
          name: 'Invalid Tool',
          config: {} // Missing toolName
        };
        
        await expect(executor.execute(activity, {}, mockExecution))
          .rejects.toThrow('Tool activity missing toolName');
      });
      
      it('should throw error if server not initialized', async () => {
        const executorNoServer = new ActivityExecutor(mockProcessEngine, mockAgentOrchestrator);
        const activity = createToolActivity();
        
        await expect(executorNoServer.execute(activity, {}, mockExecution))
          .rejects.toThrow('Server not initialized');
      });
    });
    
    describe('human activities', () => {
      it('should execute human activity and wait for input', async () => {
        const activity: Activity = {
          id: 'human-1',
          type: 'human',
          name: 'Approval Required',
          config: {
            prompt: 'Please approve this action',
            assignTo: ['user1', 'user2'],
            approvalType: 'any'
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(mockExecution.status).toBe('running'); // Should be set back after wait
        expect(mockProcessEngine.emit).toHaveBeenCalledWith(
          'execution:waiting',
          expect.objectContaining({ execution: mockExecution, activity })
        );
        
        expect(result).toMatchObject({
          approved: true,
          approvedBy: 'user1',
          approvedAt: expect.any(String)
        });
      });
      
      it('should handle form fields in human activities', async () => {
        const activity: Activity = {
          id: 'human-2',
          type: 'human',
          name: 'Data Entry',
          config: {
            prompt: 'Enter project details',
            formFields: [
              { name: 'projectName', type: 'text', label: 'Project Name', required: true },
              { name: 'priority', type: 'select', label: 'Priority', options: ['high', 'medium', 'low'] }
            ]
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result.approved).toBe(true);
      });
    });
    
    describe('agent activities', () => {
      it('should delegate to agent successfully', async () => {
        const activity: Activity = {
          id: 'agent-1',
          type: 'agent',
          name: 'AI Analysis',
          config: {
            agentType: 'analyzer',
            agentTask: 'Analyze code quality',
            agentConfig: { threshold: 0.8 }
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          agentType: 'analyzer',
          taskCompleted: true, // Agent orchestration returns true when task succeeds
          results: expect.any(String) // Results are returned as string output from orchestrator
        });
      });
    });
    
    describe('conditional activities', () => {
      it('should execute matching branch', async () => {
        const activity: Activity = {
          id: 'cond-1',
          type: 'conditional',
          name: 'Branch Decision',
          config: {
            conditions: [
              {
                condition: 'status === "active"',
                activities: [createToolActivity('branch1-tool')]
              },
              {
                condition: 'status === "inactive"',
                activities: [createToolActivity('branch2-tool')]
              }
            ]
          }
        };
        
        mockExecution.variables.status = 'active';
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          success: true,
          toolName: 'test_tool'
        });
      });
      
      it('should execute default branch when no conditions match', async () => {
        const activity: Activity = {
          id: 'cond-2',
          type: 'conditional',
          name: 'Branch with Default',
          config: {
            conditions: [
              {
                condition: 'false',
                activities: [createToolActivity('never-runs')]
              }
            ],
            defaultBranch: [createToolActivity('default-tool')]
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          success: true,
          toolName: 'test_tool'
        });
      });
      
      it('should return conditionMatched false when no branch matches', async () => {
        const activity: Activity = {
          id: 'cond-3',
          type: 'conditional',
          name: 'No Match',
          config: {
            conditions: [
              {
                condition: 'false',
                activities: []
              }
            ]
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toEqual({}); // Returns empty object when no conditions match
      });
    });
    
    describe('loop activities', () => {
      it('should iterate over collection', async () => {
        const activity: Activity = {
          id: 'loop-1',
          type: 'loop',
          name: 'Process Items',
          config: {
            collection: 'items',
            itemVariable: 'currentItem',
            activities: [createToolActivity('process-item')]
          }
        };
        
        mockExecution.variables.items = ['item1', 'item2', 'item3'];
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          loopCompleted: true,
          iterations: 3,
          results: expect.arrayContaining([
            expect.objectContaining({ success: true }),
            expect.objectContaining({ success: true }),
            expect.objectContaining({ success: true })
          ])
        });
      });
      
      it('should respect maxIterations limit', async () => {
        const activity: Activity = {
          id: 'loop-2',
          type: 'loop',
          name: 'Limited Loop',
          config: {
            collection: 'bigList',
            maxIterations: 2,
            activities: [createToolActivity('process')]
          }
        };
        
        mockExecution.variables.bigList = [1, 2, 3, 4, 5];
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result.iterations).toBe(2);
        expect(result.results).toHaveLength(2);
      });
    });
    
    describe('parallel activities', () => {
      it('should execute all branches in parallel', async () => {
        const activity: Activity = {
          id: 'parallel-1',
          type: 'parallel',
          name: 'Parallel Tasks',
          config: {
            branches: [
              [createToolActivity('branch1-act1'), createToolActivity('branch1-act2')],
              [createToolActivity('branch2-act1')],
              [createToolActivity('branch3-act1')]
            ],
            waitForAll: true
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          parallelCompleted: true,
          branches: expect.arrayContaining([
            expect.objectContaining({ success: true, branchIndex: 0 }),
            expect.objectContaining({ success: true, branchIndex: 1 }),
            expect.objectContaining({ success: true, branchIndex: 2 })
          ])
        });
      });
      
      it('should return first completed when waitForAll is false', async () => {
        const activity: Activity = {
          id: 'parallel-2',
          type: 'parallel',
          name: 'Race Condition',
          config: {
            branches: [
              [createToolActivity('slow')],
              [createToolActivity('fast')],
            ],
            waitForAll: false
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          parallelCompleted: true,
          firstCompleted: expect.objectContaining({ success: true })
        });
      });
    });
    
    describe('external activities', () => {
      it('should make external HTTP request', async () => {
        const activity: Activity = {
          id: 'ext-1',
          type: 'external',
          name: 'Call API',
          config: {
            url: 'https://api.example.com/data',
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: { key: 'value' }
          }
        };
        
        const result = await executor.execute(activity, {}, mockExecution);
        
        expect(result).toMatchObject({
          status: 200,
          response: expect.objectContaining({
            message: expect.any(String),
            data: expect.any(Object)
          })
        });
      });
      
      it('should throw error if URL is missing', async () => {
        const activity: Activity = {
          id: 'ext-2',
          type: 'external',
          name: 'Invalid External',
          config: {} // Missing URL
        };
        
        await expect(executor.execute(activity, {}, mockExecution))
          .rejects.toThrow('External activity missing URL');
      });
    });
    
    describe('error handling', () => {
      it('should throw error for unknown activity type', async () => {
        const activity = {
          id: 'unknown-1',
          type: 'unknown' as any,
          name: 'Unknown Type',
          config: {}
        };
        
        await expect(executor.execute(activity, {}, mockExecution))
          .rejects.toThrow('Unknown activity type: unknown');
      });
    });
  });
});

// Helper functions
function createMockExecution(): ProcessExecution {
  return {
    id: 'exec-123',
    processId: 'process-123',
    processVersion: '1.0.0',
    status: 'running',
    triggeredBy: 'manual',
    startedAt: new Date().toISOString(),
    variables: {},
    activityResults: [],
    logs: []
  };
}

function createToolActivity(id: string = 'tool-1'): Activity {
  return {
    id,
    type: 'tool',
    name: 'Test Tool',
    config: {
      toolName: 'test_tool',
      toolArgs: {}
    }
  };
}