import { BadRequestException, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { AiMusicPromptEnhancerService } from './ai-music-prompt-enhancer.service';
import { MediaService } from './media.service';

const createPcmWavBuffer = (frames = 8000) => {
  const sampleRate = 8000;
  const channels = 1;
  const bitsPerSample = 16;
  const bytesPerSample = bitsPerSample / 8;
  const dataSize = frames * channels * bytesPerSample;
  const buffer = Buffer.alloc(44 + dataSize);

  buffer.write('RIFF', 0, 'ascii');
  buffer.writeUInt32LE(36 + dataSize, 4);
  buffer.write('WAVE', 8, 'ascii');
  buffer.write('fmt ', 12, 'ascii');
  buffer.writeUInt32LE(16, 16);
  buffer.writeUInt16LE(1, 20);
  buffer.writeUInt16LE(channels, 22);
  buffer.writeUInt32LE(sampleRate, 24);
  buffer.writeUInt32LE(sampleRate * channels * bytesPerSample, 28);
  buffer.writeUInt16LE(channels * bytesPerSample, 32);
  buffer.writeUInt16LE(bitsPerSample, 34);
  buffer.write('data', 36, 'ascii');
  buffer.writeUInt32LE(dataSize, 40);

  for (let frame = 0; frame < frames; frame += 1) {
    const amplitude = frame < frames / 2 ? 0.2 : 0.9;
    const tone = Math.sin((frame / sampleRate) * Math.PI * 2 * 440);
    buffer.writeInt16LE(Math.round(tone * amplitude * 32767), 44 + frame * bytesPerSample);
  }

  return buffer;
};

describe('MediaService', () => {
  const createService = (
    googleCredentialsBase64 = '',
    overrides: {
      config?: Record<string, unknown>;
      storageService?: Record<string, unknown>;
      mediaProbeService?: Record<string, unknown>;
      mediaRepository?: Record<string, jest.Mock>;
      postsService?: Record<string, jest.Mock>;
    } = {},
  ) => {
    const configService = {
      get: jest.fn((key: string) => {
        if (key === 'aiMusic.googleApplicationCredentialsJsonBase64') {
          return googleCredentialsBase64;
        }
        if (key in (overrides.config ?? {})) {
          return overrides.config?.[key];
        }
        return undefined;
      }),
    };

    const mediaRepository = {
      createAiMusicArchiveItem: jest.fn().mockResolvedValue(
        createArchiveDoc({
          id: 'archive-1',
          title: 'test prompt',
          prompt: 'test prompt',
          audioUrl: '/uploads/ai-music/generated.wav',
        }),
      ),
      findAiMusicArchiveItemsByUser: jest.fn(),
      countAiMusicArchiveItemsByUser: jest.fn(),
      findAiMusicArchiveItemByUser: jest.fn(),
      updateAiMusicArchiveTitle: jest.fn(),
      softDeleteAiMusicArchiveItem: jest.fn(),
      ...(overrides.mediaRepository ?? {}),
    };
    const postsService = {
      create: jest.fn(),
      ...(overrides.postsService ?? {}),
    };
    const service = new MediaService(
      configService as any,
      (overrides.storageService ?? {}) as any,
      (overrides.mediaProbeService ?? {}) as any,
      new AiMusicPromptEnhancerService(),
      mediaRepository as any,
      postsService as any,
    ) as any;

    service.__mocks = { mediaRepository, postsService };
    return service;
  };

  it('falls back to default Google application credentials when base64 JSON is missing', () => {
    const service = createService();

    expect(service.resolveGoogleApplicationCredentials()).toEqual({});
  });

  it('decodes service account credentials from base64 JSON', () => {
    const credentials = {
      type: 'service_account',
      client_email: 'ai-music@example.iam.gserviceaccount.com',
      private_key: '-----BEGIN PRIVATE KEY-----\nredacted\n-----END PRIVATE KEY-----\n',
    };
    const encoded = Buffer.from(JSON.stringify(credentials), 'utf8').toString('base64');
    const service = createService(encoded);

    expect(service.resolveGoogleApplicationCredentials()).toEqual({ credentials });
  });

  it('throws a safe configuration error for invalid base64 credentials', () => {
    const service = createService(Buffer.from('not-json', 'utf8').toString('base64'));

    expect(() => service.resolveGoogleApplicationCredentials()).toThrow(
      ServiceUnavailableException,
    );
    expect(() => service.resolveGoogleApplicationCredentials()).toThrow(
      'Invalid GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64',
    );
  });

  it('returns the probed audio duration instead of the requested duration', async () => {
    const audioBuffer = createPcmWavBuffer();
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        predictions: [
          {
            bytesBase64Encoded: audioBuffer.toString('base64'),
            mimeType: 'audio/wav',
          },
        ],
      }),
    }) as any;
    const storageService = {
      saveFile: jest.fn().mockResolvedValue('/uploads/ai-music/generated.wav'),
      resolveLocalFilePath: jest.fn().mockReturnValue(null),
    };
    const mediaProbeService = {
      extractDurationSeconds: jest.fn(),
      extractDurationSecondsFromBuffer: jest.fn().mockResolvedValue(32),
    };
    const service = createService('', {
      config: {
        'aiMusic.enabled': true,
        'aiMusic.apiKey': 'test-api-key',
        'aiMusic.projectId': 'accordev',
        'aiMusic.location': 'us-central1',
        'aiMusic.model': 'lyria-002',
      },
      storageService,
      mediaProbeService,
    });

    try {
      const result = await service.generateMusicFromText('user-1', {
        prompt: 'test prompt',
        durationSeconds: 12,
      });

      expect(result.durationSeconds).toBe(32);
      expect(result.prompt).toBe('test prompt');
      expect(result.originalPrompt).toBe('test prompt');
      expect(result.enhancedPrompt).toContain('Instrumental only, no vocals');
      expect(result.detectedMusicIntent).toMatchObject({
        language: 'en',
        vocals: false,
      });
      expect(result.audioUrl).toBe('/uploads/ai-music/generated.wav');
      expect(result.archiveItemId).toBe('archive-1');
      expect(result.mimeType).toBe('audio/wav');
      expect(result.sizeBytes).toBe(audioBuffer.length);
      expect(result.waveformPeaks).toHaveLength(160);
      expect(result.waveformPeaksPreview).toHaveLength(160);
      expect(result.waveformPeaks).toEqual(result.waveformPeaksPreview);
      expect(result.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(500);
      expect(result.waveformPeaksDetailed.length).toBeLessThanOrEqual(1000);
      expect(result.waveformPeaksDetailed.every((value: number) => Number.isInteger(value))).toBe(true);
      expect(result.waveformPeaksDetailed.every((value: number) => value >= 0 && value <= 100)).toBe(true);
      expect(new Set(result.waveformPeaksDetailed).size).toBeGreaterThan(1);
      expect(storageService.resolveLocalFilePath).toHaveBeenCalledWith(
        '/uploads/ai-music/generated.wav',
      );
      expect(mediaProbeService.extractDurationSeconds).not.toHaveBeenCalled();
      expect(mediaProbeService.extractDurationSecondsFromBuffer).toHaveBeenCalledWith(
        audioBuffer,
        {
          originalname: 'ai-music.wav',
          mimetype: 'audio/wav',
        },
      );
    } finally {
      global.fetch = originalFetch;
    }
  });

  it('saves an archive item after successful text-to-music generation', async () => {
    const audioBuffer = createPcmWavBuffer();
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        predictions: [
          {
            bytesBase64Encoded: audioBuffer.toString('base64'),
            mimeType: 'audio/wav',
          },
        ],
      }),
    }) as any;
    const service = createService('', {
      config: {
        'aiMusic.enabled': true,
        'aiMusic.apiKey': 'test-api-key',
        'aiMusic.projectId': 'accordev',
        'aiMusic.location': 'us-central1',
        'aiMusic.model': 'lyria-002',
      },
      storageService: {
        saveFile: jest.fn().mockResolvedValue('/uploads/ai-music/generated.wav'),
        resolveLocalFilePath: jest.fn().mockReturnValue(null),
      },
      mediaProbeService: {
        extractDurationSeconds: jest.fn(),
        extractDurationSecondsFromBuffer: jest.fn().mockResolvedValue(12),
      },
    });

    try {
      const result = await service.generateMusicFromText('507f1f77bcf86cd799439011', {
        prompt: 'Calm oud melody',
        durationSeconds: 12,
      });

      expect(service.__mocks.mediaRepository.createAiMusicArchiveItem).toHaveBeenCalledWith(
        '507f1f77bcf86cd799439011',
        expect.objectContaining({
          type: 'text_to_music',
          title: 'Calm oud melody',
          prompt: 'Calm oud melody',
          tag: 'text-to-music',
          audioUrl: '/uploads/ai-music/generated.wav',
          storageKey: '/uploads/ai-music/generated.wav',
          durationSeconds: 12,
          mimeType: 'audio/wav',
          sizeBytes: audioBuffer.length,
        }),
      );
      expect(result.archiveItemId).toBe('archive-1');
      expect(result.archiveItem).toMatchObject({
        id: 'archive-1',
        audioUrl: '/uploads/ai-music/generated.wav',
      });
    } finally {
      global.fetch = originalFetch;
    }
  });

  it('does not save an archive item when generation fails', async () => {
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({
      ok: false,
      json: jest.fn().mockResolvedValue({ error: { message: 'Lyria failed' } }),
    }) as any;
    const service = createService('', {
      config: {
        'aiMusic.enabled': true,
        'aiMusic.apiKey': 'test-api-key',
        'aiMusic.projectId': 'accordev',
        'aiMusic.location': 'us-central1',
      },
    });

    try {
      await expect(
        service.generateMusicFromText('507f1f77bcf86cd799439011', {
          prompt: 'Calm oud melody',
          durationSeconds: 12,
        }),
      ).rejects.toThrow('Lyria failed');
      expect(service.__mocks.mediaRepository.createAiMusicArchiveItem).not.toHaveBeenCalled();
    } finally {
      global.fetch = originalFetch;
    }
  });

  it('returns only current user archive items with pagination', async () => {
    const item = createArchiveDoc({ id: 'archive-1', title: 'Mine' });
    const service = createService('', {
      mediaRepository: {
        findAiMusicArchiveItemsByUser: jest.fn().mockResolvedValue([item]),
        countAiMusicArchiveItemsByUser: jest.fn().mockResolvedValue(3),
      },
    });

    const result = await service.getMyAiMusicArchive('507f1f77bcf86cd799439011', {
      page: 1,
      limit: 1,
    });

    expect(service.__mocks.mediaRepository.findAiMusicArchiveItemsByUser).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      0,
      1,
    );
    expect(result.items).toHaveLength(1);
    expect(result.pagination).toEqual({ page: 1, limit: 1, hasMore: true });
  });

  it('prevents access to another user archive item by returning 404', async () => {
    const service = createService('', {
      mediaRepository: {
        findAiMusicArchiveItemByUser: jest.fn().mockResolvedValue(null),
      },
    });

    await expect(
      service.getAiMusicArchiveItem('507f1f77bcf86cd799439011', '507f191e810c19729de860ea'),
    ).rejects.toBeInstanceOf(NotFoundException);
  });

  it('renames an archive item title', async () => {
    const service = createService('', {
      mediaRepository: {
        updateAiMusicArchiveTitle: jest
          .fn()
          .mockResolvedValue(createArchiveDoc({ id: 'archive-1', title: 'New title' })),
      },
    });

    const result = await service.renameAiMusicArchiveItem(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
      { title: ' New title ' },
    );

    expect(service.__mocks.mediaRepository.updateAiMusicArchiveTitle).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
      'New title',
    );
    expect(result.title).toBe('New title');
  });

  it('rejects an empty archive title', async () => {
    const service = createService();

    await expect(
      service.renameAiMusicArchiveItem(
        '507f1f77bcf86cd799439011',
        '507f191e810c19729de860ea',
        { title: '   ' },
      ),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('soft deletes an archive item and hides deleted items through repository filters', async () => {
    const service = createService('', {
      mediaRepository: {
        softDeleteAiMusicArchiveItem: jest.fn().mockResolvedValue(true),
        findAiMusicArchiveItemsByUser: jest.fn().mockResolvedValue([]),
        countAiMusicArchiveItemsByUser: jest.fn().mockResolvedValue(0),
      },
    });

    await expect(
      service.deleteAiMusicArchiveItem(
        '507f1f77bcf86cd799439011',
        '507f191e810c19729de860ea',
      ),
    ).resolves.toEqual({ message: 'Archive item deleted' });

    const list = await service.getMyAiMusicArchive('507f1f77bcf86cd799439011', {});
    expect(list.items).toEqual([]);
  });

  it('returns a simple share payload', async () => {
    const service = createService('', {
      config: { shareBaseUrl: 'https://oudelaa.com/' },
      mediaRepository: {
        findAiMusicArchiveItemByUser: jest
          .fn()
          .mockResolvedValue(createArchiveDoc({ id: 'archive-1', title: 'Share me' })),
      },
    });

    const result = await service.shareAiMusicArchiveItem(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
    );

    expect(result).toMatchObject({
      shareUrl: 'https://oudelaa.com/ai/music/archive-1',
      title: 'Share me',
      audioUrl: '/uploads/ai-music/generated.wav',
    });
    expect(result.shareUrl).not.toContain('hosted.ghaymah.systems');
  });

  it('shares an archive item to the feed as an audio post', async () => {
    const post = { id: 'post-1', audioUrl: '/uploads/ai-music/generated.wav' };
    const service = createService('', {
      mediaRepository: {
        findAiMusicArchiveItemByUser: jest
          .fn()
          .mockResolvedValue(createArchiveDoc({ id: 'archive-1', title: 'Feed song' })),
      },
      postsService: {
        create: jest.fn().mockResolvedValue(post),
      },
    });

    const result = await service.shareAiMusicArchiveItemToFeed(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
      { content: 'Listen to this' },
    );

    expect(service.__mocks.postsService.create).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      expect.objectContaining({
        content: 'Listen to this',
        audioUrl: '/uploads/ai-music/generated.wav',
        durationSeconds: 12,
        waveformPeaks: [20, 44, 70],
        style: 'text-to-music',
      }),
    );
    expect(result).toBe(post);
  });

  it('sends the enhanced Arabic prompt to Lyria while preserving the original prompt', async () => {
    const audioBuffer = createPcmWavBuffer();
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        predictions: [
          {
            bytesBase64Encoded: audioBuffer.toString('base64'),
            mimeType: 'audio/wav',
          },
        ],
      }),
    }) as any;
    const service = createService('', {
      config: {
        'aiMusic.enabled': true,
        'aiMusic.apiKey': 'test-api-key',
        'aiMusic.projectId': 'accordev',
        'aiMusic.location': 'us-central1',
        'aiMusic.model': 'lyria-002',
      },
      storageService: {
        saveFile: jest.fn().mockResolvedValue('/uploads/ai-music/generated.wav'),
        resolveLocalFilePath: jest.fn().mockReturnValue(null),
      },
      mediaProbeService: {
        extractDurationSeconds: jest.fn(),
        extractDurationSecondsFromBuffer: jest.fn().mockResolvedValue(12),
      },
    });

    try {
      const prompt = 'بدي لحن عود حزين مقام حجاز مع إيقاع خفيف';
      const result = await service.generateMusicFromText('user-1', {
        prompt,
        durationSeconds: 12,
      });
      const fetchBody = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body);
      const sentPrompt = fetchBody.instances[0].prompt as string;

      expect(result.prompt).toBe(prompt);
      expect(result.originalPrompt).toBe(prompt);
      expect(result.enhancedPrompt).toBe(sentPrompt);
      expect(result.detectedMusicIntent).toMatchObject({
        language: 'ar',
        instrument: 'oud',
        maqam: 'hijaz',
        mood: 'sad',
        percussion: 'light',
      });
      expect(sentPrompt).toContain('Instrumental Arabic oud melody');
      expect(sentPrompt).toContain('Hijaz maqam, emotional Middle Eastern Arabic sound');
      expect(sentPrompt).toContain('sad, emotional, expressive');
      expect(sentPrompt).toContain('Light subtle percussion');
      expect(sentPrompt).toContain('slow tempo');
      expect(sentPrompt).toContain('Instrumental only, no vocals, no lyrics');
      expect(sentPrompt).toContain('No artist imitation, no copyrighted melody imitation');
    } finally {
      global.fetch = originalFetch;
    }
  });

  it('probes duration from the saved local audio file when local storage path is available', async () => {
    const audioBuffer = createPcmWavBuffer();
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        predictions: [
          {
            bytesBase64Encoded: audioBuffer.toString('base64'),
            mimeType: 'audio/wav',
          },
        ],
      }),
    }) as any;
    const storageService = {
      saveFile: jest.fn().mockResolvedValue('/uploads/ai-music/generated.wav'),
      resolveLocalFilePath: jest
        .fn()
        .mockReturnValue('C:\\app\\uploads\\ai-music\\generated.wav'),
    };
    const mediaProbeService = {
      extractDurationSeconds: jest.fn().mockResolvedValue(32),
      extractDurationSecondsFromBuffer: jest.fn().mockResolvedValue(12),
    };
    const service = createService('', {
      config: {
        'aiMusic.enabled': true,
        'aiMusic.apiKey': 'test-api-key',
        'aiMusic.projectId': 'accordev',
        'aiMusic.location': 'us-central1',
        'aiMusic.model': 'lyria-002',
      },
      storageService,
      mediaProbeService,
    });

    try {
      const result = await service.generateMusicFromText('user-1', {
        prompt: 'test prompt',
        durationSeconds: 12,
      });

      expect(result.durationSeconds).toBe(32);
      expect(storageService.resolveLocalFilePath).toHaveBeenCalledWith(
        '/uploads/ai-music/generated.wav',
      );
      expect(mediaProbeService.extractDurationSeconds).toHaveBeenCalledWith(
        'C:\\app\\uploads\\ai-music\\generated.wav',
      );
      expect(mediaProbeService.extractDurationSecondsFromBuffer).not.toHaveBeenCalled();
    } finally {
      global.fetch = originalFetch;
    }
  });
});

const createArchiveDoc = (
  overrides: Partial<Record<string, unknown>> = {},
): {
  id: string;
  audioUrl: string;
  tag: string;
  durationSeconds: number;
  waveformPeaks: number[];
  toObject: () => Record<string, unknown>;
} => {
  const value = {
    _id: overrides.id ?? 'archive-1',
    id: overrides.id ?? 'archive-1',
    type: 'text_to_music',
    title: overrides.title ?? 'Untitled AI Music',
    prompt: overrides.prompt ?? 'prompt',
    tag: overrides.tag ?? 'text-to-music',
    audioUrl: overrides.audioUrl ?? '/uploads/ai-music/generated.wav',
    storageKey: overrides.storageKey ?? '/uploads/ai-music/generated.wav',
    durationSeconds: overrides.durationSeconds ?? 12,
    mimeType: overrides.mimeType ?? 'audio/wav',
    sizeBytes: overrides.sizeBytes ?? 1234,
    waveformPeaks: overrides.waveformPeaks ?? [20, 44, 70],
    createdAt: overrides.createdAt ?? new Date('2026-06-30T10:00:00.000Z'),
    updatedAt: overrides.updatedAt ?? new Date('2026-06-30T10:00:00.000Z'),
  };

  return {
    id: String(value.id),
    audioUrl: String(value.audioUrl),
    tag: String(value.tag),
    durationSeconds: Number(value.durationSeconds),
    waveformPeaks: value.waveformPeaks as number[],
    toObject: () => value,
  };
};
