import { describe, it, expect, vi, type Mock } from 'vitest';
import { Readable } from 'node:stream';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import {
  UploadMediaUseCase,
  type MulterFile,
  type UploadMediaErrorCode,
  type UploadMediaInput,
  type UploadMediaResult,
} from './UploadMediaUseCase';
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
import type { MediaStoragePort } from '../ports/MediaStoragePort';
import { Media } from '../../domain/entities/Media';

vi.mock('uuid', () => ({
  v4: () => 'media-1',
}));

interface TestOutputPort extends UseCaseOutputPort<UploadMediaResult> {
  present: Mock;
  result?: UploadMediaResult;
}

describe('UploadMediaUseCase', () => {
  let mediaRepo: { save: Mock };
  let mediaStorage: { uploadMedia: Mock };
  let logger: Logger;
  let output: TestOutputPort;
  let useCase: UploadMediaUseCase;

  const baseFile: MulterFile = {
    fieldname: 'file',
    originalname: 'avatar.png',
    encoding: '7bit',
    mimetype: 'image/png',
    size: 123,
    buffer: Buffer.from('abc'),
    stream: Readable.from([]),
    destination: '/tmp',
    filename: 'avatar.png',
    path: '/tmp/avatar.png',
  };

  beforeEach(() => {
    mediaRepo = {
      save: vi.fn(),
    };

    mediaStorage = {
      uploadMedia: vi.fn(),
    };

    logger = {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    } as unknown as Logger;

    output = {
      present: vi.fn((result: UploadMediaResult) => {
        output.result = result;
      }),
    } as unknown as TestOutputPort;

    useCase = new UploadMediaUseCase(
      mediaRepo as unknown as IMediaRepository,
      mediaStorage as unknown as MediaStoragePort,
      output,
      logger,
    );
  });

  it('returns UPLOAD_FAILED when storage returns unsuccessful result', async () => {
    mediaStorage.uploadMedia.mockResolvedValue({
      success: false,
      errorMessage: 'Upload error',
    });

    const input: UploadMediaInput = { file: baseFile, uploadedBy: 'user-1' };
    const result: Result<void, ApplicationErrorCode<UploadMediaErrorCode, { message: string }>> =
      await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr();
    expect(err.code).toBe('UPLOAD_FAILED');
    expect(err.details?.message).toBe('Upload error');

    expect(mediaRepo.save).not.toHaveBeenCalled();
    expect(output.present).not.toHaveBeenCalled();
  });

  it('creates media and presents mediaId/url on success (includes metadata)', async () => {
    mediaStorage.uploadMedia.mockResolvedValue({
      success: true,
      url: 'https://example.com/media.png',
      filename: 'stored.png',
    });

    const input: UploadMediaInput = {
      file: baseFile,
      uploadedBy: 'user-1',
      metadata: { foo: 'bar' },
    };

    const result = await useCase.execute(input);

    expect(result).toBeInstanceOf(Result);
    expect(result.isOk()).toBe(true);

    expect(mediaStorage.uploadMedia).toHaveBeenCalledWith(baseFile.buffer, {
      filename: baseFile.originalname,
      mimeType: baseFile.mimetype,
      metadata: { foo: 'bar' },
    });

    expect(mediaRepo.save).toHaveBeenCalledTimes(1);
    const saved = (mediaRepo.save as unknown as Mock).mock.calls[0]![0] as Media;
    expect(saved.id).toBe('media-1');
    expect(saved.url.value).toBe('https://example.com/media.png');
    expect(saved.filename).toBe('stored.png');
    expect(saved.originalName).toBe('avatar.png');
    expect(saved.mimeType).toBe('image/png');
    expect(saved.size).toBe(123);
    expect(saved.type).toBe('image');
    expect(saved.uploadedBy).toBe('user-1');
    expect(saved.metadata).toEqual({ foo: 'bar' });

    expect(output.present).toHaveBeenCalledWith({
      mediaId: 'media-1',
      url: 'https://example.com/media.png',
    });
  });

  it('returns REPOSITORY_ERROR when repository throws', async () => {
    mediaStorage.uploadMedia.mockResolvedValue({
      success: true,
      url: 'https://example.com/media.png',
    });
    mediaRepo.save.mockRejectedValue(new Error('DB error'));

    const input: UploadMediaInput = { file: baseFile, uploadedBy: 'user-1' };
    const result: Result<void, ApplicationErrorCode<UploadMediaErrorCode, { message: string }>> =
      await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr();
    expect(err.code).toBe('REPOSITORY_ERROR');
    expect(err.details?.message).toBe('DB error');

    expect(output.present).not.toHaveBeenCalled();
    expect((logger.error as unknown as Mock)).toHaveBeenCalled();
  });
});