/**
 * Unit tests for OrchestrationManagerService
 * ARCHITECTURE: Tests service layer with real SQLite (in-memory) and TestEventBus
 * Pattern: Behavior-driven testing with Result pattern validation
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

// Mock git-state before importing modules that depend on it
vi.mock('../../../src/utils/git-state.js', () => ({
  captureGitState: vi.fn().mockResolvedValue({ ok: true, value: null }),
  getCurrentCommitSha: vi.fn().mockResolvedValue({ ok: true, value: 'abc1234567890abcdef1234567890abcdef123456' }),
  captureLoopGitContext: vi.fn().mockResolvedValue({ ok: true, value: {} }),
  validateGitRefName: vi.fn().mockReturnValue({ ok: true, value: undefined }),
}));

import { EvalMode, EvalType, OrchestratorId, OrchestratorStatus } from '../../../src/core/domain.js';
import { Database } from '../../../src/implementations/database.js';
import { SQLiteLoopRepository } from '../../../src/implementations/loop-repository.js';
import { SQLiteOrchestrationRepository } from '../../../src/implementations/orchestration-repository.js';
import { LoopManagerService } from '../../../src/services/loop-manager.js';
import { OrchestrationManagerService } from '../../../src/services/orchestration-manager.js';
import { createTestConfiguration } from '../../fixtures/factories.js';
import { TestEventBus, TestLogger } from '../../fixtures/test-doubles.js';

describe('OrchestrationManagerService - Unit Tests', () => {
  let db: Database;
  let loopRepo: SQLiteLoopRepository;
  let orchestrationRepo: SQLiteOrchestrationRepository;
  let eventBus: TestEventBus;
  let logger: TestLogger;
  let loopService: LoopManagerService;
  let service: OrchestrationManagerService;
  const config = createTestConfiguration({ defaultAgent: 'claude' });

  beforeEach(() => {
    db = new Database(':memory:');
    loopRepo = new SQLiteLoopRepository(db);
    orchestrationRepo = new SQLiteOrchestrationRepository(db);
    eventBus = new TestEventBus();
    logger = new TestLogger();
    loopService = new LoopManagerService(eventBus, logger, loopRepo, config);
    service = new OrchestrationManagerService({ eventBus, logger, orchestrationRepo, loopService, config });

    // Simulate LoopHandler: persist loop on LoopCreated event
    // ARCHITECTURE: In production, LoopHandler saves the loop to DB on LoopCreated.
    // In unit tests, we simulate this behavior to enable orchestration creation.
    eventBus.subscribe('LoopCreated', async (event: Record<string, unknown>) => {
      const loop = (event as { loop: Parameters<typeof loopRepo.save>[0] }).loop;
      if (loop) {
        await loopRepo.save(loop);
      }
    });
  });

  afterEach(() => {
    eventBus.dispose();
    db.close();
  });

  describe('createOrchestration()', () => {
    it('should create orchestration with loop and correct base fields', async () => {
      const result = await service.createOrchestration({
        goal: 'Build the auth system',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      const orch = result.value;
      expect(orch.id).toMatch(/^orchestrator-/);
      expect(orch.goal).toBe('Build the auth system');
      expect(orch.status).toBe(OrchestratorStatus.RUNNING);
      expect(orch.loopId).toBeDefined();
      expect(orch.maxDepth).toBe(3);
      expect(orch.maxWorkers).toBe(5);
      expect(orch.maxIterations).toBe(50);
    });

    it('should create loop with correct configuration', async () => {
      const result = await service.createOrchestration({
        goal: 'Build auth',
        maxIterations: 20,
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      // Verify the LoopCreated event was emitted
      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      expect(loopEvents.length).toBe(1);
    });

    it('should create loop with agent eval mode and SCHEMA eval type', async () => {
      const result = await service.createOrchestration({
        goal: 'Build auth system',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      expect(loopEvents.length).toBe(1);
      const loop = (loopEvents[0] as { loop: { evalMode?: string; evalType?: string } }).loop;
      expect(loop.evalMode).toBe(EvalMode.AGENT);
      expect(loop.evalType).toBe(EvalType.SCHEMA);
    });

    it('should create loop with empty exitCondition (agent mode uses evalPrompt instead)', async () => {
      const result = await service.createOrchestration({
        goal: 'Build auth system',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      const loop = (loopEvents[0] as { loop: { exitCondition?: string } }).loop;
      expect(loop.exitCondition).toBe('');
    });

    it('should create loop with goal-aware evalPrompt', async () => {
      const goal = 'Build the payment processing module';
      const result = await service.createOrchestration({ goal });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      const loop = (loopEvents[0] as { loop: { evalPrompt?: string } }).loop;
      expect(loop.evalPrompt).toBeDefined();
      expect(loop.evalPrompt).toContain(goal);
    });

    it('should not create state file for default (non-interactive) orchestrations', async () => {
      const result = await service.createOrchestration({
        goal: 'No state file needed',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      // stateFilePath should be empty or undefined — no state file for agent-eval mode
      expect(result.value.stateFilePath).toBeFalsy();
    });

    it('should emit OrchestrationCreated event', async () => {
      const result = await service.createOrchestration({ goal: 'Test goal' });

      expect(result.ok).toBe(true);
      const events = eventBus.getEmittedEvents('OrchestrationCreated');
      expect(events.length).toBe(1);
    });

    it('should reject empty goal', async () => {
      const result = await service.createOrchestration({ goal: '' });

      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error.message).toContain('goal is required');
    });

    it('should accept goals of any length', async () => {
      const result = await service.createOrchestration({ goal: 'x'.repeat(10000) });

      expect(result.ok).toBe(true);
    });

    it('should use custom maxDepth and maxWorkers', async () => {
      const result = await service.createOrchestration({
        goal: 'Custom config',
        maxDepth: 7,
        maxWorkers: 15,
        maxIterations: 100,
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.maxDepth).toBe(7);
      expect(result.value.maxWorkers).toBe(15);
      expect(result.value.maxIterations).toBe(100);
    });

    it('should pass model to loop creation when model is specified', async () => {
      const result = await service.createOrchestration({
        goal: 'Build auth with specific model',
        model: 'claude-opus-4-5',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      // Verify the LoopCreated event contains the model
      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      expect(loopEvents.length).toBe(1);
      const createdLoop = (loopEvents[0] as { loop: { taskTemplate?: { model?: string } } }).loop;
      // Model is stored on the loop's taskTemplate
      expect(createdLoop?.taskTemplate?.model).toBe('claude-opus-4-5');
    });

    it('should not set model on loop when model is not specified', async () => {
      const result = await service.createOrchestration({
        goal: 'Build without model override',
      });

      expect(result.ok).toBe(true);
      if (!result.ok) return;

      const loopEvents = eventBus.getEmittedEvents('LoopCreated');
      expect(loopEvents.length).toBe(1);
      const createdLoop = (loopEvents[0] as { loop: { taskTemplate?: { model?: string } } }).loop;
      // No model should be set when not requested
      expect(createdLoop?.taskTemplate?.model).toBeUndefined();
    });

    describe('systemPrompt handling', () => {
      it('should use user-provided systemPrompt on loop when custom systemPrompt is given', async () => {
        const result = await service.createOrchestration({
          goal: 'Build auth',
          systemPrompt: 'You are a security expert.',
        });
        expect(result.ok).toBe(true);
        if (!result.ok) return;

        const loopEvents = eventBus.getEmittedEvents('LoopCreated');
        const loop = (loopEvents[0] as { loop: { taskTemplate: { systemPrompt?: string } } }).loop;
        expect(loop.taskTemplate.systemPrompt).toBe('You are a security expert.');
      });

      it('should inject operational contract into prompt when custom systemPrompt is given', async () => {
        const result = await service.createOrchestration({
          goal: 'Build auth',
          systemPrompt: 'You are a security expert.',
        });
        expect(result.ok).toBe(true);
        if (!result.ok) return;

        const loopEvents = eventBus.getEmittedEvents('LoopCreated');
        const loop = (loopEvents[0] as { loop: { taskTemplate: { prompt: string } } }).loop;
        // Contract must be present before the goal
        expect(loop.taskTemplate.prompt).toContain('ORCHESTRATOR CONTRACT');
        expect(loop.taskTemplate.prompt).toContain('Build auth');
      });

      it('should not inject contract when no custom systemPrompt (default flow)', async () => {
        const result = await service.createOrchestration({
          goal: 'Build auth',
        });
        expect(result.ok).toBe(true);
        if (!result.ok) return;

        const loopEvents = eventBus.getEmittedEvents('LoopCreated');
        const loop = (loopEvents[0] as { loop: { taskTemplate: { prompt: string } } }).loop;
        // Default flow: prompt is just the goal, no contract
        expect(loop.taskTemplate.prompt).not.toContain('ORCHESTRATOR CONTRACT');
        expect(loop.taskTemplate.prompt).toContain('Build auth');
      });

      it('should treat empty-string systemPrompt as absent (uses auto-generated)', async () => {
        const result = await service.createOrchestration({
          goal: 'Build auth',
          systemPrompt: '   ',
        });
        expect(result.ok).toBe(true);
        if (!result.ok) return;

        const loopEvents = eventBus.getEmittedEvents('LoopCreated');
        const loop = (loopEvents[0] as { loop: { taskTemplate: { systemPrompt?: string; prompt: string } } }).loop;
        // Empty/whitespace systemPrompt → uses auto-generated, no contract in prompt
        expect(loop.taskTemplate.systemPrompt).not.toBe('   ');
        expect(loop.taskTemplate.prompt).not.toContain('ORCHESTRATOR CONTRACT');
      });
    });
  });

  describe('getOrchestration()', () => {
    it('should retrieve an existing orchestration', async () => {
      const createResult = await service.createOrchestration({ goal: 'Test' });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const getResult = await service.getOrchestration(createResult.value.id);
      expect(getResult.ok).toBe(true);
      if (!getResult.ok) return;
      expect(getResult.value.id).toBe(createResult.value.id);
    });

    it('should return error for non-existent orchestration', async () => {
      const result = await service.getOrchestration(OrchestratorId('orchestrator-nonexistent'));

      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error.message).toContain('not found');
    });
  });

  describe('listOrchestrations()', () => {
    it('should list all orchestrations', async () => {
      await service.createOrchestration({ goal: 'Goal 1' });
      await service.createOrchestration({ goal: 'Goal 2' });

      const result = await service.listOrchestrations();
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.length).toBe(2);
    });

    it('should filter by status', async () => {
      await service.createOrchestration({ goal: 'Running' });

      const result = await service.listOrchestrations(OrchestratorStatus.RUNNING);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.length).toBe(1);
    });
  });

  describe('cancelOrchestration()', () => {
    it('should cancel a running orchestration', async () => {
      const createResult = await service.createOrchestration({ goal: 'To cancel' });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const cancelResult = await service.cancelOrchestration(createResult.value.id, 'No longer needed');
      expect(cancelResult.ok).toBe(true);

      // Should emit OrchestrationCancelled event
      const events = eventBus.getEmittedEvents('OrchestrationCancelled');
      expect(events.length).toBe(1);
    });

    it('should reject cancelling a completed orchestration', async () => {
      const createResult = await service.createOrchestration({ goal: 'Already done' });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      // Manually mark as completed
      const orch = createResult.value;
      const { updateOrchestration } = await import('../../../src/core/domain.js');
      const completed = updateOrchestration(orch, {
        status: OrchestratorStatus.COMPLETED,
        completedAt: Date.now(),
      });
      await orchestrationRepo.update(completed);

      const cancelResult = await service.cancelOrchestration(orch.id);
      expect(cancelResult.ok).toBe(false);
      if (cancelResult.ok) return;
      expect(cancelResult.error.message).toContain('not active');
    });

    it('should emit OrchestrationCancelled event so AttributedTaskCancellationHandler can cascade', async () => {
      // ARCHITECTURE: v1.3.0 cancel cascade is event-driven.
      // cancelOrchestration emits OrchestrationCancelled, and AttributedTaskCancellationHandler
      // (registered in handler-setup.ts) handles attributed task cancellation.
      // This test verifies the service emits the correct event with expected payload.
      const createResult = await service.createOrchestration({ goal: 'Cascade event test' });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;
      const orch = createResult.value;

      const cancelResult = await service.cancelOrchestration(orch.id, 'cascade reason');
      expect(cancelResult.ok).toBe(true);

      // OrchestrationCancelled event must be emitted with the orchestration ID and reason
      const cancelledEvents = eventBus.getEmittedEvents('OrchestrationCancelled') as Array<{
        orchestratorId: string;
        reason?: string;
      }>;
      expect(cancelledEvents.length).toBeGreaterThanOrEqual(1);
      const evt = cancelledEvents.find((e) => e.orchestratorId === orch.id);
      expect(evt).toBeDefined();
      expect(evt?.reason).toBe('cascade reason');
    });

    it('should emit OrchestrationCancelled without performing inline task cancellation', async () => {
      // ARCHITECTURE: v1.3.0 cancel cascade is entirely event-driven.
      // OrchestrationManagerService emits OrchestrationCancelled and does not call
      // taskManager.cancel directly — AttributedTaskCancellationHandler handles the cascade.
      const createResult = await service.createOrchestration({ goal: 'No inline cascade' });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;
      const orch = createResult.value;

      const cancelResult = await service.cancelOrchestration(orch.id, 'cascade test');
      expect(cancelResult.ok).toBe(true);

      // OrchestrationCancelled must be emitted — no inline task cancellation in the service
      const cancelledEvents = eventBus.getEmittedEvents('OrchestrationCancelled');
      expect(cancelledEvents.length).toBeGreaterThanOrEqual(1);
    });
  });
});
