import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TriggerManager } from '../trigger-manager.js';
import { ProcessStore } from '../process-store.js';
import { ProcessEngine } from '../process-engine.js';
import { 
  ProcessDefinition,
  ProcessTrigger,
  TriggerExecution
} from '../types.js';
import { MockCronAdapter } from '../cron-adapter.js';

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

describe('TriggerManager', () => {
  let manager: TriggerManager;
  let mockStore: vi.Mocked<ProcessStore>;
  let mockEngine: vi.Mocked<ProcessEngine>;
  let mockCronAdapter: MockCronAdapter;
  
  beforeEach(() => {
    mockStore = {
      getAllProcesses: vi.fn().mockResolvedValue([]),
      getProcess: vi.fn(),
      saveProcess: vi.fn()
    } as any;
    
    mockEngine = {
      executeProcess: vi.fn().mockResolvedValue({
        id: 'exec-123',
        status: 'completed'
      })
    } as any;
    
    mockCronAdapter = new MockCronAdapter();
    manager = new TriggerManager(mockEngine, mockStore, mockCronAdapter);
  });
  
  afterEach(() => {
    vi.clearAllMocks();
    manager.stopAll();
  });
  
  describe('initialization', () => {
    it('should load all enabled triggers on init', async () => {
      const processes: ProcessDefinition[] = [
        createProcess({
          id: 'proc-1',
          triggers: [
            { id: 'trig-1', type: 'schedule', name: 'Daily', enabled: true, config: { cron: '0 9 * * *' } },
            { id: 'trig-2', type: 'manual', name: 'Manual', enabled: true }
          ]
        }),
        createProcess({
          id: 'proc-2',
          triggers: [
            { id: 'trig-3', type: 'schedule', name: 'Weekly', enabled: false, config: { cron: '0 9 * * 1' } }
          ]
        })
      ];
      
      mockStore.getAllProcesses.mockResolvedValue(processes);
      
      await manager.initialize();
      
      expect(mockStore.getAllProcesses).toHaveBeenCalled();
      // Should register 2 enabled triggers (trig-1 and trig-2)
      expect(manager.getActiveTriggers()).toHaveLength(2);
    });
  });
  
  describe('registerTrigger', () => {
    it('should register a manual trigger', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual Trigger',
        enabled: true
      };
      
      await manager.registerTrigger(process, trigger);
      
      const active = manager.getActiveTriggers();
      expect(active).toHaveLength(1);
      expect(active[0]).toMatchObject({
        id: 'manual-1',
        processId: 'proc-1',
        type: 'manual',
        enabled: true,
        name: 'Manual Trigger'
      });
    });
    
    it('should register a schedule trigger', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'schedule-1',
        type: 'schedule',
        name: 'Daily Run',
        enabled: true,
        config: { cron: '0 9 * * *' }
      };
      
      await manager.registerTrigger(process, trigger);
      
      const active = manager.getActiveTriggers();
      expect(active).toHaveLength(1);
      expect(active[0].type).toBe('schedule');
    });
    
    it('should not register disabled triggers', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'disabled-1',
        type: 'manual',
        name: 'Disabled',
        enabled: false
      };
      
      await manager.registerTrigger(process, trigger);
      
      expect(manager.getActiveTriggers()).toHaveLength(0);
    });
    
    it('should throw error for invalid schedule', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'bad-schedule',
        type: 'schedule',
        name: 'Invalid',
        enabled: true,
        config: { cron: 'invalid-cron' }
      };
      
      // Mock cron validation to return false
      mockCronAdapter.setShouldValidate(false);
      
      await expect(manager.registerTrigger(process, trigger))
        .rejects.toThrow('Invalid cron expression');
    });
  });
  
  describe('unregisterTrigger', () => {
    it('should unregister an active trigger', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'trig-1',
        type: 'manual',
        name: 'Manual',
        enabled: true
      };
      
      await manager.registerTrigger(process, trigger);
      expect(manager.getActiveTriggers()).toHaveLength(1);
      
      await manager.unregisterTrigger('proc-1', 'trig-1');
      expect(manager.getActiveTriggers()).toHaveLength(0);
    });
    
    it('should stop cron job when unregistering schedule', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'schedule-1',
        type: 'schedule',
        name: 'Daily',
        enabled: true,
        config: { cron: '0 9 * * *' }
      };
      
      await manager.registerTrigger(process, trigger);
      
      // Get the task from the adapter
      const tasks = mockCronAdapter.getTasks();
      const task = tasks.get('0 9 * * *');
      expect(task).toBeDefined();
      
      await manager.unregisterTrigger('proc-1', 'schedule-1');
      
      // Verify the task was stopped and destroyed
      expect(task!.isDestroyed()).toBe(true);
    });
  });
  
  describe('executeTrigger', () => {
    it('should execute a manual trigger', async () => {
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual',
        enabled: true,
        config: {}
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await manager.registerTrigger(process, trigger);
      
      const execution = await manager.executeTrigger('proc-1', 'manual-1');
      
      expect(mockEngine.executeProcess).toHaveBeenCalledWith(
        process,
        'manual-1',
        undefined
      );
      expect(execution).toMatchObject({
        id: 'exec-123',
        status: 'completed'
      });
    });
    
    it('should pass context to execution', async () => {
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual',
        enabled: true,
        config: {}
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await manager.registerTrigger(process, trigger);
      
      const context = { userId: 'user-123', data: { foo: 'bar' } };
      await manager.executeTrigger('proc-1', 'manual-1', context);
      
      expect(mockEngine.executeProcess).toHaveBeenCalledWith(
        process,
        'manual-1',
        context
      );
    });
    
    it('should throw error for unregistered trigger', async () => {
      const process = createProcess({ 
        id: 'proc-1',
        triggers: []
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await expect(manager.executeTrigger('proc-1', 'unknown'))
        .rejects.toThrow('Trigger not found');
    });
    
    it('should throw error for disabled trigger', async () => {
      const trigger: ProcessTrigger = {
        id: 'disabled-1',
        type: 'manual',
        name: 'Disabled',
        enabled: false
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      // Register the trigger even though it's disabled
      await manager.registerTrigger(process, trigger);
      
      await expect(manager.executeTrigger('proc-1', 'disabled-1'))
        .rejects.toThrow('Trigger is disabled');
    });
  });
  
  describe('updateTrigger', () => {
    it('should update trigger configuration', async () => {
      const trigger: ProcessTrigger = {
        id: 'schedule-1',
        type: 'schedule',
        name: 'Daily',
        enabled: true,
        config: { cron: '0 9 * * *' }
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      
      mockStore.getProcess.mockResolvedValue(process);
      mockStore.saveProcess.mockResolvedValue(undefined);
      
      await manager.registerTrigger(process, trigger);
      
      await manager.updateTrigger('proc-1', 'schedule-1', {
        name: 'Updated Daily',
        config: { cron: '0 10 * * *' }
      });
      
      const active = manager.getActiveTriggers();
      expect(active[0].name).toBe('Updated Daily');
    });
    
    it('should disable trigger when enabled set to false', async () => {
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual',
        enabled: true,
        config: {}
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      
      mockStore.getProcess.mockResolvedValue(process);
      mockStore.saveProcess.mockResolvedValue(undefined);
      
      await manager.registerTrigger(process, trigger);
      expect(manager.getActiveTriggers()).toHaveLength(1);
      
      await manager.updateTrigger('proc-1', 'manual-1', { enabled: false });
      
      // After disabling, the trigger should be removed from active triggers
      const active = manager.getActiveTriggers();
      expect(active).toHaveLength(0);
    });
    
    it('should restart schedule when cron changes', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'schedule-1',
        type: 'schedule',
        name: 'Daily',
        enabled: true,
        config: { cron: '0 9 * * *' }
      };
      
      process.triggers = [trigger];
      mockStore.getProcess.mockResolvedValue(process);
      mockStore.saveProcess.mockResolvedValue(undefined);
      
      await manager.registerTrigger(process, trigger);
      
      // Get the original task
      const originalTasks = mockCronAdapter.getTasks();
      const originalTask = originalTasks.get('0 9 * * *');
      expect(originalTask).toBeDefined();
      
      await manager.updateTrigger('proc-1', 'schedule-1', {
        config: { cron: '0 10 * * *' }
      });
      
      // Verify the old task was destroyed
      expect(originalTask!.isDestroyed()).toBe(true);
      
      // Verify a new task was created
      const newTasks = mockCronAdapter.getTasks();
      const newTask = newTasks.get('0 10 * * *');
      expect(newTask).toBeDefined();
    });
  });
  
  describe('getTriggerHistory', () => {
    it('should return execution history for trigger', async () => {
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual',
        enabled: true,
        config: {}
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await manager.registerTrigger(process, trigger);
      
      // Execute multiple times
      await manager.executeTrigger('proc-1', 'manual-1');
      await manager.executeTrigger('proc-1', 'manual-1');
      await manager.executeTrigger('proc-1', 'manual-1');
      
      const history = manager.getTriggerHistory('proc-1', 'manual-1');
      
      expect(history).toHaveLength(3);
      expect(history[0]).toMatchObject({
        triggerId: 'manual-1',
        processId: 'proc-1',
        executionId: 'exec-123',
        executedAt: expect.any(String),
        status: 'success'
      });
    });
    
    it('should limit history to recent executions', async () => {
      const trigger: ProcessTrigger = {
        id: 'manual-1',
        type: 'manual',
        name: 'Manual',
        enabled: true,
        config: {}
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await manager.registerTrigger(process, trigger);
      
      // Execute more than limit (100)
      for (let i = 0; i < 105; i++) {
        await manager.executeTrigger('proc-1', 'manual-1');
      }
      
      const history = manager.getTriggerHistory('proc-1', 'manual-1');
      expect(history.length).toBeLessThanOrEqual(100);
    });
  });
  
  describe('getActiveTriggers', () => {
    it('should return all active triggers', async () => {
      const process1 = createProcess({ id: 'proc-1' });
      const process2 = createProcess({ id: 'proc-2' });
      
      await manager.registerTrigger(process1, {
        id: 'trig-1',
        type: 'manual',
        name: 'Manual 1',
        enabled: true,
        config: {}
      });
      
      await manager.registerTrigger(process2, {
        id: 'trig-2',
        type: 'schedule',
        name: 'Schedule 1',
        enabled: true,
        config: { cron: '0 9 * * *' }
      });
      
      const active = manager.getActiveTriggers();
      
      expect(active).toHaveLength(2);
      expect(active.map(t => t.type).sort()).toEqual(['manual', 'schedule']);
    });
    
    it('should filter by process id', async () => {
      const process1 = createProcess({ id: 'proc-1' });
      const process2 = createProcess({ id: 'proc-2' });
      
      await manager.registerTrigger(process1, {
        id: 'trig-1',
        type: 'manual',
        name: 'Manual 1',
        enabled: true,
        config: {}
      });
      
      await manager.registerTrigger(process2, {
        id: 'trig-2',
        type: 'manual',
        name: 'Manual 2',
        enabled: true,
        config: {}
      });
      
      const active = manager.getActiveTriggers('proc-1');
      
      expect(active).toHaveLength(1);
      expect(active[0].processId).toBe('proc-1');
    });
  });
  
  describe('stopAll', () => {
    it('should stop all active triggers', async () => {
      const process1 = createProcess({ id: 'proc-1' });
      const process2 = createProcess({ id: 'proc-2' });
      
      await manager.registerTrigger(process1, {
        id: 'schedule-1',
        type: 'schedule',
        name: 'Schedule 1',
        enabled: true,
        config: { cron: '0 9 * * *' }
      });
      
      await manager.registerTrigger(process2, {
        id: 'schedule-2',
        type: 'schedule',
        name: 'Schedule 2',
        enabled: true,
        config: { cron: '0 10 * * *' }
      });
      
      // Get tasks before stopping
      const tasks = mockCronAdapter.getTasks();
      const task1 = tasks.get('0 9 * * *');
      const task2 = tasks.get('0 10 * * *');
      expect(task1).toBeDefined();
      expect(task2).toBeDefined();
      
      manager.stopAll();
      
      // Verify tasks were destroyed
      expect(task1!.isDestroyed()).toBe(true);
      expect(task2!.isDestroyed()).toBe(true);
      expect(manager.getActiveTriggers()).toHaveLength(0);
    });
  });
  
  describe('event triggers', () => {
    it('should handle event triggers', async () => {
      const process = createProcess({ id: 'proc-1' });
      const trigger: ProcessTrigger = {
        id: 'event-1',
        type: 'event',
        name: 'On File Change',
        enabled: true,
        config: {
          eventType: 'file_change',
          eventFilter: { extension: '.js' }
        }
      };
      
      await manager.registerTrigger(process, trigger);
      
      const active = manager.getActiveTriggers();
      expect(active[0].type).toBe('event');
    });
    
    it('should emit event for event triggers', async () => {
      const trigger: ProcessTrigger = {
        id: 'event-1',
        type: 'event',
        name: 'On Data',
        enabled: true,
        config: { event: 'data_received' }
      };
      
      const process = createProcess({ 
        id: 'proc-1',
        triggers: [trigger]
      });
      mockStore.getProcess.mockResolvedValue(process);
      
      await manager.registerTrigger(process, trigger);
      
      // Simulate event
      const eventData = { source: 'api', data: { value: 42 } };
      await manager.handleEvent('data_received', eventData);
      
      expect(mockEngine.executeProcess).toHaveBeenCalledWith(
        process,
        'event-1',
        expect.objectContaining({
          event: 'data_received',
          eventData
        })
      );
    });
  });
});

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