import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, LessThan } from 'typeorm';
import { ConversationMemoryService } from './conversation-memory.service';
import {
  AIConversation,
  ConversationMessage,
  MessageRole,
} from '../../../database/entities';
import { EmbeddingsService } from '../embeddings/embeddings.service';

describe('ConversationMemoryService', () => {
  let service: ConversationMemoryService;
  let conversationRepository: Repository<AIConversation>;
  let embeddingsService: EmbeddingsService;

  const mockConversationRepository = {
    findOne: jest.fn(),
    find: jest.fn(),
    save: jest.fn(),
    delete: jest.fn(),
  };

  const mockEmbeddingsService = {
    searchSimilarConversations: jest.fn(),
    storeEmbedding: jest.fn(),
    backfillEmbeddings: jest.fn(),
  };

  const mockMessages: ConversationMessage[] = [
    {
      role: MessageRole.USER,
      content: 'How do I feed my baby?',
      timestamp: new Date('2025-01-01T10:00:00'),
    },
    {
      role: MessageRole.ASSISTANT,
      content: 'Here are some feeding tips...',
      timestamp: new Date('2025-01-01T10:01:00'),
    },
  ];

  const mockConversation: AIConversation = {
    id: 'conv_123',
    userId: 'user_123',
    messages: mockMessages,
    totalTokens: 500,
    createdAt: new Date(),
    updatedAt: new Date(),
    metadata: {},
  } as AIConversation;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ConversationMemoryService,
        {
          provide: getRepositoryToken(AIConversation),
          useValue: mockConversationRepository,
        },
        {
          provide: EmbeddingsService,
          useValue: mockEmbeddingsService,
        },
      ],
    }).compile();

    service = module.get<ConversationMemoryService>(ConversationMemoryService);
    conversationRepository = module.get<Repository<AIConversation>>(
      getRepositoryToken(AIConversation),
    );
    embeddingsService = module.get<EmbeddingsService>(EmbeddingsService);

    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getConversationWithMemory', () => {
    it('should return full conversation if messages are within limit', async () => {
      mockConversationRepository.findOne.mockResolvedValue(mockConversation);

      const result = await service.getConversationWithMemory('conv_123');

      expect(result.conversation).toEqual(mockConversation);
      expect(result.context).toEqual(mockMessages);
      expect(result.summary).toBeUndefined();
    });

    it('should summarize old messages when conversation is long', async () => {
      const longMessages = Array.from({ length: 25 }, (_, i) => ({
        role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
        content: `Message ${i}`,
        timestamp: new Date(),
      }));

      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: longMessages,
      });

      const result = await service.getConversationWithMemory('conv_123');

      expect(result.context.length).toBeLessThanOrEqual(21); // 20 recent + 1 summary
      expect(result.summary).toBeDefined();
      expect(result.summary?.messageCount).toBeGreaterThan(0);
    });

    it('should throw error if conversation not found', async () => {
      mockConversationRepository.findOne.mockResolvedValue(null);

      await expect(
        service.getConversationWithMemory('conv_123'),
      ).rejects.toThrow('Conversation not found');
    });

    it('should include summary message with key topics', async () => {
      const feedingMessages = Array.from({ length: 25 }, (_, i) => ({
        role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
        content: `Feeding question ${i}`,
        timestamp: new Date(),
      }));

      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: feedingMessages,
      });

      const result = await service.getConversationWithMemory('conv_123');

      const summaryMessage = result.context.find(
        (m) => m.role === MessageRole.SYSTEM,
      );
      expect(summaryMessage).toBeDefined();
      expect(summaryMessage?.content).toContain('Key topics');
    });
  });

  describe('cleanupOldConversations', () => {
    it('should delete conversations older than specified days', async () => {
      mockConversationRepository.delete.mockResolvedValue({ affected: 5 });

      const result = await service.cleanupOldConversations(90);

      expect(result).toBe(5);
      expect(mockConversationRepository.delete).toHaveBeenCalledWith({
        updatedAt: expect.any(Object),
      });
    });

    it('should return 0 if no conversations deleted', async () => {
      mockConversationRepository.delete.mockResolvedValue({ affected: 0 });

      const result = await service.cleanupOldConversations(30);

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

    it('should use default retention period of 90 days', async () => {
      mockConversationRepository.delete.mockResolvedValue({ affected: 3 });

      await service.cleanupOldConversations();

      expect(mockConversationRepository.delete).toHaveBeenCalled();
    });
  });

  describe('getConversationStats', () => {
    it('should return conversation statistics', async () => {
      const statsMessages: ConversationMessage[] = [
        {
          role: MessageRole.USER,
          content: 'How do I feed my baby?',
          timestamp: new Date(),
        },
        {
          role: MessageRole.ASSISTANT,
          content: 'Here are some feeding tips...',
          timestamp: new Date(),
        },
        {
          role: MessageRole.USER,
          content: 'What about sleep schedules?',
          timestamp: new Date(),
        },
        {
          role: MessageRole.ASSISTANT,
          content: 'Sleep schedules are important...',
          timestamp: new Date(),
        },
      ];

      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: statsMessages,
        totalTokens: 1000,
      });

      const result = await service.getConversationStats('conv_123');

      expect(result.messageCount).toBe(4);
      expect(result.totalTokens).toBe(1000);
      expect(result.userMessageCount).toBe(2);
      expect(result.assistantMessageCount).toBe(2);
      expect(result.topicsDiscussed).toContain('feeding');
      expect(result.topicsDiscussed).toContain('sleep');
    });

    it('should throw error if conversation not found', async () => {
      mockConversationRepository.findOne.mockResolvedValue(null);

      await expect(service.getConversationStats('conv_123')).rejects.toThrow(
        'Conversation not found',
      );
    });

    it('should handle empty messages', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: [],
      });

      const result = await service.getConversationStats('conv_123');

      expect(result.messageCount).toBe(0);
      expect(result.userMessageCount).toBe(0);
      expect(result.assistantMessageCount).toBe(0);
    });
  });

  describe('pruneConversation', () => {
    it('should keep messages within token budget', () => {
      const messages: ConversationMessage[] = [
        {
          role: MessageRole.SYSTEM,
          content: 'System prompt',
          timestamp: new Date(),
        },
        {
          role: MessageRole.USER,
          content: 'First question',
          timestamp: new Date(),
        },
        {
          role: MessageRole.ASSISTANT,
          content: 'First answer',
          timestamp: new Date(),
        },
      ];

      const result = service.pruneConversation(messages, 1000);

      expect(result.length).toBeGreaterThan(0);
      expect(result.length).toBeLessThanOrEqual(messages.length);
    });

    it('should always keep system messages', () => {
      const messages: ConversationMessage[] = [
        {
          role: MessageRole.SYSTEM,
          content: 'System prompt that should always be kept',
          timestamp: new Date(),
        },
        ...Array.from({ length: 50 }, (_, i) => ({
          role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
          content: `Message ${i} with lots of content to exceed budget`,
          timestamp: new Date(),
        })),
      ];

      const result = service.pruneConversation(messages, 500);

      const systemMessage = result.find((m) => m.role === MessageRole.SYSTEM);
      expect(systemMessage).toBeDefined();
    });

    it('should prioritize recent messages', () => {
      const messages: ConversationMessage[] = Array.from(
        { length: 10 },
        (_, i) => ({
          role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
          content: `Message ${i}`,
          timestamp: new Date(Date.now() + i * 1000),
        }),
      );

      const result = service.pruneConversation(messages, 100);

      expect(result[result.length - 1].content).toContain('9'); // Most recent
    });
  });

  describe('archiveConversation', () => {
    it('should archive conversation with summary', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: mockMessages,
      });
      mockConversationRepository.save.mockResolvedValue(mockConversation);

      await service.archiveConversation('conv_123');

      expect(mockConversationRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          metadata: expect.objectContaining({
            archived: true,
            archivedSummary: expect.any(String),
          }),
        }),
      );
    });

    it('should keep only last 5 messages after archiving', async () => {
      const manyMessages = Array.from({ length: 20 }, (_, i) => ({
        role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
        content: `Message ${i}`,
        timestamp: new Date(),
      }));

      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: manyMessages,
      });
      mockConversationRepository.save.mockResolvedValue(mockConversation);

      await service.archiveConversation('conv_123');

      const savedConv = mockConversationRepository.save.mock.calls[0][0];
      expect(savedConv.messages.length).toBe(5);
    });

    it('should throw error if conversation not found', async () => {
      mockConversationRepository.findOne.mockResolvedValue(null);

      await expect(service.archiveConversation('conv_123')).rejects.toThrow(
        'Conversation not found',
      );
    });
  });

  describe('getUserConversationSummary', () => {
    it('should return user conversation summary', async () => {
      const conversations = [
        {
          userId: 'user_123',
          messages: [
            {
              role: MessageRole.USER,
              content: 'Question about feeding',
              timestamp: new Date(),
            },
            {
              role: MessageRole.ASSISTANT,
              content: 'Answer about feeding',
              timestamp: new Date(),
            },
          ],
          totalTokens: 100,
        },
        {
          userId: 'user_123',
          messages: [
            {
              role: MessageRole.USER,
              content: 'Question about sleep',
              timestamp: new Date(),
            },
          ],
          totalTokens: 50,
        },
      ];

      mockConversationRepository.find.mockResolvedValue(conversations);

      const result = await service.getUserConversationSummary('user_123');

      expect(result.totalConversations).toBe(2);
      expect(result.totalMessages).toBe(3);
      expect(result.totalTokens).toBe(150);
      expect(result.mostDiscussedTopics).toContain('feeding');
      expect(result.mostDiscussedTopics).toContain('sleep');
    });

    it('should handle user with no conversations', async () => {
      mockConversationRepository.find.mockResolvedValue([]);

      const result = await service.getUserConversationSummary('user_123');

      expect(result.totalConversations).toBe(0);
      expect(result.totalMessages).toBe(0);
      expect(result.totalTokens).toBe(0);
      expect(result.mostDiscussedTopics).toEqual([]);
    });

    it('should rank topics by frequency', async () => {
      const conversations = [
        {
          messages: [
            {
              role: MessageRole.USER,
              content: 'feeding feeding feeding',
              timestamp: new Date(),
            },
          ],
          totalTokens: 50,
        },
        {
          messages: [
            {
              role: MessageRole.USER,
              content: 'sleep question',
              timestamp: new Date(),
            },
          ],
          totalTokens: 50,
        },
        {
          messages: [
            {
              role: MessageRole.USER,
              content: 'another feeding question',
              timestamp: new Date(),
            },
          ],
          totalTokens: 50,
        },
      ];

      mockConversationRepository.find.mockResolvedValue(conversations as any);

      const result = await service.getUserConversationSummary('user_123');

      expect(result.mostDiscussedTopics[0]).toBe('feeding'); // Most frequent
    });
  });

  describe('getSemanticContext', () => {
    it('should retrieve semantic context from similar conversations', async () => {
      mockEmbeddingsService.searchSimilarConversations.mockResolvedValue([
        {
          conversationId: 'conv_old',
          messageContent: 'Previous question about feeding',
          similarity: 0.85,
          createdAt: new Date(),
          topics: ['feeding'],
        },
      ]);

      const result = await service.getSemanticContext(
        'user_123',
        'How do I feed my baby?',
      );

      expect(result).toHaveLength(1);
      expect(result[0].role).toBe(MessageRole.SYSTEM);
      expect(result[0].content).toContain('similarity');
      expect(result[0].content).toContain('feeding');
    });

    it('should return empty array if no similar conversations found', async () => {
      mockEmbeddingsService.searchSimilarConversations.mockResolvedValue([]);

      const result = await service.getSemanticContext(
        'user_123',
        'Unique question',
      );

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

    it('should use custom similarity threshold', async () => {
      mockEmbeddingsService.searchSimilarConversations.mockResolvedValue([]);

      await service.getSemanticContext('user_123', 'Test query', {
        similarityThreshold: 0.9,
        maxResults: 3,
      });

      expect(
        mockEmbeddingsService.searchSimilarConversations,
      ).toHaveBeenCalledWith('Test query', 'user_123', {
        similarityThreshold: 0.9,
        limit: 3,
        topicFilter: undefined,
      });
    });

    it('should handle embeddings service errors gracefully', async () => {
      mockEmbeddingsService.searchSimilarConversations.mockRejectedValue(
        new Error('Embeddings error'),
      );

      const result = await service.getSemanticContext(
        'user_123',
        'Test query',
      );

      expect(result).toEqual([]); // Should not throw, just return empty
    });
  });

  describe('storeMessageEmbedding', () => {
    it('should store embedding for a message', async () => {
      mockEmbeddingsService.storeEmbedding.mockResolvedValue({
        id: 'embed_1',
      });

      await service.storeMessageEmbedding(
        'conv_123',
        'user_123',
        0,
        MessageRole.USER,
        'How do I feed my baby?',
      );

      expect(mockEmbeddingsService.storeEmbedding).toHaveBeenCalledWith(
        'conv_123',
        'user_123',
        0,
        MessageRole.USER,
        'How do I feed my baby?',
        expect.arrayContaining(['feeding']),
      );
    });

    it('should not throw on embedding storage error', async () => {
      mockEmbeddingsService.storeEmbedding.mockRejectedValue(
        new Error('Storage error'),
      );

      await expect(
        service.storeMessageEmbedding(
          'conv_123',
          'user_123',
          0,
          MessageRole.USER,
          'Test message',
        ),
      ).resolves.not.toThrow();
    });
  });

  describe('backfillConversationEmbeddings', () => {
    it('should backfill embeddings for existing conversation', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: mockMessages,
      });
      mockEmbeddingsService.backfillEmbeddings.mockResolvedValue(2);

      const result = await service.backfillConversationEmbeddings('conv_123');

      expect(result).toBe(2);
      expect(mockEmbeddingsService.backfillEmbeddings).toHaveBeenCalled();
    });

    it('should throw error if conversation not found', async () => {
      mockConversationRepository.findOne.mockResolvedValue(null);

      await expect(
        service.backfillConversationEmbeddings('conv_123'),
      ).rejects.toThrow('Conversation not found');
    });

    it('should return 0 for conversation with no messages', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: [],
      });

      const result = await service.backfillConversationEmbeddings('conv_123');

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

  describe('getConversationWithSemanticMemory', () => {
    it('should combine traditional and semantic memory', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: mockMessages,
      });
      mockEmbeddingsService.searchSimilarConversations.mockResolvedValue([
        {
          conversationId: 'conv_old',
          messageContent: 'Similar past conversation',
          similarity: 0.88,
          createdAt: new Date(),
          topics: ['feeding'],
        },
      ]);

      const result = await service.getConversationWithSemanticMemory(
        'conv_123',
        'How do I feed my baby?',
      );

      expect(result.context.length).toBeGreaterThan(mockMessages.length);
      expect(result.semanticContext).toBeDefined();
    });

    it('should return traditional memory only if no query provided', async () => {
      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: mockMessages,
      });

      const result =
        await service.getConversationWithSemanticMemory('conv_123');

      expect(result.semanticContext).toBeUndefined();
      expect(mockEmbeddingsService.searchSimilarConversations).not.toHaveBeenCalled();
    });

    it('should prune combined context to fit token budget', async () => {
      const longMessages = Array.from({ length: 30 }, (_, i) => ({
        role: i % 2 === 0 ? MessageRole.USER : MessageRole.ASSISTANT,
        content: `Long message ${i} with lots of content to exceed budget`,
        timestamp: new Date(),
      }));

      mockConversationRepository.findOne.mockResolvedValue({
        ...mockConversation,
        messages: longMessages,
      });
      mockEmbeddingsService.searchSimilarConversations.mockResolvedValue([
        {
          conversationId: 'conv_old',
          messageContent: 'Similar conversation',
          similarity: 0.9,
          createdAt: new Date(),
          topics: ['feeding'],
        },
      ]);

      const result = await service.getConversationWithSemanticMemory(
        'conv_123',
        'Test query',
      );

      // Context should be pruned to fit budget
      expect(result.context.length).toBeLessThan(longMessages.length);
    });
  });
});
