import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EmbeddingsService } from './embeddings.service';
import { ConversationEmbedding } from '../../../database/entities';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('EmbeddingsService', () => {
  let service: EmbeddingsService;
  let embeddingRepository: Repository<ConversationEmbedding>;

  const mockEmbeddingRepository = {
    create: jest.fn(),
    save: jest.fn(),
    find: jest.fn(),
    findOne: jest.fn(),
    delete: jest.fn(),
    count: jest.fn(),
    query: jest.fn(),
  };

  const mockEmbeddingResponse = {
    data: {
      data: [
        {
          embedding: new Array(1536).fill(0.1), // 1536-dimensional vector
        },
      ],
      usage: {
        total_tokens: 10,
      },
    },
  };

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

    service = module.get<EmbeddingsService>(EmbeddingsService);
    embeddingRepository = module.get<Repository<ConversationEmbedding>>(
      getRepositoryToken(ConversationEmbedding),
    );

    jest.clearAllMocks();
  });

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

  describe('generateEmbedding', () => {
    it('should generate embedding for text', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);

      const result = await service.generateEmbedding('Test text');

      expect(result).toHaveProperty('embedding');
      expect(result).toHaveProperty('tokenCount', 10);
      expect(result).toHaveProperty('model');
      expect(result.embedding).toHaveLength(1536);
      expect(mockedAxios.post).toHaveBeenCalledWith(
        expect.stringContaining('embeddings'),
        expect.objectContaining({ input: 'Test text' }),
        expect.any(Object),
      );
    });

    it('should throw error if embedding dimension is incorrect', async () => {
      const badResponse = {
        data: {
          data: [{ embedding: [0.1, 0.2] }], // Wrong dimension
          usage: { total_tokens: 10 },
        },
      };
      mockedAxios.post.mockResolvedValue(badResponse);

      await expect(service.generateEmbedding('Test text')).rejects.toThrow(
        'Expected 1536 dimensions',
      );
    });

    it('should throw error on API failure', async () => {
      mockedAxios.post.mockRejectedValue(new Error('API error'));

      await expect(service.generateEmbedding('Test text')).rejects.toThrow(
        'Embedding generation failed',
      );
    });
  });

  describe('generateEmbeddingsBatch', () => {
    it('should generate embeddings for multiple texts', async () => {
      const batchResponse = {
        data: {
          data: [
            { embedding: new Array(1536).fill(0.1) },
            { embedding: new Array(1536).fill(0.2) },
          ],
          usage: { total_tokens: 20 },
        },
      };
      mockedAxios.post.mockResolvedValue(batchResponse);

      const result = await service.generateEmbeddingsBatch([
        'Text 1',
        'Text 2',
      ]);

      expect(result).toHaveLength(2);
      expect(result[0].embedding).toHaveLength(1536);
      expect(result[1].embedding).toHaveLength(1536);
    });

    it('should return empty array for empty input', async () => {
      const result = await service.generateEmbeddingsBatch([]);

      expect(result).toEqual([]);
      expect(mockedAxios.post).not.toHaveBeenCalled();
    });

    it('should split large batches into multiple requests', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);

      const texts = new Array(150).fill('test'); // Exceeds BATCH_SIZE of 100
      await service.generateEmbeddingsBatch(texts);

      expect(mockedAxios.post).toHaveBeenCalledTimes(2); // Split into 2 batches
    });

    it('should handle batch generation errors', async () => {
      mockedAxios.post.mockRejectedValue(new Error('Batch error'));

      await expect(
        service.generateEmbeddingsBatch(['Text 1', 'Text 2']),
      ).rejects.toThrow('Batch embedding generation failed');
    });
  });

  describe('storeEmbedding', () => {
    it('should store embedding for a message', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);
      const mockEntity = { id: 'embed_1' };
      mockEmbeddingRepository.create.mockReturnValue(mockEntity as any);
      mockEmbeddingRepository.save.mockResolvedValue(mockEntity as any);

      const result = await service.storeEmbedding(
        'conv_123',
        'user_123',
        0,
        'user' as any,
        'Hello, I need help with feeding',
        ['feeding'],
      );

      expect(result).toEqual(mockEntity);
      expect(mockEmbeddingRepository.create).toHaveBeenCalled();
      expect(mockEmbeddingRepository.save).toHaveBeenCalled();
      expect(mockedAxios.post).toHaveBeenCalled();
    });

    it('should throw error if embedding generation fails', async () => {
      mockedAxios.post.mockRejectedValue(new Error('API error'));

      await expect(
        service.storeEmbedding(
          'conv_123',
          'user_123',
          0,
          'user' as any,
          'Test message',
          ['feeding'],
        ),
      ).rejects.toThrow('Embedding generation failed');
    });
  });

  describe('searchSimilarConversations', () => {
    it('should search for similar conversations', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);
      mockEmbeddingRepository.query.mockResolvedValue([
        {
          conversation_id: 'conv_1',
          message_content: 'Previous question about feeding',
          similarity: 0.85,
          created_at: new Date(),
          topics: ['feeding'],
        },
      ]);

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

      expect(result).toHaveLength(1);
      expect(result[0]).toHaveProperty('conversationId', 'conv_1');
      expect(result[0]).toHaveProperty('similarity', 0.85);
      expect(result[0]).toHaveProperty('topics');
    });

    it('should filter by topic if provided', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);
      mockEmbeddingRepository.query.mockResolvedValue([]);

      await service.searchSimilarConversations(
        'Feeding question',
        'user_123',
        { topicFilter: 'feeding' },
      );

      expect(mockEmbeddingRepository.query).toHaveBeenCalledWith(
        expect.stringContaining('search_conversations_by_topic'),
        expect.arrayContaining([
          expect.any(String),
          'user_123',
          'feeding',
          expect.any(Number),
          expect.any(Number),
        ]),
      );
    });

    it('should use custom similarity threshold', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);
      mockEmbeddingRepository.query.mockResolvedValue([]);

      await service.searchSimilarConversations(
        'Test query',
        'user_123',
        { similarityThreshold: 0.9, limit: 10 },
      );

      expect(mockEmbeddingRepository.query).toHaveBeenCalledWith(
        expect.any(String),
        expect.arrayContaining([expect.any(String), 'user_123', 0.9, 10]),
      );
    });

    it('should handle search errors gracefully', async () => {
      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);
      mockEmbeddingRepository.query.mockRejectedValue(
        new Error('Database error'),
      );

      await expect(
        service.searchSimilarConversations('Test query', 'user_123'),
      ).rejects.toThrow('Similarity search failed');
    });
  });

  describe('getConversationEmbeddings', () => {
    it('should retrieve embeddings for a conversation', async () => {
      const mockEmbeddings = [
        { id: '1', conversationId: 'conv_123', messageIndex: 0 },
        { id: '2', conversationId: 'conv_123', messageIndex: 1 },
      ];
      mockEmbeddingRepository.find.mockResolvedValue(mockEmbeddings as any);

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

      expect(result).toEqual(mockEmbeddings);
      expect(mockEmbeddingRepository.find).toHaveBeenCalledWith({
        where: { conversationId: 'conv_123' },
        order: { messageIndex: 'ASC' },
      });
    });
  });

  describe('deleteConversationEmbeddings', () => {
    it('should delete embeddings for a conversation', async () => {
      mockEmbeddingRepository.delete.mockResolvedValue({ affected: 5 } as any);

      await service.deleteConversationEmbeddings('conv_123');

      expect(mockEmbeddingRepository.delete).toHaveBeenCalledWith({
        conversationId: 'conv_123',
      });
    });
  });

  describe('backfillEmbeddings', () => {
    it('should backfill embeddings for existing conversation', async () => {
      const messages = [
        { index: 0, role: 'user' as any, content: 'Message 1' },
        { index: 1, role: 'assistant' as any, content: 'Message 2' },
      ];

      mockEmbeddingRepository.count.mockResolvedValue(0);
      const batchResponse = {
        data: {
          data: [
            { embedding: new Array(1536).fill(0.1) },
            { embedding: new Array(1536).fill(0.2) },
          ],
          usage: { total_tokens: 20 },
        },
      };
      mockedAxios.post.mockResolvedValue(batchResponse);
      mockEmbeddingRepository.create.mockImplementation((data) => data);
      mockEmbeddingRepository.save.mockResolvedValue([] as any);

      const result = await service.backfillEmbeddings(
        'conv_123',
        'user_123',
        messages,
        ['feeding'],
      );

      expect(result).toBe(2);
      expect(mockEmbeddingRepository.save).toHaveBeenCalled();
    });

    it('should skip if embeddings already exist', async () => {
      mockEmbeddingRepository.count.mockResolvedValue(5);

      const result = await service.backfillEmbeddings(
        'conv_123',
        'user_123',
        [],
        ['feeding'],
      );

      expect(result).toBe(0);
      expect(mockEmbeddingRepository.save).not.toHaveBeenCalled();
    });

    it('should return 0 for empty messages', async () => {
      const result = await service.backfillEmbeddings(
        'conv_123',
        'user_123',
        [],
        ['feeding'],
      );

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

  describe('getUserEmbeddingStats', () => {
    it('should return user embedding statistics', async () => {
      const mockEmbeddings = [
        {
          conversationId: 'conv_1',
          topics: ['feeding', 'sleep'],
        },
        {
          conversationId: 'conv_1',
          topics: ['feeding'],
        },
        {
          conversationId: 'conv_2',
          topics: ['sleep', 'diaper'],
        },
      ];
      mockEmbeddingRepository.find.mockResolvedValue(mockEmbeddings as any);

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

      expect(result.totalEmbeddings).toBe(3);
      expect(result.conversationsWithEmbeddings).toBe(2);
      expect(result.topicsDistribution).toHaveProperty('feeding', 2);
      expect(result.topicsDistribution).toHaveProperty('sleep', 2);
      expect(result.topicsDistribution).toHaveProperty('diaper', 1);
    });

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

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

      expect(result.totalEmbeddings).toBe(0);
      expect(result.conversationsWithEmbeddings).toBe(0);
      expect(result.topicsDistribution).toEqual({});
    });
  });

  describe('healthCheck', () => {
    it('should return ok status when configured correctly', async () => {
      // Set environment variables
      process.env.AZURE_OPENAI_EMBEDDINGS_API_KEY = 'test-key';
      process.env.AZURE_OPENAI_EMBEDDINGS_ENDPOINT = 'https://test.openai.com';

      mockedAxios.post.mockResolvedValue(mockEmbeddingResponse);

      // Create new service instance to pick up env vars
      const testModule = await Test.createTestingModule({
        providers: [
          EmbeddingsService,
          {
            provide: getRepositoryToken(ConversationEmbedding),
            useValue: mockEmbeddingRepository,
          },
        ],
      }).compile();
      const testService = testModule.get<EmbeddingsService>(EmbeddingsService);

      const result = await testService.healthCheck();

      expect(result.status).toBe('ok');
      expect(result.message).toContain('operational');
    });

    it('should return error if credentials not configured', async () => {
      // Clear environment variables
      delete process.env.AZURE_OPENAI_EMBEDDINGS_API_KEY;
      delete process.env.AZURE_OPENAI_EMBEDDINGS_ENDPOINT;

      // Create new service instance
      const testModule = await Test.createTestingModule({
        providers: [
          EmbeddingsService,
          {
            provide: getRepositoryToken(ConversationEmbedding),
            useValue: mockEmbeddingRepository,
          },
        ],
      }).compile();
      const testService = testModule.get<EmbeddingsService>(EmbeddingsService);

      const result = await testService.healthCheck();

      expect(result.status).toBe('error');
      expect(result.message).toContain('not configured');
    });

    it('should return error if health check fails', async () => {
      process.env.AZURE_OPENAI_EMBEDDINGS_API_KEY = 'test-key';
      process.env.AZURE_OPENAI_EMBEDDINGS_ENDPOINT = 'https://test.openai.com';

      mockedAxios.post.mockRejectedValue(new Error('Connection failed'));

      const testModule = await Test.createTestingModule({
        providers: [
          EmbeddingsService,
          {
            provide: getRepositoryToken(ConversationEmbedding),
            useValue: mockEmbeddingRepository,
          },
        ],
      }).compile();
      const testService = testModule.get<EmbeddingsService>(EmbeddingsService);

      const result = await testService.healthCheck();

      expect(result.status).toBe('error');
      expect(result.message).toContain('Health check failed');
    });
  });
});
