import type { Mock, MockedClass, MockedFunction } from 'vitest';
import { vi } from 'vitest';
import { ProcessBuilder } from '../process-builder.js';
import { ProcessStore } from '../process-store.js';
import { 
  ProcessDefinition,
  Activity,
  ProcessTrigger,
  PersonaType
} from '../types.js';

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

describe('ProcessBuilder', () => {
  let builder: ProcessBuilder;
  let mockStore: vi.Mocked<ProcessStore>;
  
  beforeEach(() => {
    // Create a simple in-memory store for tests
    const processes = new Map<string, ProcessDefinition>();
    
    mockStore = {
      saveProcess: vi.fn().mockImplementation(async (process: ProcessDefinition) => {
        processes.set(process.id, process);
      }),
      getProcess: vi.fn().mockImplementation(async (id: string) => {
        return processes.get(id) || null;
      }),
      getAllProcesses: vi.fn().mockImplementation(async () => {
        return Array.from(processes.values());
      })
    } as any;
    
    builder = new ProcessBuilder(mockStore);
  });
  
  afterEach(() => {
    vi.clearAllMocks();
  });
  
  describe('createProcess', () => {
    it('should create a basic process', async () => {
      const process = await builder.createProcess({
        name: 'Test Process',
        description: 'A test process'
      });
      
      expect(process).toMatchObject({
        id: expect.stringMatching(/^process-/),
        name: 'Test Process',
        description: 'A test process',
        version: '1.0.0',
        triggers: [],
        activities: [],
        variables: {},
        metadata: {
          createdAt: expect.any(String),
          updatedAt: expect.any(String),
          executionCount: 0
        }
      });
      
      expect(mockStore.saveProcess).toHaveBeenCalledWith(process);
    });
    
    it('should create process with persona', async () => {
      const process = await builder.createProcess({
        name: 'Founder Process',
        persona: 'founder'
      });
      
      expect(process.persona).toBe('founder');
    });
    
    it('should throw error if name is missing', async () => {
      await expect(builder.createProcess({} as any))
        .rejects.toThrow('Process name is required');
    });
    
    it('should create process with initial activities', async () => {
      const activities: Activity[] = [
        {
          id: 'act-1',
          type: 'tool',
          name: 'Step 1',
          config: { toolName: 'test_tool' }
        }
      ];
      
      const process = await builder.createProcess({
        name: 'Process with Activities',
        activities
      });
      
      expect(process.activities).toEqual(activities);
    });
  });
  
  describe('addActivity', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ name: 'Test Process' });
    });
    
    it('should add activity to process', async () => {
      const activity: Activity = {
        id: 'new-activity',
        type: 'tool',
        name: 'New Activity',
        config: { toolName: 'test_tool' }
      };
      
      const updated = await builder.addActivity(process.id, activity);
      
      expect(updated.activities).toHaveLength(1);
      expect(updated.activities[0]).toEqual(activity);
      expect(mockStore.saveProcess).toHaveBeenCalledWith(updated);
    });
    
    it('should insert activity at specific position', async () => {
      // Add initial activities
      await builder.addActivity(process.id, {
        id: 'act-1',
        type: 'tool',
        name: 'First',
        config: { toolName: 'tool1' }
      });
      
      await builder.addActivity(process.id, {
        id: 'act-2',
        type: 'tool',
        name: 'Third',
        config: { toolName: 'tool3' }
      });
      
      // Insert in middle
      const updated = await builder.addActivity(process.id, {
        id: 'act-middle',
        type: 'tool',
        name: 'Second',
        config: { toolName: 'tool2' }
      }, 1);
      
      expect(updated.activities).toHaveLength(3);
      expect(updated.activities[1].id).toBe('act-middle');
    });
    
    it('should update metadata when adding activity', async () => {
      const before = process.metadata.updatedAt;
      
      // Wait a bit to ensure timestamp difference
      await new Promise(resolve => setTimeout(resolve, 10));
      
      const updated = await builder.addActivity(process.id, {
        id: 'act-1',
        type: 'tool',
        name: 'Activity',
        config: { toolName: 'tool' }
      });
      
      expect(updated.metadata.updatedAt).not.toBe(before);
    });
  });
  
  describe('removeActivity', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ 
        name: 'Test Process',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Activity 1', config: { toolName: 'tool1' } },
          { id: 'act-2', type: 'tool', name: 'Activity 2', config: { toolName: 'tool2' } },
          { id: 'act-3', type: 'tool', name: 'Activity 3', config: { toolName: 'tool3' } }
        ]
      });
    });
    
    it('should remove activity by id', async () => {
      const updated = await builder.removeActivity(process.id, 'act-2');
      
      expect(updated.activities).toHaveLength(2);
      expect(updated.activities.find(a => a.id === 'act-2')).toBeUndefined();
      expect(updated.activities[0].id).toBe('act-1');
      expect(updated.activities[1].id).toBe('act-3');
    });
    
    it('should throw error if activity not found', async () => {
      await expect(builder.removeActivity(process.id, 'non-existent'))
        .rejects.toThrow('Activity not found');
    });
  });
  
  describe('updateActivity', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ 
        name: 'Test Process',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Original', config: { toolName: 'tool1' } }
        ]
      });
    });
    
    it('should update activity properties', async () => {
      const updated = await builder.updateActivity(process.id, 'act-1', {
        name: 'Updated Name',
        config: { toolName: 'updated_tool', newProp: 'value' }
      });
      
      expect(updated.activities[0].name).toBe('Updated Name');
      expect(updated.activities[0].config).toEqual({
        toolName: 'updated_tool',
        newProp: 'value'
      });
    });
    
    it('should preserve unchanged properties', async () => {
      const updated = await builder.updateActivity(process.id, 'act-1', {
        name: 'New Name'
      });
      
      expect(updated.activities[0].type).toBe('tool');
      expect(updated.activities[0].config).toEqual({ toolName: 'tool1' });
    });
  });
  
  describe('addTrigger', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ name: 'Test Process' });
    });
    
    it('should add trigger to process', async () => {
      const trigger: ProcessTrigger = {
        id: 'trigger-1',
        type: 'schedule',
        name: 'Daily Run',
        enabled: true,
        config: { cron: '0 9 * * *' }
      };
      
      const updated = await builder.addTrigger(process.id, trigger);
      
      expect(updated.triggers).toHaveLength(1);
      expect(updated.triggers[0]).toEqual(trigger);
    });
    
    it('should prevent duplicate trigger ids', async () => {
      const trigger: ProcessTrigger = {
        id: 'trigger-1',
        type: 'manual',
        name: 'Manual',
        enabled: true
      };
      
      await builder.addTrigger(process.id, trigger);
      
      await expect(builder.addTrigger(process.id, trigger))
        .rejects.toThrow('Trigger with ID trigger-1 already exists');
    });
  });
  
  describe('updateTrigger', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ 
        name: 'Test Process',
        triggers: [{
          id: 'trigger-1',
          type: 'schedule',
          name: 'Original',
          enabled: true,
          config: { cron: '0 9 * * *' }
        }]
      });
    });
    
    it('should update trigger properties', async () => {
      const updated = await builder.updateTrigger(process.id, 'trigger-1', {
        name: 'Updated Schedule',
        enabled: false,
        config: { cron: '0 10 * * *' }
      });
      
      expect(updated.triggers[0]).toMatchObject({
        id: 'trigger-1',
        type: 'schedule',
        name: 'Updated Schedule',
        enabled: false,
        config: { cron: '0 10 * * *' }
      });
    });
  });
  
  describe('removeTrigger', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ 
        name: 'Test Process',
        triggers: [
          { id: 'trigger-1', type: 'manual', name: 'Manual', enabled: true },
          { id: 'trigger-2', type: 'schedule', name: 'Schedule', enabled: true, config: { cron: '0 9 * * *' } }
        ]
      });
    });
    
    it('should remove trigger by id', async () => {
      const updated = await builder.removeTrigger(process.id, 'trigger-1');
      
      expect(updated.triggers).toHaveLength(1);
      expect(updated.triggers[0].id).toBe('trigger-2');
    });
  });
  
  describe('setVariables', () => {
    let process: ProcessDefinition;
    
    beforeEach(async () => {
      process = await builder.createProcess({ 
        name: 'Test Process',
        variables: { existing: 'value' }
      });
    });
    
    it('should update process variables', async () => {
      const updated = await builder.setVariables(process.id, {
        newVar: 'new value',
        number: 42,
        nested: { prop: 'value' }
      });
      
      expect(updated.variables).toEqual({
        newVar: 'new value',
        number: 42,
        nested: { prop: 'value' }
      });
    });
    
    it('should merge variables when merge=true', async () => {
      const updated = await builder.setVariables(process.id, {
        newVar: 'new value'
      }, true);
      
      expect(updated.variables).toEqual({
        existing: 'value',
        newVar: 'new value'
      });
    });
  });
  
  describe('validateProcess', () => {
    it('should validate complete process', async () => {
      const process = await builder.createProcess({
        name: 'Valid Process',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Step 1', config: { toolName: 'tool1' } }
        ],
        triggers: [
          { id: 'trigger-1', type: 'manual', name: 'Manual', enabled: true }
        ]
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.isValid).toBe(true);
      expect(validation.errors).toHaveLength(0);
      expect(validation.warnings).toHaveLength(0);
    });
    
    it('should warn about process without activities', async () => {
      const process = await builder.createProcess({
        name: 'Empty Process'
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.isValid).toBe(true);
      expect(validation.warnings).toContainEqual(
        expect.objectContaining({
          type: 'warning',
          message: 'Process has no activities defined'
        })
      );
    });
    
    it('should warn about process without triggers', async () => {
      const process = await builder.createProcess({
        name: 'No Triggers',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Activity', config: { toolName: 'tool' } }
        ]
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.warnings).toContainEqual(
        expect.objectContaining({
          message: 'Process has no triggers defined'
        })
      );
    });
    
    it('should error on invalid activity config', async () => {
      const process = await builder.createProcess({
        name: 'Invalid Activity',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Bad Tool', config: {} } // Missing toolName
        ]
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.isValid).toBe(false);
      expect(validation.errors).toContainEqual(
        expect.objectContaining({
          type: 'error',
          field: 'activities[0].config',
          message: expect.stringContaining('toolName')
        })
      );
    });
    
    it('should error on invalid condition syntax', async () => {
      const process = await builder.createProcess({
        name: 'Bad Condition',
        activities: [
          { 
            id: 'act-1', 
            type: 'tool', 
            name: 'Conditional', 
            condition: 'invalid === syntax @#$',
            config: { toolName: 'tool' } 
          }
        ]
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.errors).toContainEqual(
        expect.objectContaining({
          field: 'activities[0].condition',
          message: expect.stringContaining('Invalid condition syntax')
        })
      );
    });
    
    it('should validate trigger configurations', async () => {
      const process = await builder.createProcess({
        name: 'Bad Schedule',
        triggers: [
          {
            id: 'trigger-1',
            type: 'schedule',
            name: 'Invalid Cron',
            enabled: true,
            config: { cron: 'invalid-cron' }
          }
        ]
      });
      
      const validation = await builder.validateProcess(process.id);
      
      expect(validation.errors).toContainEqual(
        expect.objectContaining({
          field: 'triggers[0].config.cron',
          message: expect.stringContaining('Invalid cron expression')
        })
      );
    });
  });
  
  describe('cloneProcess', () => {
    it('should create a copy of existing process', async () => {
      const original = await builder.createProcess({
        name: 'Original Process',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Activity', config: { toolName: 'tool' } }
        ],
        triggers: [
          { id: 'trigger-1', type: 'manual', name: 'Manual', enabled: true }
        ],
        variables: { var1: 'value' }
      });
      
      const cloned = await builder.cloneProcess(original.id, 'Cloned Process');
      
      expect(cloned.id).not.toBe(original.id);
      expect(cloned.name).toBe('Cloned Process');
      expect(cloned.activities).toHaveLength(1);
      expect(cloned.activities[0].id).not.toBe(original.activities[0].id);
      expect(cloned.triggers[0].id).not.toBe(original.triggers[0].id);
      expect(cloned.variables).toEqual(original.variables);
      expect(cloned.metadata.executionCount).toBe(0);
    });
    
    it('should handle cloning with new persona', async () => {
      const original = await builder.createProcess({
        name: 'Original',
        persona: 'founder'
      });
      
      const cloned = await builder.cloneProcess(original.id, 'For Engineers', {
        persona: 'software-engineer'
      });
      
      expect(cloned.persona).toBe('software-engineer');
    });
  });
  
  describe('exportProcess', () => {
    it('should export process as JSON', async () => {
      const process = await builder.createProcess({
        name: 'Export Test',
        activities: [
          { id: 'act-1', type: 'tool', name: 'Activity', config: { toolName: 'tool' } }
        ]
      });
      
      const exported = await builder.exportProcess(process.id);
      
      expect(exported).toMatchObject({
        name: 'Export Test',
        version: '1.0.0',
        activities: expect.any(Array),
        exportedAt: expect.any(String),
        exportVersion: '1.0'
      });
      
      // Should not include runtime data
      expect(exported).not.toHaveProperty('id');
      expect(exported).not.toHaveProperty('metadata.executionCount');
    });
  });
  
  describe('importProcess', () => {
    it('should import process from JSON', async () => {
      const exportData = {
        name: 'Imported Process',
        version: '1.0.0',
        activities: [
          { id: 'act-1', type: 'tool' as const, name: 'Activity', config: { toolName: 'tool' } }
        ],
        triggers: [],
        variables: { imported: true },
        exportVersion: '1.0',
        exportedAt: new Date().toISOString()
      };
      
      const imported = await builder.importProcess(exportData);
      
      expect(imported.name).toBe('Imported Process');
      expect(imported.activities).toHaveLength(1);
      expect(imported.variables).toEqual({ imported: true });
      expect(imported.id).toMatch(/^process-/);
      expect(imported.metadata.executionCount).toBe(0);
    });
    
    it('should handle version conflicts on import', async () => {
      const exportData = {
        name: 'Old Version',
        version: '0.1.0',
        activities: [],
        triggers: [],
        exportVersion: '0.5', // Old export version
        exportedAt: new Date().toISOString()
      };
      
      const imported = await builder.importProcess(exportData, {
        namePrefix: '[Imported] '
      });
      
      expect(imported.name).toBe('[Imported] Old Version');
    });
  });
});