import { BadRequestException, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleAuth } from 'google-auth-library';
import { readFile } from 'fs/promises';
import { AiMusicPromptEnhancerService } from './ai-music-prompt-enhancer.service';
import { MediaService } from './media.service';

jest.mock('fs/promises', () => ({ readFile: jest.fn() }));

type ServiceInternals = {
  resolveSavedAudioDurationSeconds(path: string | null, buffer: Buffer, extension: string, mime: string): Promise<number | null>;
  resolveSavedAudioBuffer(path: string | null, fallback: Buffer): Promise<Buffer>;
  resolveAudioExtension(mime: string): string;
  buildArchiveTitle(prompt: string): string;
  formatAiMusicArchiveItem(item: unknown): Record<string, unknown>;
  resolveGoogleApplicationCredentials(): { credentials?: unknown };
};

const create = (values: Record<string, unknown> = {}, overrides: Record<string, unknown> = {}) => {
  const config = { get: jest.fn((key: string) => values[key]) } as unknown as ConfigService;
  const storage = {
    getHealth: jest.fn().mockResolvedValue({}), saveFile: jest.fn(), resolveLocalFilePath: jest.fn(),
    ...(overrides.storage as object),
  };
  const probe = {
    checkFfmpeg: jest.fn().mockResolvedValue({ path: 'ffmpeg', available: true, version: '7' }),
    checkFfprobe: jest.fn().mockResolvedValue({ path: 'ffprobe', available: true, version: '7' }),
    extractDurationSeconds: jest.fn(), extractDurationSecondsFromBuffer: jest.fn(),
    ...(overrides.probe as object),
  };
  const repository = {
    findAiMusicArchiveItemByUser: jest.fn(), updateAiMusicArchiveTitle: jest.fn(),
    softDeleteAiMusicArchiveItem: jest.fn(), findAiMusicArchiveItemsByUser: jest.fn(),
    countAiMusicArchiveItemsByUser: jest.fn(), createAiMusicArchiveItem: jest.fn(),
    ...(overrides.repository as object),
  };
  const posts = { create: jest.fn(), ...(overrides.posts as object) };
  const service = new MediaService(config, storage as never, probe as never, new AiMusicPromptEnhancerService(), repository as never, posts as never);
  return { service, storage, probe, repository, posts };
};

const doc = (overrides: Record<string, unknown> = {}) => ({
  id: 'archive-1', audioUrl: '/audio.wav', durationSeconds: null, waveformPeaks: [], tag: 'ai',
  toObject: () => ({ _id: { toString: () => 'archive-1' }, type: 'text_to_music', title: 'Song', prompt: '', tag: 'ai', audioUrl: '/audio.wav', ...overrides }),
});

describe('MediaService edge behavior', () => {
  beforeEach(() => jest.clearAllMocks());

  it('reports local storage and processing dependency warnings and a write error', async () => {
    const { service } = create(
      { 'storage.provider': 'local', 'imageProcessing.enabled': true, 'audioProcessing.enabled': true },
      {
        storage: { getHealth: jest.fn().mockResolvedValue({ uploadPathWritable: false, publicPath: '/media' }) },
        probe: {
          checkFfmpeg: jest.fn().mockResolvedValue({ path: 'ffmpeg', available: false, version: '' }),
          checkFfprobe: jest.fn().mockResolvedValue({ path: 'ffprobe', available: false, version: '' }),
        },
      },
    );
    const health = await service.getMediaHealth();
    expect(health.status).toBe('error');
    expect(health.warnings).toEqual(expect.arrayContaining([
      expect.stringContaining('Local storage'), 'PUBLIC_BASE_URL is not configured',
      expect.stringContaining('ffmpeg'), expect.stringContaining('ffprobe'),
    ]));
    expect(health.processing).toEqual(expect.objectContaining({ imageProcessingEnabled: true, audioProcessingEnabled: true, videoHlsGenerationEnabled: true, videoThumbnailGenerationEnabled: true }));
    expect(health.staticServing.uploadsPublicPath).toBe('/media');
  });

  it('reports every S3 configuration and reachability warning', async () => {
    const { service } = create(
      { 'storage.provider': 's3', publicBaseUrl: 'https://api.test' },
      { storage: { getHealth: jest.fn().mockResolvedValue({ isS3Configured: false, storagePublicBaseUrlConfigured: false, s3: { reachable: false } }) } },
    );
    const health = await service.getMediaHealth();
    expect(health.status).toBe('warning');
    expect(health.warnings).toEqual(expect.arrayContaining([
      expect.stringContaining('missing required env'), expect.stringContaining('STORAGE_PUBLIC_BASE_URL'), expect.stringContaining('not reachable'),
    ]));
  });

  it('returns ok for healthy configured S3 delivery', async () => {
    const { service } = create(
      { 'storage.provider': 's3', publicBaseUrl: 'https://api.test', 'videoProcessing.generateHls': false, 'videoProcessing.generateThumbnails': false },
      { storage: { getHealth: jest.fn().mockResolvedValue({ s3Configured: true, storagePublicBaseUrlConfigured: true, s3: { reachable: true } }) } },
    );
    await expect(service.getMediaHealth()).resolves.toEqual(expect.objectContaining({ status: 'ok', warnings: [] }));
  });

  it('rejects disabled and incomplete AI music configuration', async () => {
    await expect(create().service.generateMusicFromText('u', { prompt: 'song' })).rejects.toBeInstanceOf(ServiceUnavailableException);
    await expect(create({ 'aiMusic.enabled': true }).service.generateMusicFromText('u', { prompt: 'song' })).rejects.toThrow('settings are not configured');
  });

  it('handles Google authentication failures and absent tokens', async () => {
    const getClient = jest.spyOn(GoogleAuth.prototype, 'getClient');
    const values = { 'aiMusic.enabled': true, 'aiMusic.projectId': 'p', 'aiMusic.location': 'loc' };
    getClient.mockRejectedValueOnce(new Error('credentials rejected'));
    await expect(create(values).service.generateMusicFromText('u', { prompt: 'song' })).rejects.toThrow('credentials rejected');
    getClient.mockResolvedValueOnce({ getAccessToken: jest.fn().mockResolvedValue(null) } as never);
    await expect(create(values).service.generateMusicFromText('u', { prompt: 'song' })).rejects.toThrow('access token was not returned');
    getClient.mockRestore();
  });

  it('rejects successful provider responses that contain no audio', async () => {
    const originalFetch = global.fetch;
    global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ predictions: [{}] }) }) as never;
    try {
      await expect(create({ 'aiMusic.enabled': true, 'aiMusic.apiKey': 'k', 'aiMusic.projectId': 'p', 'aiMusic.location': 'loc' }).service.generateMusicFromText('u', { prompt: 'song', seed: 3 })).rejects.toThrow('did not return audio');
      const body = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body);
      expect(body.parameters.seed).toBe(3);
    } finally {
      global.fetch = originalFetch;
    }
  });

  it('covers archive not-found branches and successful item formatting', async () => {
    const missing = create({}, { repository: { findAiMusicArchiveItemByUser: jest.fn().mockResolvedValue(null), updateAiMusicArchiveTitle: jest.fn().mockResolvedValue(null), softDeleteAiMusicArchiveItem: jest.fn().mockResolvedValue(false) } });
    await expect(missing.service.getAiMusicArchiveItem('u', 'id')).rejects.toBeInstanceOf(NotFoundException);
    await expect(missing.service.renameAiMusicArchiveItem('u', 'id', { title: 'New' })).rejects.toBeInstanceOf(NotFoundException);
    await expect(missing.service.deleteAiMusicArchiveItem('u', 'id')).rejects.toBeInstanceOf(NotFoundException);
    await expect(missing.service.renameAiMusicArchiveItem('u', 'id', { title: ' ' })).rejects.toBeInstanceOf(BadRequestException);

    const item = doc({ waveformPeaks: 'bad', durationSeconds: undefined });
    const found = create({}, { repository: { findAiMusicArchiveItemByUser: jest.fn().mockResolvedValue(item) } });
    await expect(found.service.getAiMusicArchiveItem('u', 'id')).resolves.toEqual(expect.objectContaining({ id: 'archive-1', waveformPeaks: [], durationSeconds: null }));
  });

  it('uses item title when sharing to feed without content', async () => {
    const item = doc();
    const posts = { create: jest.fn().mockResolvedValue({ id: 'post' }) };
    const { service } = create({}, { repository: { findAiMusicArchiveItemByUser: jest.fn().mockResolvedValue(item) }, posts });
    await service.shareAiMusicArchiveItemToFeed('u', 'id');
    expect(posts.create).toHaveBeenCalledWith('u', expect.objectContaining({ content: 'Song', durationSeconds: undefined }));
  });

  it('falls back across duration, buffer, extension, and title helpers', async () => {
    const { service, probe } = create();
    const target = service as unknown as ServiceInternals;
    probe.extractDurationSeconds.mockResolvedValue(null);
    probe.extractDurationSecondsFromBuffer.mockResolvedValue(7);
    await expect(target.resolveSavedAudioDurationSeconds('/saved', Buffer.from('x'), 'mp3', 'audio/mpeg')).resolves.toBe(7);
    expect(target.resolveAudioExtension('audio/mpeg')).toBe('mp3');
    expect(target.resolveAudioExtension('audio/ogg')).toBe('ogg');
    expect(target.resolveAudioExtension('audio/aac')).toBe('aac');
    expect(target.resolveAudioExtension('audio/wav')).toBe('wav');
    expect(target.resolveAudioExtension('unknown')).toBe('wav');
    await expect(target.resolveSavedAudioBuffer(null, Buffer.from('fallback'))).resolves.toEqual(Buffer.from('fallback'));
    (readFile as jest.Mock).mockResolvedValueOnce(Buffer.from('disk')).mockRejectedValueOnce(new Error('missing'));
    await expect(target.resolveSavedAudioBuffer('/saved', Buffer.from('fallback'))).resolves.toEqual(Buffer.from('disk'));
    await expect(target.resolveSavedAudioBuffer('/saved', Buffer.from('fallback'))).resolves.toEqual(Buffer.from('fallback'));
    expect(target.buildArchiveTitle('   ')).toBe('Untitled AI Music');
    expect(target.buildArchiveTitle('a'.repeat(90))).toBe(`${'a'.repeat(77)}...`);
  });

  it.each([
    ['null', 'not a JSON object'],
    [JSON.stringify({ private_key: 'key' }), 'missing client_email'],
    [JSON.stringify({ client_email: 'a@b' }), 'missing private_key'],
  ])('validates decoded credential object %#', (json, message) => {
    const encoded = Buffer.from(json).toString('base64');
    const target = create({ 'aiMusic.googleApplicationCredentialsJsonBase64': encoded }).service as unknown as ServiceInternals;
    expect(() => target.resolveGoogleApplicationCredentials()).toThrow(message);
  });
});
