/**
 * OutputHandler Tests
 *
 * Test suite for the OutputHandler class, specifically focusing on
 * question parsing and agent completion handling.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { OutputHandler } from './output-handler.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { EventBus, DomainEvent, AgentWaitingEvent } from '../events/types.js';
import { getProvider } from './providers/registry.js';

// =============================================================================
// Test Helpers
// =============================================================================

function createMockEventBus(): EventBus & { emittedEvents: DomainEvent[] } {
  const emittedEvents: DomainEvent[] = [];

  const mockBus = {
    emittedEvents,
    emit: vi.fn().mockImplementation(<T extends DomainEvent>(event: T): void => {
      emittedEvents.push(event);
    }),
    on: vi.fn(),
    off: vi.fn(),
    once: vi.fn(),
  };

  return mockBus;
}

function createMockAgentRepository() {
  return {
    findById: vi.fn(),
    update: vi.fn(),
    create: vi.fn(),
    findByName: vi.fn(),
    findByStatus: vi.fn(),
    findAll: vi.fn(),
    delete: vi.fn(),
  };
}

// =============================================================================
// Tests
// =============================================================================

describe('OutputHandler', () => {
  let outputHandler: OutputHandler;
  let mockAgentRepo: ReturnType<typeof createMockAgentRepository>;
  let eventBus: ReturnType<typeof createMockEventBus>;

  const mockAgent = {
    id: 'agent-123',
    name: 'test-agent',
    taskId: 'task-456',
    sessionId: 'session-789',
    provider: 'claude',
    mode: 'refine',
  };

  beforeEach(() => {
    mockAgentRepo = createMockAgentRepository();
    eventBus = createMockEventBus();

    outputHandler = new OutputHandler(
      mockAgentRepo as any,
      eventBus,
    );

    // Setup default mock behavior
    mockAgentRepo.findById.mockResolvedValue(mockAgent);
  });

  describe('processAgentOutput', () => {
    it('should correctly parse and handle questions from Claude CLI output', async () => {
      // Arrange: Create realistic Claude CLI output with questions (like fantastic-crane)
      const questionsResult = {
        status: "questions",
        questions: [
          {
            id: "q1",
            question: "What specific components are in the current admin UI? (e.g., tables, forms, modals, navigation)"
          },
          {
            id: "q2",
            question: "What does 'modern look' mean for you? (e.g., dark mode support, specific color scheme, animations)"
          },
          {
            id: "q3",
            question: "Are there any specific shadcn components you want to use or prioritize?"
          }
        ]
      };

      const claudeOutput = JSON.stringify({
        type: "result",
        subtype: "success",
        is_error: false,
        session_id: "test-session-123",
        result: JSON.stringify(questionsResult),
        total_cost_usd: 0.05
      });

      const getAgentWorkdir = vi.fn().mockReturnValue('/test/workdir');
      const provider = getProvider('claude')!;

      // Act
      await outputHandler.processAgentOutput(
        mockAgent.id,
        claudeOutput,
        provider,
        getAgentWorkdir
      );

      // Assert: Agent should be updated with questions and waiting_for_input status
      expect(mockAgentRepo.update).toHaveBeenCalledWith(mockAgent.id, {
        pendingQuestions: JSON.stringify({
          questions: [
            {
              id: 'q1',
              question: 'What specific components are in the current admin UI? (e.g., tables, forms, modals, navigation)'
            },
            {
              id: 'q2',
              question: 'What does \'modern look\' mean for you? (e.g., dark mode support, specific color scheme, animations)'
            },
            {
              id: 'q3',
              question: 'Are there any specific shadcn components you want to use or prioritize?'
            }
          ]
        }),
        status: 'waiting_for_input'
      });

      // Should be called at least once (could be once or twice depending on session ID extraction)
      expect(mockAgentRepo.update).toHaveBeenCalledTimes(1);

      // Assert: AgentWaitingEvent should be emitted
      const waitingEvents = eventBus.emittedEvents.filter(e => e.type === 'agent:waiting') as AgentWaitingEvent[];
      expect(waitingEvents).toHaveLength(1);
      expect(waitingEvents[0].payload.questions).toEqual([
        {
          id: 'q1',
          question: 'What specific components are in the current admin UI? (e.g., tables, forms, modals, navigation)'
        },
        {
          id: 'q2',
          question: 'What does \'modern look\' mean for you? (e.g., dark mode support, specific color scheme, animations)'
        },
        {
          id: 'q3',
          question: 'Are there any specific shadcn components you want to use or prioritize?'
        }
      ]);
    });

    it('should handle malformed questions gracefully', async () => {
      // Arrange: Create output with malformed questions JSON
      const malformedOutput = JSON.stringify({
        type: "result",
        subtype: "success",
        is_error: false,
        session_id: "test-session",
        result: '{"status": "questions", "questions": [malformed json]}',
        total_cost_usd: 0.05
      });

      const getAgentWorkdir = vi.fn().mockReturnValue('/test/workdir');
      const provider = getProvider('claude')!;

      // Act & Assert: Should not throw, should handle error gracefully
      await expect(
        outputHandler.processAgentOutput(
          mockAgent.id,
          malformedOutput,
          provider,
          getAgentWorkdir
        )
      ).resolves.not.toThrow();

      // Should update status to crashed due to malformed JSON
      const updateCalls = mockAgentRepo.update.mock.calls;
      const crashedCall = updateCalls.find(call => call[1]?.status === 'crashed');
      expect(crashedCall).toBeDefined();
    });

    it('should correctly handle "done" status without questions', async () => {
      // Arrange: Create output with done status
      const doneOutput = JSON.stringify({
        type: "result",
        subtype: "success",
        is_error: false,
        session_id: "test-session",
        result: JSON.stringify({
          status: "done",
          message: "Task completed successfully"
        }),
        total_cost_usd: 0.05
      });

      const getAgentWorkdir = vi.fn().mockReturnValue('/test/workdir');
      const provider = getProvider('claude')!;

      // Act
      await outputHandler.processAgentOutput(
        mockAgent.id,
        doneOutput,
        provider,
        getAgentWorkdir
      );

      // Assert: Should not set waiting_for_input status or pendingQuestions
      const updateCalls = mockAgentRepo.update.mock.calls;
      const waitingCall = updateCalls.find(call => call[1]?.status === 'waiting_for_input');
      expect(waitingCall).toBeUndefined();

      const questionsCall = updateCalls.find(call => call[1]?.pendingQuestions);
      expect(questionsCall).toBeUndefined();
    });
  });

  describe('getPendingQuestions', () => {
    it('should retrieve and parse stored pending questions', async () => {
      // Arrange
      const questionsPayload = {
        questions: [
          { id: 'q1', question: 'Test question 1?' },
          { id: 'q2', question: 'Test question 2?' }
        ]
      };

      mockAgentRepo.findById.mockResolvedValue({
        ...mockAgent,
        pendingQuestions: JSON.stringify(questionsPayload)
      });

      // Act
      const result = await outputHandler.getPendingQuestions(mockAgent.id);

      // Assert
      expect(result).toEqual(questionsPayload);
      expect(mockAgentRepo.findById).toHaveBeenCalledWith(mockAgent.id);
    });

    it('should return null when no pending questions exist', async () => {
      // Arrange
      mockAgentRepo.findById.mockResolvedValue({
        ...mockAgent,
        pendingQuestions: null
      });

      // Act
      const result = await outputHandler.getPendingQuestions(mockAgent.id);

      // Assert
      expect(result).toBeNull();
    });
  });

  // =============================================================================
  // formatAnswersAsPrompt Tests
  // =============================================================================

  describe('formatAnswersAsPrompt', () => {
    it('should format normal answers correctly', () => {
      const answers = {
        'q1': 'The admin UI has tables and forms',
        'q2': 'Modern means dark mode and clean aesthetics'
      };

      const result = outputHandler.formatAnswersAsPrompt(answers);

      expect(result).toBe(
        'Here are my answers to your questions:\n' +
        '[q1]: The admin UI has tables and forms\n' +
        '[q2]: Modern means dark mode and clean aesthetics'
      );
    });

    it('should handle instruction-enhanced answers for retry scenarios', () => {
      const answers = {
        'q1': 'Fix the authentication bug',
        '__instruction__': 'IMPORTANT: Create a signal.json file when done'
      };

      const result = outputHandler.formatAnswersAsPrompt(answers);

      expect(result).toBe(
        'IMPORTANT: Create a signal.json file when done\n\n' +
        'Here are my answers to your questions:\n' +
        '[q1]: Fix the authentication bug'
      );
    });

    it('should handle instruction with whitespace correctly', () => {
      const answers = {
        'q1': 'Complete the task',
        '__instruction__': '   \n  Some instruction with whitespace  \n   '
      };

      const result = outputHandler.formatAnswersAsPrompt(answers);

      expect(result).toBe(
        'Some instruction with whitespace\n\n' +
        'Here are my answers to your questions:\n' +
        '[q1]: Complete the task'
      );
    });

    it('should work with only instruction and no real answers', () => {
      const answers = {
        '__instruction__': 'Retry with this instruction'
      };

      const result = outputHandler.formatAnswersAsPrompt(answers);

      expect(result).toBe(
        'Retry with this instruction\n\n' +
        'Here are my answers to your questions:\n'
      );
    });

    it('should work with empty answers object', () => {
      const answers = {};

      const result = outputHandler.formatAnswersAsPrompt(answers);

      expect(result).toBe(
        'Here are my answers to your questions:\n'
      );
    });
  });
});