import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BadRequestException } from '@nestjs/common';
import {
  VoiceService,
  TranscriptionResult,
  ActivityExtractionResult,
} from './voice.service';
import { VoiceFeedback } from '../../database/entities';
import { SaveVoiceFeedbackDto } from './dto/save-voice-feedback.dto';

describe('VoiceService', () => {
  let service: VoiceService;
  let configService: ConfigService;
  let voiceFeedbackRepository: Repository<VoiceFeedback>;

  const mockConfigService = {
    get: jest.fn((key: string) => {
      const config: Record<string, any> = {
        AZURE_OPENAI_ENABLED: false,
        OPENAI_API_KEY: 'sk-test-key',
      };
      return config[key];
    }),
  };

  const mockVoiceFeedbackRepository = {
    create: jest.fn(),
    save: jest.fn(),
  };

  const mockOpenAI = {
    audio: {
      transcriptions: {
        create: jest.fn().mockResolvedValue({
          text: 'Baby ate 120 milliliters',
          language: 'en',
        }),
      },
    },
    chat: {
      completions: {
        create: jest.fn().mockResolvedValue({
          choices: [
            {
              message: {
                content: JSON.stringify({
                  type: 'feeding',
                  timestamp: null,
                  details: {
                    feedingType: 'bottle',
                    amount: 120,
                    unit: 'ml',
                  },
                  confidence: 0.95,
                  action: 'create_activity',
                }),
              },
            },
          ],
        }),
      },
    },
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        VoiceService,
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
        {
          provide: getRepositoryToken(VoiceFeedback),
          useValue: mockVoiceFeedbackRepository,
        },
      ],
    }).compile();

    service = module.get<VoiceService>(VoiceService);
    configService = module.get<ConfigService>(ConfigService);
    voiceFeedbackRepository = module.get<Repository<VoiceFeedback>>(
      getRepositoryToken(VoiceFeedback),
    );

    // Inject mock OpenAI client
    (service as any).openai = mockOpenAI;
    (service as any).chatOpenAI = mockOpenAI;

    jest.clearAllMocks();
  });

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

  describe('constructor', () => {
    it('should warn if OpenAI not configured', () => {
      const warnMockConfig = {
        get: jest.fn((key: string) => {
          if (key === 'OPENAI_API_KEY') return 'sk-your-openai-api-key-here';
          if (key === 'AZURE_OPENAI_ENABLED') return false;
          return null;
        }),
      };

      const testModule = Test.createTestingModule({
        providers: [
          VoiceService,
          {
            provide: ConfigService,
            useValue: warnMockConfig,
          },
          {
            provide: getRepositoryToken(VoiceFeedback),
            useValue: mockVoiceFeedbackRepository,
          },
        ],
      });

      // Should not throw, just log warning
      expect(testModule).toBeDefined();
    });

    it('should support Azure OpenAI configuration', () => {
      const azureConfig = {
        get: jest.fn((key: string) => {
          const config: Record<string, any> = {
            AZURE_OPENAI_ENABLED: true,
            AZURE_OPENAI_WHISPER_ENDPOINT:
              'https://test.openai.azure.com',
            AZURE_OPENAI_WHISPER_API_KEY: 'test-key',
            AZURE_OPENAI_WHISPER_DEPLOYMENT: 'whisper-1',
            AZURE_OPENAI_WHISPER_API_VERSION: '2024-02-01',
            AZURE_OPENAI_CHAT_ENDPOINT: 'https://test.openai.azure.com',
            AZURE_OPENAI_CHAT_API_KEY: 'test-key',
            AZURE_OPENAI_CHAT_DEPLOYMENT: 'gpt-4o-mini',
            AZURE_OPENAI_CHAT_API_VERSION: '2024-02-01',
          };
          return config[key];
        }),
      };

      const testModule = Test.createTestingModule({
        providers: [
          VoiceService,
          {
            provide: ConfigService,
            useValue: azureConfig,
          },
          {
            provide: getRepositoryToken(VoiceFeedback),
            useValue: mockVoiceFeedbackRepository,
          },
        ],
      });

      expect(testModule).toBeDefined();
    });
  });

  describe('transcribeAudio', () => {
    it('should transcribe audio buffer successfully', async () => {
      const audioBuffer = Buffer.from('fake-audio-data');

      // Mock fs operations
      const fsMock = require('fs');
      jest.spyOn(fsMock, 'existsSync').mockReturnValue(true);
      jest.spyOn(fsMock, 'writeFileSync').mockImplementation();
      jest.spyOn(fsMock, 'createReadStream').mockReturnValue({} as any);
      jest.spyOn(fsMock, 'unlinkSync').mockImplementation();

      const result = await service.transcribeAudio(audioBuffer);

      expect(result).toHaveProperty('text');
      expect(result).toHaveProperty('language');
      expect(result.text).toBe('Baby ate 120 milliliters');
      expect(result.language).toBe('en');
    });

    it('should throw error if service not configured', async () => {
      (service as any).openai = null;

      await expect(
        service.transcribeAudio(Buffer.from('test')),
      ).rejects.toThrow(BadRequestException);
    });

    it('should support language parameter', async () => {
      const audioBuffer = Buffer.from('fake-audio-data');

      const fsMock = require('fs');
      jest.spyOn(fsMock, 'existsSync').mockReturnValue(true);
      jest.spyOn(fsMock, 'writeFileSync').mockImplementation();
      jest.spyOn(fsMock, 'createReadStream').mockReturnValue({} as any);
      jest.spyOn(fsMock, 'unlinkSync').mockImplementation();

      await service.transcribeAudio(audioBuffer, 'es');

      expect(mockOpenAI.audio.transcriptions.create).toHaveBeenCalledWith(
        expect.objectContaining({
          language: 'es',
        }),
      );
    });

    it('should handle transcription failures', async () => {
      mockOpenAI.audio.transcriptions.create.mockRejectedValueOnce(
        new Error('API error'),
      );

      const fsMock = require('fs');
      jest.spyOn(fsMock, 'existsSync').mockReturnValue(true);
      jest.spyOn(fsMock, 'writeFileSync').mockImplementation();
      jest.spyOn(fsMock, 'createReadStream').mockReturnValue({} as any);
      jest.spyOn(fsMock, 'unlinkSync').mockImplementation();

      await expect(
        service.transcribeAudio(Buffer.from('test')),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('extractActivityFromText', () => {
    it('should extract feeding activity from text', async () => {
      const result = await service.extractActivityFromText(
        'Fed baby 120ml of formula',
        'en',
      );

      expect(result.type).toBe('feeding');
      expect(result.details.feedingType).toBe('bottle');
      expect(result.details.amount).toBe(120);
      expect(result.details.unit).toBe('ml');
      expect(result.confidence).toBeGreaterThan(0.9);
    });

    it('should extract sleep activity from text', async () => {
      mockOpenAI.chat.completions.create.mockResolvedValueOnce({
        choices: [
          {
            message: {
              content: JSON.stringify({
                type: 'sleep',
                timestamp: null,
                details: {
                  quality: 'peaceful',
                  duration: 120,
                  location: 'crib',
                },
                confidence: 0.92,
                action: 'create_activity',
              }),
            },
          },
        ],
      });

      const result = await service.extractActivityFromText(
        'Baby slept for 2 hours in the crib',
        'en',
      );

      expect(result.type).toBe('sleep');
      expect(result.details.duration).toBe(120);
    });

    it('should extract diaper activity from text', async () => {
      mockOpenAI.chat.completions.create.mockResolvedValueOnce({
        choices: [
          {
            message: {
              content: JSON.stringify({
                type: 'diaper',
                timestamp: null,
                details: {
                  diaperType: 'dirty',
                  color: 'yellow',
                },
                confidence: 0.88,
                action: 'create_activity',
              }),
            },
          },
        ],
      });

      const result = await service.extractActivityFromText(
        'Changed a dirty diaper',
        'en',
      );

      expect(result.type).toBe('diaper');
      expect(result.details.diaperType).toBe('dirty');
    });

    it('should extract medicine activity from text', async () => {
      mockOpenAI.chat.completions.create.mockResolvedValueOnce({
        choices: [
          {
            message: {
              content: JSON.stringify({
                type: 'medicine',
                timestamp: null,
                details: {
                  medicineName: 'Vitamin D',
                  dosage: 400,
                  unit: 'IU',
                },
                confidence: 0.95,
                action: 'create_activity',
              }),
            },
          },
        ],
      });

      const result = await service.extractActivityFromText(
        'Gave baby 400 IU of Vitamin D drops',
        'en',
      );

      expect(result.type).toBe('medicine');
      expect(result.details.medicineName).toBe('Vitamin D');
    });

    it('should handle unknown activity types', async () => {
      mockOpenAI.chat.completions.create.mockResolvedValueOnce({
        choices: [
          {
            message: {
              content: JSON.stringify({
                type: 'unknown',
                timestamp: null,
                details: {},
                confidence: 0,
                action: 'unknown',
              }),
            },
          },
        ],
      });

      const result = await service.extractActivityFromText(
        'The weather is nice today',
        'en',
      );

      expect(result.type).toBe('unknown');
      expect(result.confidence).toBe(0);
    });

    it('should include child name in prompt if provided', async () => {
      await service.extractActivityFromText(
        'Fed baby',
        'en',
        'Emma',
      );

      expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith(
        expect.objectContaining({
          messages: expect.arrayContaining([
            expect.objectContaining({
              role: 'user',
              content: expect.stringContaining('Emma'),
            }),
          ]),
        }),
      );
    });

    it('should throw error if service not configured', async () => {
      (service as any).chatOpenAI = null;

      await expect(
        service.extractActivityFromText('test', 'en'),
      ).rejects.toThrow(BadRequestException);
    });

    it('should handle extraction failures', async () => {
      mockOpenAI.chat.completions.create.mockRejectedValueOnce(
        new Error('API error'),
      );

      await expect(
        service.extractActivityFromText('test', 'en'),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('processVoiceInput', () => {
    it('should transcribe and extract activity', async () => {
      const fsMock = require('fs');
      jest.spyOn(fsMock, 'existsSync').mockReturnValue(true);
      jest.spyOn(fsMock, 'writeFileSync').mockImplementation();
      jest.spyOn(fsMock, 'createReadStream').mockReturnValue({} as any);
      jest.spyOn(fsMock, 'unlinkSync').mockImplementation();

      const result = await service.processVoiceInput(
        Buffer.from('fake-audio'),
        'en',
        'Baby',
      );

      expect(result).toHaveProperty('transcription');
      expect(result).toHaveProperty('activity');
      expect(result.transcription.text).toBe('Baby ate 120 milliliters');
      expect(result.activity.type).toBe('feeding');
    });

    it('should pass language to transcription', async () => {
      const fsMock = require('fs');
      jest.spyOn(fsMock, 'existsSync').mockReturnValue(true);
      jest.spyOn(fsMock, 'writeFileSync').mockImplementation();
      jest.spyOn(fsMock, 'createReadStream').mockReturnValue({} as any);
      jest.spyOn(fsMock, 'unlinkSync').mockImplementation();

      mockOpenAI.audio.transcriptions.create.mockResolvedValueOnce({
        text: 'El bebé comió',
        language: 'es',
      });

      const result = await service.processVoiceInput(
        Buffer.from('fake-audio'),
        'es',
      );

      expect(result.transcription.language).toBe('es');
    });
  });

  describe('generateClarificationQuestion', () => {
    it('should generate clarification question', async () => {
      mockOpenAI.chat.completions.create.mockResolvedValueOnce({
        choices: [
          {
            message: {
              content: 'How much did the baby eat?',
            },
          },
        ],
      });

      const result = await service.generateClarificationQuestion(
        'Baby ate',
        'feeding',
        'en',
      );

      expect(result).toBe('How much did the baby eat?');
      expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith(
        expect.objectContaining({
          messages: expect.arrayContaining([
            expect.objectContaining({
              role: 'system',
              content: expect.stringContaining('feeding'),
            }),
          ]),
        }),
      );
    });

    it('should return fallback question on error', async () => {
      mockOpenAI.chat.completions.create.mockRejectedValueOnce(
        new Error('API error'),
      );

      const result = await service.generateClarificationQuestion(
        'Baby ate',
        'feeding',
        'en',
      );

      expect(result).toBe('Could you provide more details about this activity?');
    });

    it('should throw error if service not configured', async () => {
      (service as any).chatOpenAI = null;

      await expect(
        service.generateClarificationQuestion('test', 'feeding', 'en'),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('saveFeedback', () => {
    it('should save voice feedback', async () => {
      const feedbackDto: SaveVoiceFeedbackDto = {
        childId: 'child_123',
        activityId: 'activity_123',
        transcript: 'Baby ate 120ml',
        language: 'en',
        extractedType: 'feeding',
        extractedData: { amount: 120, unit: 'ml' },
        confidence: 0.95,
        action: 'accepted',
        finalType: 'feeding',
        finalData: { amount: 120, unit: 'ml' },
        userNotes: 'Worked perfectly',
      };

      const mockFeedback = {
        id: 'vfb_123',
        userId: 'user_123',
        ...feedbackDto,
      };

      mockVoiceFeedbackRepository.create.mockReturnValue(mockFeedback);
      mockVoiceFeedbackRepository.save.mockResolvedValue(mockFeedback);

      const result = await service.saveFeedback('user_123', feedbackDto);

      expect(voiceFeedbackRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          id: expect.stringContaining('vfb_'),
          userId: 'user_123',
          transcript: 'Baby ate 120ml',
        }),
      );
      expect(voiceFeedbackRepository.save).toHaveBeenCalled();
      expect(result).toEqual(mockFeedback);
    });

    it('should handle feedback save failures', async () => {
      mockVoiceFeedbackRepository.save.mockRejectedValueOnce(
        new Error('Database error'),
      );

      const feedbackDto: SaveVoiceFeedbackDto = {
        transcript: 'test',
        language: 'en',
        extractedType: 'feeding',
        extractedData: {},
        confidence: 0.5,
        action: 'rejected',
      };

      await expect(
        service.saveFeedback('user_123', feedbackDto),
      ).rejects.toThrow(BadRequestException);
    });
  });
});
