import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MinioMediaStorageService } from '../services/media-storage.service';

// Use vi.hoisted so variables are available when vi.mock factory runs (hoisted)
const { mockSend, mockGetSignedUrl } = vi.hoisted(() => ({
  mockSend: vi.fn(),
  mockGetSignedUrl: vi.fn(),
}));

vi.mock('@aws-sdk/client-s3', () => {
  return {
    S3Client: class MockS3Client {
      send = mockSend;
      constructor() {}
    },
    PutObjectCommand: class MockPutObjectCommand {
      constructor(public input: unknown) {}
    },
    DeleteObjectCommand: class MockDeleteObjectCommand {
      constructor(public input: unknown) {}
    },
    HeadBucketCommand: class MockHeadBucketCommand {
      constructor(public input: unknown) {}
    },
    CreateBucketCommand: class MockCreateBucketCommand {
      constructor(public input: unknown) {}
    },
  };
});

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

describe('MinioMediaStorageService', () => {
  let service: MinioMediaStorageService;
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    vi.clearAllMocks();

    // Set required env variables
    process.env['MINIO_ACCESS_KEY'] = 'test-access-key';
    process.env['MINIO_SECRET_KEY'] = 'test-secret-key';
    process.env['MINIO_ENDPOINT'] = 'localhost';
    process.env['MINIO_PORT'] = '9000';
    process.env['MINIO_BUCKET'] = 'test-bucket';
    process.env['MINIO_USE_SSL'] = 'false';

    mockLogger = {
      log: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    };

    service = new MinioMediaStorageService(mockLogger as any);
  });

  describe('onModuleInit', () => {
    it('should log when bucket already exists', async () => {
      mockSend.mockResolvedValueOnce({});

      await service.onModuleInit();

      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('test-bucket'),
        'MinioMediaStorageService',
      );
    });

    it('should create bucket when it does not exist (404)', async () => {
      const notFoundError = { $metadata: { httpStatusCode: 404 } };
      mockSend.mockRejectedValueOnce(notFoundError).mockResolvedValueOnce({});

      await service.onModuleInit();

      expect(mockSend).toHaveBeenCalledTimes(2);
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Creating bucket'),
        'MinioMediaStorageService',
      );
    });

    it('should warn when bucket cannot be verified', async () => {
      mockSend.mockRejectedValueOnce({ $metadata: { httpStatusCode: 500 } });

      await service.onModuleInit();

      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('Could not verify bucket'),
        'MinioMediaStorageService',
      );
    });
  });

  describe('upload', () => {
    it('should upload a file and return URL', async () => {
      mockSend.mockResolvedValueOnce({});
      const buffer = Buffer.from('test-image-data');

      const url = await service.upload(buffer, 'photo.jpg', 'image/jpeg', 'listings');

      expect(url).toContain('http://localhost:9000/test-bucket/listings/');
      expect(url).toContain('.jpg');
      expect(mockSend).toHaveBeenCalledTimes(1);
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Media uploaded'),
        'MinioMediaStorageService',
      );
    });

    it('should throw and log error when upload fails', async () => {
      mockSend.mockRejectedValueOnce(new Error('Network error'));
      const buffer = Buffer.from('test-data');

      await expect(service.upload(buffer, 'file.png', 'image/png', 'media')).rejects.toThrow('Network error');
      expect(mockLogger.error).toHaveBeenCalled();
    });
  });

  describe('delete', () => {
    it('should delete a file by URL', async () => {
      mockSend.mockResolvedValueOnce({});

      await service.delete('http://localhost:9000/test-bucket/listings/123-abc.jpg');

      expect(mockSend).toHaveBeenCalledTimes(1);
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Media deleted'),
        'MinioMediaStorageService',
      );
    });

    it('should throw and log error when delete fails', async () => {
      mockSend.mockRejectedValueOnce(new Error('Delete error'));

      await expect(
        service.delete('http://localhost:9000/test-bucket/listings/abc.jpg'),
      ).rejects.toThrow('Delete error');
      expect(mockLogger.error).toHaveBeenCalled();
    });
  });

  describe('getPresignedUploadUrl', () => {
    it('should return a presigned URL', async () => {
      mockGetSignedUrl.mockResolvedValueOnce('https://presigned-url.example.com');

      const url = await service.getPresignedUploadUrl('listings/test.jpg', 'image/jpeg');

      expect(url).toBe('https://presigned-url.example.com');
      expect(mockGetSignedUrl).toHaveBeenCalledTimes(1);
    });
  });
});
