import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AgentService } from '@/services/agent-service.js';
import { ProviderFactory } from '@/providers/provider-factory.js';
import * as fs from 'fs/promises';
import * as imageHandler from '@automaker/utils';
import * as promptBuilder from '@automaker/utils';
import * as contextLoader from '@automaker/utils';
import { collectAsyncGenerator } from '../../utils/helpers.js';

// Create a shared mock logger instance for assertions using vi.hoisted
const mockLogger = vi.hoisted(() => ({
  info: vi.fn(),
  error: vi.fn(),
  warn: vi.fn(),
  debug: vi.fn(),
}));

vi.mock('fs/promises');
vi.mock('@/providers/provider-factory.js');
vi.mock('@automaker/utils', async () => {
  const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
  return {
    ...actual,
    loadContextFiles: vi.fn(),
    buildPromptWithImages: vi.fn(),
    readImageAsBase64: vi.fn(),
    createLogger: vi.fn(() => mockLogger),
  };
});

describe('agent-service.ts', () => {
  let service: AgentService;
  const mockEvents = {
    subscribe: vi.fn(),
    emit: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
    service = new AgentService('/test/data', mockEvents as any);

    // Mock loadContextFiles to return empty context by default
    vi.mocked(contextLoader.loadContextFiles).mockResolvedValue({
      files: [],
      formattedPrompt: '',
    });
  });

  describe('initialize', () => {
    it('should create state directory', async () => {
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.initialize();

      expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('agent-sessions'), {
        recursive: true,
      });
    });
  });

  describe('startConversation', () => {
    it('should create new session with empty messages', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      const result = await service.startConversation({
        sessionId: 'session-1',
        workingDirectory: '/test/dir',
      });

      expect(result.success).toBe(true);
      expect(result.messages).toEqual([]);
      expect(result.sessionId).toBe('session-1');
    });

    it('should load existing session', async () => {
      const existingMessages = [
        {
          id: 'msg-1',
          role: 'user',
          content: 'Hello',
          timestamp: '2024-01-01T00:00:00Z',
        },
      ];

      vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingMessages));

      const result = await service.startConversation({
        sessionId: 'session-1',
        workingDirectory: '/test/dir',
      });

      expect(result.success).toBe(true);
      expect(result.messages).toEqual(existingMessages);
    });

    it('should use process.cwd() if no working directory provided', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      const result = await service.startConversation({
        sessionId: 'session-1',
      });

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

    it('should reuse existing session if already started', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      // Start session first time
      await service.startConversation({
        sessionId: 'session-1',
      });

      // Start again with same ID
      const result = await service.startConversation({
        sessionId: 'session-1',
      });

      expect(result.success).toBe(true);
      // First call reads session file, metadata file, and queue state file (3 calls)
      // Second call should reuse in-memory session (no additional calls)
      expect(fs.readFile).toHaveBeenCalledTimes(3);
    });
  });

  describe('sendMessage', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
        workingDirectory: '/test/dir',
      });
    });

    it('should throw if session not found', async () => {
      await expect(
        service.sendMessage({
          sessionId: 'nonexistent',
          message: 'Hello',
        })
      ).rejects.toThrow('Session nonexistent not found');
    });

    it('should process message and stream responses', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'assistant',
            message: {
              role: 'assistant',
              content: [{ type: 'text', text: 'Response' }],
            },
          };
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
        content: 'Hello',
        hasImages: false,
      });

      const result = await service.sendMessage({
        sessionId: 'session-1',
        message: 'Hello',
        workingDirectory: '/custom/dir',
      });

      expect(result.success).toBe(true);
      expect(mockEvents.emit).toHaveBeenCalled();
    });

    it('should handle images in message', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      vi.mocked(imageHandler.readImageAsBase64).mockResolvedValue({
        base64: 'base64data',
        mimeType: 'image/png',
        filename: 'test.png',
        originalPath: '/path/test.png',
      });

      vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
        content: 'Check image',
        hasImages: true,
      });

      await service.sendMessage({
        sessionId: 'session-1',
        message: 'Check this',
        imagePaths: ['/path/test.png'],
      });

      expect(imageHandler.readImageAsBase64).toHaveBeenCalledWith('/path/test.png');
    });

    it('should handle failed image loading gracefully', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      vi.mocked(imageHandler.readImageAsBase64).mockRejectedValue(new Error('Image not found'));

      vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
        content: 'Check image',
        hasImages: false,
      });

      await service.sendMessage({
        sessionId: 'session-1',
        message: 'Check this',
        imagePaths: ['/path/test.png'],
      });

      expect(mockLogger.error).toHaveBeenCalled();
    });

    it('should use custom model if provided', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
        content: 'Hello',
        hasImages: false,
      });

      await service.sendMessage({
        sessionId: 'session-1',
        message: 'Hello',
        model: 'claude-sonnet-4-20250514',
      });

      expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
    });

    it('should save session messages', async () => {
      const mockProvider = {
        getName: () => 'claude',
        executeQuery: async function* () {
          yield {
            type: 'result',
            subtype: 'success',
          };
        },
      };

      vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

      vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
        content: 'Hello',
        hasImages: false,
      });

      await service.sendMessage({
        sessionId: 'session-1',
        message: 'Hello',
      });

      expect(fs.writeFile).toHaveBeenCalled();
    });
  });

  describe('stopExecution', () => {
    it('should stop execution for a session', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      await service.startConversation({
        sessionId: 'session-1',
      });

      // Should return success
      const result = await service.stopExecution('session-1');
      expect(result.success).toBeDefined();
    });
  });

  describe('getHistory', () => {
    it('should return message history', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);

      await service.startConversation({
        sessionId: 'session-1',
      });

      const history = service.getHistory('session-1');

      expect(history).toBeDefined();
      expect(history?.messages).toEqual([]);
    });

    it('should handle non-existent session', () => {
      const history = service.getHistory('nonexistent');
      expect(history).toBeDefined(); // Returns error object
    });
  });

  describe('clearSession', () => {
    it('should clear session messages', async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });

      await service.clearSession('session-1');

      const history = service.getHistory('session-1');
      expect(history?.messages).toEqual([]);
      expect(fs.writeFile).toHaveBeenCalled();
    });
  });

  describe('createSession', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue('{}');
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
    });

    it('should create a new session with metadata', async () => {
      const session = await service.createSession('Test Session', '/test/project', '/test/dir');

      expect(session.id).toBeDefined();
      expect(session.name).toBe('Test Session');
      expect(session.projectPath).toBe('/test/project');
      expect(session.workingDirectory).toBeDefined();
      expect(session.createdAt).toBeDefined();
      expect(session.updatedAt).toBeDefined();
    });

    it('should use process.cwd() if no working directory provided', async () => {
      const session = await service.createSession('Test Session');

      expect(session.workingDirectory).toBeDefined();
    });

    it('should validate working directory', async () => {
      // Set ALLOWED_ROOT_DIRECTORY to restrict paths
      const originalAllowedRoot = process.env.ALLOWED_ROOT_DIRECTORY;
      process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/projects';

      // Re-import platform to initialize with new env var
      vi.resetModules();
      const { initAllowedPaths } = await import('@automaker/platform');
      initAllowedPaths();

      const { AgentService } = await import('@/services/agent-service.js');
      const testService = new AgentService('/test/data', mockEvents as any);
      vi.mocked(fs.readFile).mockResolvedValue('{}');
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);

      await expect(
        testService.createSession('Test Session', undefined, '/invalid/path')
      ).rejects.toThrow();

      // Restore original value
      if (originalAllowedRoot) {
        process.env.ALLOWED_ROOT_DIRECTORY = originalAllowedRoot;
      } else {
        delete process.env.ALLOWED_ROOT_DIRECTORY;
      }
      vi.resetModules();
      const { initAllowedPaths: reinit } = await import('@automaker/platform');
      reinit();
    });
  });

  describe('setSessionModel', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });
    });

    it('should set model for existing session', async () => {
      vi.mocked(fs.readFile).mockResolvedValue('{"session-1": {}}');
      const result = await service.setSessionModel('session-1', 'claude-sonnet-4-20250514');

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

    it('should return false for non-existent session', async () => {
      const result = await service.setSessionModel('nonexistent', 'claude-sonnet-4-20250514');

      expect(result).toBe(false);
    });
  });

  describe('updateSession', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          'session-1': {
            id: 'session-1',
            name: 'Test Session',
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-01T00:00:00Z',
          },
        })
      );
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
    });

    it('should update session metadata', async () => {
      const result = await service.updateSession('session-1', { name: 'Updated Name' });

      expect(result).not.toBeNull();
      expect(result?.name).toBe('Updated Name');
      expect(result?.updatedAt).not.toBe('2024-01-01T00:00:00Z');
    });

    it('should return null for non-existent session', async () => {
      const result = await service.updateSession('nonexistent', { name: 'Updated Name' });

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

  describe('archiveSession', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          'session-1': {
            id: 'session-1',
            name: 'Test Session',
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-01T00:00:00Z',
          },
        })
      );
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
    });

    it('should archive a session', async () => {
      const result = await service.archiveSession('session-1');

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

    it('should return false for non-existent session', async () => {
      const result = await service.archiveSession('nonexistent');

      expect(result).toBe(false);
    });
  });

  describe('unarchiveSession', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          'session-1': {
            id: 'session-1',
            name: 'Test Session',
            archived: true,
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-01T00:00:00Z',
          },
        })
      );
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
    });

    it('should unarchive a session', async () => {
      const result = await service.unarchiveSession('session-1');

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

    it('should return false for non-existent session', async () => {
      const result = await service.unarchiveSession('nonexistent');

      expect(result).toBe(false);
    });
  });

  describe('deleteSession', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          'session-1': {
            id: 'session-1',
            name: 'Test Session',
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-01T00:00:00Z',
          },
        })
      );
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.unlink).mockResolvedValue(undefined);
    });

    it('should delete a session', async () => {
      const result = await service.deleteSession('session-1');

      expect(result).toBe(true);
      expect(fs.writeFile).toHaveBeenCalled();
    });

    it('should return false for non-existent session', async () => {
      const result = await service.deleteSession('nonexistent');

      expect(result).toBe(false);
    });
  });

  describe('listSessions', () => {
    beforeEach(() => {
      vi.mocked(fs.readFile).mockResolvedValue(
        JSON.stringify({
          'session-1': {
            id: 'session-1',
            name: 'Test Session 1',
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-02T00:00:00Z',
            archived: false,
          },
          'session-2': {
            id: 'session-2',
            name: 'Test Session 2',
            createdAt: '2024-01-01T00:00:00Z',
            updatedAt: '2024-01-03T00:00:00Z',
            archived: true,
          },
        })
      );
    });

    it('should list non-archived sessions by default', async () => {
      const sessions = await service.listSessions();

      expect(sessions.length).toBe(1);
      expect(sessions[0].id).toBe('session-1');
    });

    it('should include archived sessions when requested', async () => {
      const sessions = await service.listSessions(true);

      expect(sessions.length).toBe(2);
    });

    it('should sort sessions by updatedAt descending', async () => {
      const sessions = await service.listSessions(true);

      expect(sessions[0].id).toBe('session-2');
      expect(sessions[1].id).toBe('session-1');
    });
  });

  describe('addToQueue', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });
    });

    it('should add prompt to queue', async () => {
      const result = await service.addToQueue('session-1', {
        message: 'Test prompt',
        imagePaths: ['/test/image.png'],
        model: 'claude-sonnet-4-20250514',
      });

      expect(result.success).toBe(true);
      expect(result.queuedPrompt).toBeDefined();
      expect(result.queuedPrompt?.message).toBe('Test prompt');
      expect(mockEvents.emit).toHaveBeenCalled();
    });

    it('should return error for non-existent session', async () => {
      const result = await service.addToQueue('nonexistent', {
        message: 'Test prompt',
      });

      expect(result.success).toBe(false);
      expect(result.error).toBe('Session not found');
    });
  });

  describe('getQueue', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });
    });

    it('should return queue for session', async () => {
      await service.addToQueue('session-1', { message: 'Test prompt' });
      const result = service.getQueue('session-1');

      expect(result.success).toBe(true);
      expect(result.queue).toBeDefined();
      expect(result.queue?.length).toBe(1);
    });

    it('should return error for non-existent session', () => {
      const result = service.getQueue('nonexistent');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Session not found');
    });
  });

  describe('removeFromQueue', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });

      const addResult = await service.addToQueue('session-1', { message: 'Test prompt' });
      vi.clearAllMocks();
    });

    it('should remove prompt from queue', async () => {
      const queueResult = service.getQueue('session-1');
      const promptId = queueResult.queue![0].id;

      const result = await service.removeFromQueue('session-1', promptId);

      expect(result.success).toBe(true);
      expect(mockEvents.emit).toHaveBeenCalled();
    });

    it('should return error for non-existent session', async () => {
      const result = await service.removeFromQueue('nonexistent', 'prompt-id');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Session not found');
    });

    it('should return error for non-existent prompt', async () => {
      const result = await service.removeFromQueue('session-1', 'nonexistent-prompt-id');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Prompt not found in queue');
    });
  });

  describe('clearQueue', () => {
    beforeEach(async () => {
      const error: any = new Error('ENOENT');
      error.code = 'ENOENT';
      vi.mocked(fs.readFile).mockRejectedValue(error);
      vi.mocked(fs.writeFile).mockResolvedValue(undefined);
      vi.mocked(fs.mkdir).mockResolvedValue(undefined);

      await service.startConversation({
        sessionId: 'session-1',
      });

      await service.addToQueue('session-1', { message: 'Test prompt 1' });
      await service.addToQueue('session-1', { message: 'Test prompt 2' });
      vi.clearAllMocks();
    });

    it('should clear all prompts from queue', async () => {
      const result = await service.clearQueue('session-1');

      expect(result.success).toBe(true);
      const queueResult = service.getQueue('session-1');
      expect(queueResult.queue?.length).toBe(0);
      expect(mockEvents.emit).toHaveBeenCalled();
    });

    it('should return error for non-existent session', async () => {
      const result = await service.clearQueue('nonexistent');

      expect(result.success).toBe(false);
      expect(result.error).toBe('Session not found');
    });
  });
});
