import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { IdeationService } from '@/services/ideation-service.js';
import type { EventEmitter } from '@/lib/events.js';
import type { SettingsService } from '@/services/settings-service.js';
import type { FeatureLoader } from '@/services/feature-loader.js';
import * as secureFs from '@/lib/secure-fs.js';
import * as platform from '@automaker/platform';
import * as utils from '@automaker/utils';
import type {
  CreateIdeaInput,
  UpdateIdeaInput,
  Idea,
  IdeationSession,
  StartSessionOptions,
} from '@automaker/types';
import { ProviderFactory } from '@/providers/provider-factory.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(),
}));

// Mock dependencies
vi.mock('@/lib/secure-fs.js');
vi.mock('@automaker/platform');
vi.mock('@automaker/utils', async () => {
  const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
  return {
    ...actual,
    createLogger: vi.fn(() => mockLogger),
    loadContextFiles: vi.fn(),
    isAbortError: vi.fn(),
  };
});
vi.mock('@/providers/provider-factory.js');
vi.mock('@/lib/sdk-options.js', () => ({
  createChatOptions: vi.fn(() => ({
    model: 'claude-sonnet-4-20250514',
    systemPrompt: 'test prompt',
  })),
  validateWorkingDirectory: vi.fn(),
}));

describe('IdeationService', () => {
  let service: IdeationService;
  let mockEvents: EventEmitter;
  let mockSettingsService: SettingsService;
  let mockFeatureLoader: FeatureLoader;
  const testProjectPath = '/test/project';

  beforeEach(() => {
    vi.clearAllMocks();

    // Create mock event emitter
    mockEvents = {
      emit: vi.fn(),
      on: vi.fn(),
      off: vi.fn(),
      removeAllListeners: vi.fn(),
    } as unknown as EventEmitter;

    // Create mock settings service
    mockSettingsService = {} as SettingsService;

    // Create mock feature loader
    mockFeatureLoader = {
      getAll: vi.fn().mockResolvedValue([]),
    } as unknown as FeatureLoader;

    // Mock platform functions
    vi.mocked(platform.ensureIdeationDir).mockResolvedValue(undefined);
    vi.mocked(platform.getIdeaDir).mockReturnValue(
      '/test/project/.automaker/ideation/ideas/idea-123'
    );
    vi.mocked(platform.getIdeaPath).mockReturnValue(
      '/test/project/.automaker/ideation/ideas/idea-123/idea.json'
    );
    vi.mocked(platform.getIdeasDir).mockReturnValue('/test/project/.automaker/ideation/ideas');
    vi.mocked(platform.getIdeationSessionPath).mockReturnValue(
      '/test/project/.automaker/ideation/sessions/session-123.json'
    );
    vi.mocked(platform.getIdeationSessionsDir).mockReturnValue(
      '/test/project/.automaker/ideation/sessions'
    );
    vi.mocked(platform.getIdeationAnalysisPath).mockReturnValue(
      '/test/project/.automaker/ideation/analysis.json'
    );

    // Mock utils (already mocked above, but reset return values)
    vi.mocked(utils.loadContextFiles).mockResolvedValue({
      formattedPrompt: 'Test context',
      files: [],
    });
    vi.mocked(utils.isAbortError).mockReturnValue(false);

    service = new IdeationService(mockEvents, mockSettingsService, mockFeatureLoader);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // ============================================================================
  // Session Management Tests
  // ============================================================================

  describe('Session Management', () => {
    describe('startSession', () => {
      it('should create a new session with default options', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const session = await service.startSession(testProjectPath);

        expect(session).toBeDefined();
        expect(session.id).toMatch(/^session-/);
        expect(session.projectPath).toBe(testProjectPath);
        expect(session.status).toBe('active');
        expect(session.createdAt).toBeDefined();
        expect(session.updatedAt).toBeDefined();
        expect(platform.ensureIdeationDir).toHaveBeenCalledWith(testProjectPath);
        expect(secureFs.writeFile).toHaveBeenCalled();
        expect(mockEvents.emit).toHaveBeenCalledWith('ideation:session-started', {
          sessionId: session.id,
          projectPath: testProjectPath,
        });
      });

      it('should create session with custom options', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const options: StartSessionOptions = {
          promptCategory: 'features',
          promptId: 'new-features',
        };

        const session = await service.startSession(testProjectPath, options);

        expect(session.promptCategory).toBe('features');
        expect(session.promptId).toBe('new-features');
      });

      it('should send initial message if provided in options', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify({ features: [] }));

        // Mock provider
        const mockProvider = {
          executeQuery: vi.fn().mockReturnValue({
            async *[Symbol.asyncIterator]() {
              yield {
                type: 'result',
                subtype: 'success',
                result: 'AI response',
              };
            },
          }),
        };
        vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);

        const options: StartSessionOptions = {
          initialMessage: 'Hello, AI!',
        };

        await service.startSession(testProjectPath, options);

        // Give time for the async message to process
        await new Promise((resolve) => setTimeout(resolve, 10));

        expect(mockProvider.executeQuery).toHaveBeenCalled();
      });
    });

    describe('getSession', () => {
      it('should return null for non-existent session', async () => {
        vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));

        const result = await service.getSession(testProjectPath, 'non-existent');

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

      it('should return active session from memory', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const session = await service.startSession(testProjectPath);
        const retrieved = await service.getSession(testProjectPath, session.id);

        expect(retrieved).toBeDefined();
        expect(retrieved?.id).toBe(session.id);
        expect(retrieved?.messages).toEqual([]);
      });

      it('should load session from disk if not in memory', async () => {
        const mockSession: IdeationSession = {
          id: 'session-123',
          projectPath: testProjectPath,
          status: 'active',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        const sessionData = {
          session: mockSession,
          messages: [],
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(sessionData));

        const result = await service.getSession(testProjectPath, 'session-123');

        expect(result).toBeDefined();
        expect(result?.id).toBe('session-123');
      });
    });

    describe('stopSession', () => {
      it('should stop an active session', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const session = await service.startSession(testProjectPath);
        await service.stopSession(session.id);

        expect(mockEvents.emit).toHaveBeenCalledWith('ideation:session-ended', {
          sessionId: session.id,
        });
      });

      it('should handle stopping non-existent session gracefully', async () => {
        await expect(service.stopSession('non-existent')).resolves.not.toThrow();
      });
    });

    describe('isSessionRunning', () => {
      it('should return false for non-existent session', () => {
        expect(service.isSessionRunning('non-existent')).toBe(false);
      });

      it('should return false for idle session', async () => {
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const session = await service.startSession(testProjectPath);
        expect(service.isSessionRunning(session.id)).toBe(false);
      });
    });
  });

  // ============================================================================
  // Ideas CRUD Tests
  // ============================================================================

  describe('Ideas CRUD', () => {
    describe('createIdea', () => {
      it('should create a new idea with required fields', async () => {
        vi.mocked(secureFs.mkdir).mockResolvedValue(undefined);
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const input: CreateIdeaInput = {
          title: 'Test Idea',
          description: 'This is a test idea',
          category: 'features',
        };

        const idea = await service.createIdea(testProjectPath, input);

        expect(idea).toBeDefined();
        expect(idea.id).toMatch(/^idea-/);
        expect(idea.title).toBe('Test Idea');
        expect(idea.description).toBe('This is a test idea');
        expect(idea.category).toBe('features');
        expect(idea.status).toBe('raw');
        expect(idea.impact).toBe('medium');
        expect(idea.effort).toBe('medium');
        expect(secureFs.mkdir).toHaveBeenCalled();
        expect(secureFs.writeFile).toHaveBeenCalled();
      });

      it('should create idea with all optional fields', async () => {
        vi.mocked(secureFs.mkdir).mockResolvedValue(undefined);
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const input: CreateIdeaInput = {
          title: 'Full Idea',
          description: 'Complete idea',
          category: 'features',
          status: 'refined',
          impact: 'high',
          effort: 'low',
          conversationId: 'conv-123',
          sourcePromptId: 'prompt-123',
          userStories: ['Story 1', 'Story 2'],
          notes: 'Additional notes',
        };

        const idea = await service.createIdea(testProjectPath, input);

        expect(idea.status).toBe('refined');
        expect(idea.impact).toBe('high');
        expect(idea.effort).toBe('low');
        expect(idea.conversationId).toBe('conv-123');
        expect(idea.sourcePromptId).toBe('prompt-123');
        expect(idea.userStories).toEqual(['Story 1', 'Story 2']);
        expect(idea.notes).toBe('Additional notes');
      });
    });

    describe('getIdeas', () => {
      it('should return empty array when ideas directory does not exist', async () => {
        vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));

        const ideas = await service.getIdeas(testProjectPath);

        expect(ideas).toEqual([]);
      });

      it('should load all ideas from disk', async () => {
        vi.mocked(secureFs.access).mockResolvedValue(undefined);
        vi.mocked(secureFs.readdir).mockResolvedValue([
          { name: 'idea-1', isDirectory: () => true } as any,
          { name: 'idea-2', isDirectory: () => true } as any,
        ]);

        const idea1: Idea = {
          id: 'idea-1',
          title: 'Idea 1',
          description: 'First idea',
          category: 'features',
          status: 'raw',
          impact: 'medium',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        const idea2: Idea = {
          id: 'idea-2',
          title: 'Idea 2',
          description: 'Second idea',
          category: 'bugs',
          status: 'refined',
          impact: 'high',
          effort: 'low',
          createdAt: '2024-01-02T00:00:00.000Z',
          updatedAt: '2024-01-02T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile)
          .mockResolvedValueOnce(JSON.stringify(idea1))
          .mockResolvedValueOnce(JSON.stringify(idea2));

        const ideas = await service.getIdeas(testProjectPath);

        expect(ideas).toHaveLength(2);
        expect(ideas[0].id).toBe('idea-2'); // Sorted by updatedAt descending
        expect(ideas[1].id).toBe('idea-1');
      });

      it('should skip invalid idea files', async () => {
        vi.mocked(secureFs.access).mockResolvedValue(undefined);
        vi.mocked(secureFs.readdir).mockResolvedValue([
          { name: 'idea-1', isDirectory: () => true } as any,
          { name: 'idea-2', isDirectory: () => true } as any,
        ]);

        const validIdea: Idea = {
          id: 'idea-1',
          title: 'Valid Idea',
          description: 'Valid',
          category: 'features',
          status: 'raw',
          impact: 'medium',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile)
          .mockResolvedValueOnce(JSON.stringify(validIdea))
          .mockRejectedValueOnce(new Error('Invalid JSON'));

        const ideas = await service.getIdeas(testProjectPath);

        expect(ideas).toHaveLength(1);
        expect(ideas[0].id).toBe('idea-1');
      });
    });

    describe('getIdea', () => {
      it('should return idea by id', async () => {
        const mockIdea: Idea = {
          id: 'idea-123',
          title: 'Test Idea',
          description: 'Test',
          category: 'features',
          status: 'raw',
          impact: 'medium',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));

        const idea = await service.getIdea(testProjectPath, 'idea-123');

        expect(idea).toBeDefined();
        expect(idea?.id).toBe('idea-123');
      });

      it('should return null for non-existent idea', async () => {
        vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));

        const idea = await service.getIdea(testProjectPath, 'non-existent');

        expect(idea).toBeNull();
      });
    });

    describe('updateIdea', () => {
      it('should update idea fields', async () => {
        const existingIdea: Idea = {
          id: 'idea-123',
          title: 'Original Title',
          description: 'Original',
          category: 'features',
          status: 'raw',
          impact: 'medium',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(existingIdea));
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const updates: UpdateIdeaInput = {
          title: 'Updated Title',
          status: 'refined',
        };

        const updated = await service.updateIdea(testProjectPath, 'idea-123', updates);

        expect(updated).toBeDefined();
        expect(updated?.title).toBe('Updated Title');
        expect(updated?.status).toBe('refined');
        expect(updated?.description).toBe('Original'); // Unchanged
        expect(updated?.updatedAt).not.toBe('2024-01-01T00:00:00.000Z'); // Should be updated
      });

      it('should return null for non-existent idea', async () => {
        vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));

        const updated = await service.updateIdea(testProjectPath, 'non-existent', {
          title: 'New Title',
        });

        expect(updated).toBeNull();
      });
    });

    describe('deleteIdea', () => {
      it('should delete idea directory', async () => {
        vi.mocked(secureFs.rm).mockResolvedValue(undefined);

        await service.deleteIdea(testProjectPath, 'idea-123');

        expect(secureFs.rm).toHaveBeenCalledWith(
          expect.stringContaining('idea-123'),
          expect.objectContaining({ recursive: true })
        );
      });

      it('should handle non-existent idea gracefully', async () => {
        vi.mocked(secureFs.rm).mockRejectedValue(new Error('ENOENT'));

        await expect(service.deleteIdea(testProjectPath, 'non-existent')).resolves.not.toThrow();
      });
    });

    describe('archiveIdea', () => {
      it('should set idea status to archived', async () => {
        const existingIdea: Idea = {
          id: 'idea-123',
          title: 'Test',
          description: 'Test',
          category: 'features',
          status: 'raw',
          impact: 'medium',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(existingIdea));
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);

        const archived = await service.archiveIdea(testProjectPath, 'idea-123');

        expect(archived).toBeDefined();
        expect(archived?.status).toBe('archived');
      });
    });
  });

  // ============================================================================
  // Conversion Tests
  // ============================================================================

  describe('Idea to Feature Conversion', () => {
    describe('convertToFeature', () => {
      it('should convert idea to feature with basic fields', async () => {
        const mockIdea: Idea = {
          id: 'idea-123',
          title: 'Add Dark Mode',
          description: 'Implement dark mode theme',
          category: 'feature',
          status: 'refined',
          impact: 'high',
          effort: 'medium',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));

        const feature = await service.convertToFeature(testProjectPath, 'idea-123');

        expect(feature).toBeDefined();
        expect(feature.id).toMatch(/^feature-/);
        expect(feature.title).toBe('Add Dark Mode');
        expect(feature.description).toBe('Implement dark mode theme');
        expect(feature.category).toBe('ui'); // features -> ui mapping
        expect(feature.status).toBe('backlog');
      });

      it('should include user stories in feature description', async () => {
        const mockIdea: Idea = {
          id: 'idea-123',
          title: 'Test',
          description: 'Base description',
          category: 'features',
          status: 'refined',
          impact: 'medium',
          effort: 'medium',
          userStories: ['As a user, I want X', 'As a user, I want Y'],
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));

        const feature = await service.convertToFeature(testProjectPath, 'idea-123');

        expect(feature.description).toContain('Base description');
        expect(feature.description).toContain('## User Stories');
        expect(feature.description).toContain('As a user, I want X');
        expect(feature.description).toContain('As a user, I want Y');
      });

      it('should include notes in feature description', async () => {
        const mockIdea: Idea = {
          id: 'idea-123',
          title: 'Test',
          description: 'Base description',
          category: 'features',
          status: 'refined',
          impact: 'medium',
          effort: 'medium',
          notes: 'Important implementation notes',
          createdAt: '2024-01-01T00:00:00.000Z',
          updatedAt: '2024-01-01T00:00:00.000Z',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));

        const feature = await service.convertToFeature(testProjectPath, 'idea-123');

        expect(feature.description).toContain('Base description');
        expect(feature.description).toContain('## Notes');
        expect(feature.description).toContain('Important implementation notes');
      });

      it('should throw error for non-existent idea', async () => {
        vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));

        await expect(service.convertToFeature(testProjectPath, 'non-existent')).rejects.toThrow(
          'Idea non-existent not found'
        );
      });
    });
  });

  // ============================================================================
  // Project Analysis Tests
  // ============================================================================

  describe('Project Analysis', () => {
    describe('analyzeProject', () => {
      it('should analyze project and generate suggestions', async () => {
        vi.mocked(secureFs.readFile).mockResolvedValue(
          JSON.stringify({
            name: 'test-project',
            dependencies: {},
          })
        );
        vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
        vi.mocked(secureFs.access).mockResolvedValue(undefined);
        vi.mocked(secureFs.readdir).mockResolvedValue([]);

        const result = await service.analyzeProject(testProjectPath);

        expect(result).toBeDefined();
        expect(result.projectPath).toBe(testProjectPath);
        expect(result.analyzedAt).toBeDefined();
        expect(result.suggestions).toBeDefined();
        expect(Array.isArray(result.suggestions)).toBe(true);
        expect(mockEvents.emit).toHaveBeenCalledWith(
          'ideation:analysis',
          expect.objectContaining({
            type: 'ideation:analysis-started',
          })
        );
        expect(mockEvents.emit).toHaveBeenCalledWith(
          'ideation:analysis',
          expect.objectContaining({
            type: 'ideation:analysis-complete',
          })
        );
      });

      it('should emit error event on failure', async () => {
        // Mock writeFile to fail (this is called after gatherProjectStructure and isn't caught)
        vi.mocked(secureFs.readFile).mockResolvedValue(
          JSON.stringify({
            name: 'test-project',
            dependencies: {},
          })
        );
        vi.mocked(secureFs.writeFile).mockRejectedValue(new Error('Write failed'));

        await expect(service.analyzeProject(testProjectPath)).rejects.toThrow();

        expect(mockEvents.emit).toHaveBeenCalledWith(
          'ideation:analysis',
          expect.objectContaining({
            type: 'ideation:analysis-error',
          })
        );
      });
    });

    describe('getCachedAnalysis', () => {
      it('should return cached analysis if exists', async () => {
        const mockAnalysis = {
          projectPath: testProjectPath,
          analyzedAt: '2024-01-01T00:00:00.000Z',
          totalFiles: 10,
          suggestions: [],
          summary: 'Test summary',
        };

        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockAnalysis));

        const result = await service.getCachedAnalysis(testProjectPath);

        expect(result).toEqual(mockAnalysis);
      });

      it('should return null if cache does not exist', async () => {
        vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));

        const result = await service.getCachedAnalysis(testProjectPath);

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

  // ============================================================================
  // Prompt Management Tests
  // ============================================================================

  describe('Prompt Management', () => {
    describe('getPromptCategories', () => {
      it('should return list of prompt categories', () => {
        const categories = service.getPromptCategories();

        expect(Array.isArray(categories)).toBe(true);
        expect(categories.length).toBeGreaterThan(0);
        expect(categories[0]).toHaveProperty('id');
        expect(categories[0]).toHaveProperty('name');
      });
    });

    describe('getAllPrompts', () => {
      it('should return all guided prompts', () => {
        const prompts = service.getAllPrompts();

        expect(Array.isArray(prompts)).toBe(true);
        expect(prompts.length).toBeGreaterThan(0);
        expect(prompts[0]).toHaveProperty('id');
        expect(prompts[0]).toHaveProperty('category');
        expect(prompts[0]).toHaveProperty('title');
        expect(prompts[0]).toHaveProperty('prompt');
      });
    });

    describe('getPromptsByCategory', () => {
      it('should return prompts filtered by category', () => {
        const allPrompts = service.getAllPrompts();
        const firstCategory = allPrompts[0].category;

        const filtered = service.getPromptsByCategory(firstCategory);

        expect(Array.isArray(filtered)).toBe(true);
        filtered.forEach((prompt) => {
          expect(prompt.category).toBe(firstCategory);
        });
      });

      it('should return empty array for non-existent category', () => {
        const filtered = service.getPromptsByCategory('non-existent-category' as any);

        expect(filtered).toEqual([]);
      });
    });
  });

  // ============================================================================
  // Suggestions Generation Tests
  // ============================================================================

  describe('Suggestion Generation', () => {
    describe('generateSuggestions', () => {
      it('should generate suggestions for a prompt', async () => {
        vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify({}));

        const mockProvider = {
          executeQuery: vi.fn().mockReturnValue({
            async *[Symbol.asyncIterator]() {
              yield {
                type: 'result',
                subtype: 'success',
                result: JSON.stringify([
                  {
                    title: 'Add user authentication',
                    description: 'Implement auth',
                    category: 'security',
                    impact: 'high',
                    effort: 'high',
                  },
                ]),
              };
            },
          }),
        };

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

        const prompts = service.getAllPrompts();
        const firstPrompt = prompts[0];

        const suggestions = await service.generateSuggestions(
          testProjectPath,
          firstPrompt.id,
          'features',
          5
        );

        expect(Array.isArray(suggestions)).toBe(true);
        expect(mockEvents.emit).toHaveBeenCalledWith(
          'ideation:suggestions',
          expect.objectContaining({
            type: 'started',
          })
        );
      });

      it('should throw error for non-existent prompt', async () => {
        await expect(
          service.generateSuggestions(testProjectPath, 'non-existent', 'features', 5)
        ).rejects.toThrow('Prompt non-existent not found');
      });
    });
  });
});
