import { BadRequestException, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { AppLoggerService } from '../../infrastructure/logging/app-logger.service';
import { MediaStorageService } from './media-storage.service';

jest.mock('@aws-sdk/s3-request-presigner', () => ({
  getSignedUrl: jest.fn(),
}));

const mockedGetSignedUrl = jest.mocked(getSignedUrl);

describe('MediaStorageService direct uploads', () => {
  const config: Record<string, unknown> = {
    'storage.provider': 's3',
    'storage.mediaAccessMode': 'signed',
    'storage.basePath': 'uploads',
    'storage.publicBaseUrl': 'https://cdn.oudelaa.test',
    'storage.s3.bucket': 'oudelaa-media',
    'storage.s3.region': 'us-east-1',
    'storage.s3.endpoint': 'https://s3.oudelaa.test',
    'storage.s3.accessKeyId': 'test-access-key',
    'storage.s3.secretAccessKey': 'test-secret-key',
    'storage.s3.forcePathStyle': true,
  };
  const configService = {
    get: jest.fn((key: string, defaultValue?: unknown) => config[key] ?? defaultValue),
  } as unknown as ConfigService;
  const appLogger = { error: jest.fn() } as unknown as AppLoggerService;

  beforeEach(() => {
    mockedGetSignedUrl.mockReset();
    mockedGetSignedUrl.mockResolvedValue('https://s3.oudelaa.test/presigned-put');
    config['storage.provider'] = 's3';
  });

  it('creates a user-scoped PUT URL with signed MIME and exact content length', async () => {
    const service = new MediaStorageService(configService, appLogger);

    const result = await service.createPresignedUpload({
      userId: '507f1f77bcf86cd799439011',
      folder: 'posts/video',
      mimeType: 'video/mp4',
      size: 5_242_880,
    });

    expect(result).toEqual(
      expect.objectContaining({
        method: 'PUT',
        uploadUrl: 'https://s3.oudelaa.test/presigned-put',
        mediaUrl: expect.stringMatching(
          /^https:\/\/cdn\.oudelaa\.test\/uploads\/posts\/video\/507f1f77bcf86cd799439011\/media-[0-9a-f-]+\.mp4$/,
        ),
        storageKey: expect.stringMatching(
          /^uploads\/posts\/video\/507f1f77bcf86cd799439011\/media-[0-9a-f-]+\.mp4$/,
        ),
        mimeType: 'video/mp4',
        size: 5_242_880,
        requiredHeaders: { 'Content-Type': 'video/mp4' },
        expiresIn: 900,
        expiresAt: expect.any(String),
      }),
    );
    expect(mockedGetSignedUrl).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        input: expect.objectContaining({
          Bucket: 'oudelaa-media',
          Key: result.storageKey,
          ContentLength: 5_242_880,
          ContentType: 'video/mp4',
          CacheControl: 'public, max-age=31536000, immutable',
          Metadata: {
            owner: '507f1f77bcf86cd799439011',
            'upload-source': 'direct',
          },
        }),
      }),
      {
        expiresIn: 900,
        signableHeaders: new Set(['content-type']),
      },
    );
  });

  it('is disabled for local storage without attempting to sign', async () => {
    config['storage.provider'] = 'local';
    const service = new MediaStorageService(configService, appLogger);

    await expect(
      service.createPresignedUpload({
        userId: 'user-1',
        folder: 'posts/images',
        mimeType: 'image/jpeg',
        size: 1024,
      }),
    ).rejects.toBeInstanceOf(ServiceUnavailableException);
    expect(mockedGetSignedUrl).not.toHaveBeenCalled();
  });

  it('does not rewrite an existing presigned PUT URL as a signed GET URL', async () => {
    const service = new MediaStorageService(configService, appLogger);
    const uploadUrl =
      'https://s3.oudelaa.test/oudelaa-media/uploads/posts/video/file.mp4' +
      '?X-Amz-Algorithm=AWS4-HMAC-SHA256' +
      '&X-Amz-Credential=test' +
      '&X-Amz-Signature=presigned-put';

    await expect(service.resolveResponseUrl(uploadUrl)).resolves.toBe(uploadUrl);
    expect(mockedGetSignedUrl).not.toHaveBeenCalled();
  });

  it.each([
    {
      name: 'a MIME type that does not match the folder',
      input: {
        userId: 'user-1',
        folder: 'users/avatar' as const,
        mimeType: 'video/mp4' as const,
        size: 1024,
      },
    },
    {
      name: 'an oversized image',
      input: {
        userId: 'user-1',
        folder: 'users/avatar' as const,
        mimeType: 'image/jpeg' as const,
        size: 5 * 1024 * 1024 + 1,
      },
    },
    {
      name: 'a zero-byte body',
      input: {
        userId: 'user-1',
        folder: 'posts/audio' as const,
        mimeType: 'audio/mpeg' as const,
        size: 0,
      },
    },
    {
      name: 'an unsafe user segment',
      input: {
        userId: '../another-user',
        folder: 'posts/images' as const,
        mimeType: 'image/jpeg' as const,
        size: 1024,
      },
    },
    {
      name: 'an unsafe folder',
      input: {
        userId: 'user-1',
        folder: '../posts/images' as unknown as 'posts/images',
        mimeType: 'image/jpeg' as const,
        size: 1024,
      },
    },
  ])('rejects $name', async ({ input }) => {
    const service = new MediaStorageService(configService, appLogger);

    await expect(service.createPresignedUpload(input)).rejects.toBeInstanceOf(BadRequestException);
    expect(mockedGetSignedUrl).not.toHaveBeenCalled();
  });
});
