import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { LLMService } from './llm-service';
import type { ChatMessage } from '../lib/db';

// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;

describe('LLM Service', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    mockFetch.mockReset();
  });

  describe('getTeacherResponse - Success Cases', () => {
    it('should return successful response with text', async () => {
      const mockResponse = {
        success: true,
        data: { text: 'That sounds frustrating. What specific error are you seeing?' },
      };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await LLMService.getTeacherResponse("I'm stuck", []);

      expect(result.success).toBe(true);
      expect(result.text).toBe(mockResponse.data.text);
      expect(result.intent).toBe('venting');
    });

    it('should classify venting intent correctly', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: 'Response' },
        }),
      });

      const result = await LLMService.getTeacherResponse("I hate this bug", []);

      expect(result.intent).toBe('venting');
    });

    it('should classify insight intent correctly', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: 'Response' },
        }),
      });

      const result = await LLMService.getTeacherResponse("I finally get it", []);

      expect(result.intent).toBe('insight');
    });

    it('should include chat history in prompt', async () => {
      const chatHistory: ChatMessage[] = [
        { role: 'user', content: 'Having trouble', timestamp: Date.now() },
        { role: 'assistant', content: 'What trouble?', timestamp: Date.now() },
      ];

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: 'Response' },
        }),
      });

      await LLMService.getTeacherResponse("Still stuck", chatHistory);

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('Having trouble');
    });
  });

  describe('getTeacherResponse - Error Handling', () => {
    it('should handle timeout errors', async () => {
      // Mock timeout by rejecting with an error that will trigger timeout
      const abortError = new Error('The operation was aborted');
      (abortError as any).name = 'AbortError';

      mockFetch.mockImplementationOnce(() => {
        return Promise.reject(abortError);
      });

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
      expect(result.error?.message).toBeTruthy();
    });

    it('should handle rate limit errors', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 429,
        json: async () => ({
          error: { code: 'RATE_LIMIT', message: 'Rate limit exceeded' },
        }),
      });

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
      expect(result.error?.message).toBeTruthy();
    });

    it('should handle invalid API key errors', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
        json: async () => ({
          error: { code: 'INVALID_API_KEY', message: 'Invalid API key' },
        }),
      });

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(false);
      expect(result.error?.code).toBe('INVALID_API_KEY');
    });

    it('should handle network errors', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
      expect(result.error?.message).toBeTruthy();
    });

    it('should retry on transient failures', async () => {
      // Fail twice, then succeed
      mockFetch
        .mockRejectedValueOnce(new Error('Temporary failure'))
        .mockRejectedValueOnce(new Error('Temporary failure'))
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            success: true,
            data: { text: 'Success after retries' },
          }),
        });

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(true);
      expect(result.text).toBe('Success after retries');
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('should not retry on invalid API key errors', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
        json: async () => ({
          error: { code: 'INVALID_API_KEY', message: 'Invalid API key' },
        }),
      });

      const result = await LLMService.getTeacherResponse("Test", []);

      expect(result.success).toBe(false);
      expect(mockFetch).toHaveBeenCalledTimes(1); // No retries
    });
  });

  describe('classifyIntent', () => {
    it('should expose intent classification', () => {
      expect(LLMService.classifyIntent("I'm frustrated")).toBe('venting');
      expect(LLMService.classifyIntent("I figured it out")).toBe('insight');
    });
  });

  describe('generatePrompt', () => {
    it('should expose prompt generation', () => {
      const prompt = LLMService.generatePrompt("Test", [], 'venting');

      expect(prompt).toContain('Test');
      expect(prompt).toContain('empath');
    });
  });

  describe('getGhostwriterResponse - Basic Functionality', () => {
    it('should return successful response with structured draft', async () => {
      const mockDraftText = `# My Breakthrough Moment

Today I finally understood closures in JavaScript. It was a struggle but worth it.

**Tags:** javascript, learning, closures`;

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: mockDraftText },
        }),
      });

      const chatHistory: ChatMessage[] = [
        { role: 'user', content: 'Struggling with closures', timestamp: Date.now() },
        { role: 'assistant', content: 'What part is confusing?', timestamp: Date.now() },
        { role: 'user', content: 'How variables are captured', timestamp: Date.now() },
      ];

      const result = await LLMService.getGhostwriterResponse(chatHistory, 'venting');

      expect(result.success).toBe(true);
      expect(result.draft).toBeDefined();
      expect(result.draft?.content).toBe(mockDraftText);
    });

    it('should include chat history in Ghostwriter prompt', async () => {
      const chatHistory: ChatMessage[] = [
        { role: 'user', content: 'Learned React hooks', timestamp: Date.now() },
      ];

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft content' },
        }),
      });

      await LLMService.getGhostwriterResponse(chatHistory, 'insight');

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('React hooks');
      expect(callArgs.prompt).toContain('Ghostwriter');
    });

    it('should include intent context in prompt', async () => {
      const chatHistory: ChatMessage[] = [
        { role: 'user', content: 'Frustrated with debugging', timestamp: Date.now() },
      ];

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft' },
        }),
      });

      await LLMService.getGhostwriterResponse(chatHistory, 'venting');

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('venting');
    });

    it('should handle streaming response for draft generation', async () => {
      const chunks = ['# My', ' Breakthrough', '\n\nToday I learned'];
      let chunkIndex = 0;

      mockFetch.mockImplementationOnce(() => {
        const encoder = new TextEncoder();
        const stream = new ReadableStream({
          async start(controller) {
            for (const chunk of chunks) {
              controller.enqueue(encoder.encode(chunk));
              await new Promise(resolve => setTimeout(resolve, 10));
            }
            controller.close();
          },
        });
        return Promise.resolve({
          ok: true,
          body: stream,
        });
      });

      const collectedTokens: string[] = [];
      const onComplete = vi.fn();

      await LLMService.getGhostwriterResponseStream(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'insight',
        {
          onToken: (token) => collectedTokens.push(token),
          onComplete,
          onError: vi.fn(),
        }
      );

      expect(collectedTokens).toEqual(chunks);
      expect(onComplete).toHaveBeenCalledWith(chunks.join(''));
    });

    it('should handle errors in Ghostwriter streaming', async () => {
      const onError = vi.fn();

      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      await LLMService.getGhostwriterResponseStream(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'venting',
        {
          onToken: vi.fn(),
          onComplete: vi.fn(),
          onError,
        }
      );

      expect(onError).toHaveBeenCalled();
      const errorResponse = onError.mock.calls[0][0];
      expect(errorResponse.success).toBe(false);
      expect(errorResponse.error).toBeDefined();
    });

    it('should add retry logic for failed Ghostwriter generations', async () => {
      // Fail once, then succeed
      mockFetch
        .mockRejectedValueOnce(new Error('Temporary failure'))
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            success: true,
            data: { text: '# Success Draft' },
          }),
        });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'venting'
      );

      expect(result.success).toBe(true);
      expect(result.draft?.content).toBe('# Success Draft');
    });
  });

  describe('getGhostwriterResponse - Edge Cases', () => {
    it('should handle empty chat history gracefully', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Empty History Draft' },
        }),
      });

      const result = await LLMService.getGhostwriterResponse([], 'venting');

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

    it('should handle undefined intent gracefully', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft' },
        }),
      });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        undefined
      );

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

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('unknown');
    });

    it('should handle very long chat history', async () => {
      const longHistory: ChatMessage[] = Array.from({ length: 100 }, (_, i) => ({
        role: i % 2 === 0 ? 'user' : 'assistant',
        content: `Message ${i} with some content`,
        timestamp: Date.now() + i * 1000,
      }));

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft from long history' },
        }),
      });

      const result = await LLMService.getGhostwriterResponse(longHistory, 'venting');

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

    it('should return structured Markdown object', async () => {
      const mockResponse = `# The Moment It Clicked

Learning async/await was challenging at first.

**Tags:** javascript, async, learning`;

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: mockResponse },
        }),
      });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'insight'
      );

      expect(result.success).toBe(true);
      expect(result.draft).toBeDefined();
      expect(result.draft?.content).toContain('The Moment It Clicked');
      expect(result.draft?.content).toContain('**Tags:**');
    });
  });

  describe('getGhostwriterResponse - Error Handling', () => {
    it('should handle timeout errors', async () => {
      const abortError = new Error('The operation was aborted');
      (abortError as any).name = 'AbortError';

      mockFetch.mockImplementationOnce(() => {
        return Promise.reject(abortError);
      });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'venting'
      );

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
    });

    it('should handle rate limit errors', async () => {
      // Mock all retry attempts to return rate limit error
      mockFetch.mockResolvedValue({
        ok: false,
        status: 429,
        json: async () => ({
          error: { code: 'RATE_LIMIT', message: 'Rate limit exceeded' },
        }),
      });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'insight'
      );

      expect(result.success).toBe(false);
      expect(result.error?.code).toBe('RATE_LIMIT');
    });

    it('should handle LLM API failures', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: async () => ({
          error: { code: 'INTERNAL_ERROR', message: 'LLM API failed' },
        }),
      });

      const result = await LLMService.getGhostwriterResponse(
        [{ role: 'user', content: 'Test', timestamp: Date.now() }],
        'venting'
      );

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

  // Story 2.3: Refinement Loop Tests
  describe('regenerateDraft - Story 2.3 Refinement Loop', () => {
    const mockChatHistory: ChatMessage[] = [
      { role: 'user', content: 'I learned async/await', timestamp: Date.now() },
      { role: 'assistant', content: 'Tell me more', timestamp: Date.now() },
    ];

    const originalDraftContent = `# Learning Async/Await

It was hard but I got it.

**Tags:** javascript, learning`;

    it('should regenerate draft with user feedback', async () => {
      const regeneratedDraft = `# Learning Async/Await

After struggling, I finally understood promises.

**Tags:** javascript, promises, breakthrough`;

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: regeneratedDraft },
        }),
      });

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'Make it more specific about what was hard',
        undefined
      );

      expect(result.success).toBe(true);
      expect(result.draft).toBeDefined();
      expect(result.draft?.content).toContain('promises');
    });

    it('should include original draft in refinement prompt', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Refined Draft' },
        }),
      });

      await LLMService.regenerateDraft(
        mockChatHistory,
        'Shorten it',
        'venting'
      );

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('ORIGINAL DRAFT:');
      expect(callArgs.prompt).toContain('USER FEEDBACK:');
      expect(callArgs.prompt).toContain('Shorten it');
    });

    it('should include chat history for context', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft' },
        }),
      });

      await LLMService.regenerateDraft(mockChatHistory, 'Improve it');

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('Original Chat History:');
      expect(callArgs.prompt).toContain('I learned async/await');
    });

    it('should handle vague feedback gracefully', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Improved Draft' },
        }),
      });

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'make it better', // Vague feedback
        undefined
      );

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

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('If feedback is vague');
    });

    it('should maintain user intent context in regeneration', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft' },
        }),
      });

      await LLMService.regenerateDraft(mockChatHistory, 'Fix tone', 'venting');

      const callArgs = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(callArgs.prompt).toContain('User Intent: venting');
    });

    it('should handle contradictory feedback', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: '# Draft' },
        }),
      });

      // Contradictory: make it shorter but add more detail
      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'Make it shorter but add more detail',
        undefined
      );

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

    it('should handle empty feedback', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: originalDraftContent },
        }),
      });

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        '',
        undefined
      );

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

    it('should handle LLM errors during regeneration', async () => {
      mockFetch.mockRejectedValueOnce(new Error('LLM failed'));

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'Improve it',
        undefined
      );

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
    });

    it('should handle timeout during regeneration', async () => {
      const abortError = new Error('The operation was aborted');
      (abortError as any).name = 'AbortError';

      mockFetch.mockImplementationOnce(() => {
        return Promise.reject(abortError);
      });

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'Make it shorter',
        undefined
      );

      expect(result.success).toBe(false);
      expect(result.error?.message).toBeTruthy();
    });

    it('should return structured draft object', async () => {
      const mockResponse = `# My Breakthrough

I finally understood how promises work.

**Tags:** javascript, promises, async`;

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          success: true,
          data: { text: mockResponse },
        }),
      });

      const result = await LLMService.regenerateDraft(
        mockChatHistory,
        'Add more detail',
        'insight'
      );

      expect(result.success).toBe(true);
      expect(result.draft).toBeDefined();
      expect(result.draft?.content).toContain('promises work');
      expect(result.draft?.content).toContain('**Tags:**');
    });
  });

  describe('extractOriginalDraft - Helper for refinement', () => {
    it('should extract draft from chat history when present', () => {
      const chatHistoryWithDraft: ChatMessage[] = [
        { role: 'user', content: 'Create a draft', timestamp: Date.now() },
        { role: 'assistant', content: '# Original Draft\n\nContent here that is long enough to be considered a draft because it has more than two hundred characters which is the threshold for being detected as draft content', timestamp: Date.now() },
        { role: 'user', content: 'Make it better', timestamp: Date.now() },
      ];

      const draft = (LLMService as any).extractOriginalDraft(chatHistoryWithDraft);
      expect(draft).toBeDefined();
      expect(draft).toContain('# Original Draft');
    });

    it('should return fallback when no draft in history', () => {
      const chatHistoryWithoutDraft: ChatMessage[] = [
        { role: 'user', content: 'Hello', timestamp: Date.now() },
        { role: 'assistant', content: 'Hi there!', timestamp: Date.now() },
      ];

      const draft = (LLMService as any).extractOriginalDraft(chatHistoryWithoutDraft);
      expect(draft).toContain('[Content not available]');
    });

    it('should handle empty history gracefully', () => {
      const draft = (LLMService as any).extractOriginalDraft([]);
      expect(draft).toContain('[Content not available]');
    });

    it('should detect draft by markdown header and length', () => {
      const chatHistoryWithShortDraft: ChatMessage[] = [
        { role: 'user', content: 'Create a draft', timestamp: Date.now() },
        // Short message with header but under 200 chars - should return fallback
        { role: 'assistant', content: '# Short\n\nToo short to be a draft', timestamp: Date.now() },
      ];

      const draft = (LLMService as any).extractOriginalDraft(chatHistoryWithShortDraft);
      expect(draft).toContain('[Content not available]');
    });
  });
});
