import { type IMediaStorageService } from '../../../../listings/infrastructure/services/media-storage.service';
import { UserEntity } from '../../domain/entities/user.entity';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { type HashedPassword } from '../../domain/value-objects/hashed-password.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
import { GenerateKycUploadUrlsCommand } from '../commands/generate-kyc-upload-urls/generate-kyc-upload-urls.command';
import { GenerateKycUploadUrlsHandler } from '../commands/generate-kyc-upload-urls/generate-kyc-upload-urls.handler';

function createTestUser(overrides: Partial<{ kycStatus: string }> = {}): UserEntity {
  const phone = Phone.create('0912345678').unwrap();
  const pw = { value: 'hashed' } as HashedPassword;
  return new UserEntity('user-1', {
    email: null,
    phone,
    passwordHash: pw,
    fullName: 'Nguyen Van A',
    avatarUrl: null,
    role: 'BUYER',
    kycStatus: overrides.kycStatus ?? 'NONE',
    kycData: null,
    isActive: true,
  });
}

describe('GenerateKycUploadUrlsHandler', () => {
  let handler: GenerateKycUploadUrlsHandler;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockMediaStorage: { [K in keyof IMediaStorageService]: ReturnType<typeof vi.fn> };
  let mockLogger: { error: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; log: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockUserRepo = {
      findById: vi.fn(),
      findByPhone: vi.fn(),
      findByEmail: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
    };
    mockMediaStorage = {
      upload: vi.fn(),
      delete: vi.fn(),
      getPresignedUploadUrl: vi.fn(),
      generatePresignedUpload: vi.fn(),
      getPublicUrl: vi.fn(),
      isTrustedUrl: vi.fn().mockReturnValue(true),
    };
    mockLogger = {
      error: vi.fn(),
      warn: vi.fn(),
      log: vi.fn(),
    };

    handler = new GenerateKycUploadUrlsHandler(
      mockUserRepo as any,
      mockMediaStorage as any,
      mockLogger as any,
    );
  });

  it('generates presigned URLs for valid files', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);
    mockMediaStorage.generatePresignedUpload.mockResolvedValue({
      uploadUrl: 'https://minio/upload',
      publicUrl: 'https://minio/public',
      objectKey: 'kyc/user-1/front.jpg',
    });

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    const result = await handler.execute(command);

    expect(result).toHaveLength(1);
    expect(result[0]).toEqual({
      field: 'frontImage',
      uploadUrl: 'https://minio/upload',
      publicUrl: 'https://minio/public',
      objectKey: 'kyc/user-1/front.jpg',
    });
    expect(mockMediaStorage.generatePresignedUpload).toHaveBeenCalledWith(
      'kyc/user-1',
      'front.jpg',
      'image/jpeg',
      300,
    );
  });

  it('generates presigned URLs for multiple files', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);
    mockMediaStorage.generatePresignedUpload.mockResolvedValue({
      uploadUrl: 'https://minio/upload',
      publicUrl: 'https://minio/public',
      objectKey: 'kyc/user-1/file.jpg',
    });

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
      { field: 'backImage', mimeType: 'image/png', fileName: 'back.png' },
      { field: 'selfieImage', mimeType: 'image/webp', fileName: 'selfie.webp' },
    ]);

    const result = await handler.execute(command);

    expect(result).toHaveLength(3);
    expect(mockMediaStorage.generatePresignedUpload).toHaveBeenCalledTimes(3);
  });

  it('allows resubmission when kycStatus is REJECTED', async () => {
    const user = createTestUser({ kycStatus: 'REJECTED' });
    mockUserRepo.findById.mockResolvedValue(user);
    mockMediaStorage.generatePresignedUpload.mockResolvedValue({
      uploadUrl: 'https://minio/upload',
      publicUrl: 'https://minio/public',
      objectKey: 'kyc/user-1/front.jpg',
    });

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    const result = await handler.execute(command);
    expect(result).toHaveLength(1);
  });

  it('throws NotFoundException when user does not exist', async () => {
    mockUserRepo.findById.mockResolvedValue(null);

    const command = new GenerateKycUploadUrlsCommand('non-existent', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException when kycStatus is PENDING', async () => {
    const user = createTestUser({ kycStatus: 'PENDING' });
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException when kycStatus is VERIFIED', async () => {
    const user = createTestUser({ kycStatus: 'VERIFIED' });
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for empty files array', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new GenerateKycUploadUrlsCommand('user-1', []);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for more than 3 files', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: '1.jpg' },
      { field: 'backImage', mimeType: 'image/jpeg', fileName: '2.jpg' },
      { field: 'selfieImage', mimeType: 'image/jpeg', fileName: '3.jpg' },
      { field: 'frontImage' as any, mimeType: 'image/jpeg', fileName: '4.jpg' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for unsupported MIME type', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'application/pdf', fileName: 'doc.pdf' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException when presigned URL generation fails', async () => {
    const user = createTestUser();
    mockUserRepo.findById.mockResolvedValue(user);
    mockMediaStorage.generatePresignedUpload.mockRejectedValue(
      new Error('S3 connection failed'),
    );

    const command = new GenerateKycUploadUrlsCommand('user-1', [
      { field: 'frontImage', mimeType: 'image/jpeg', fileName: 'front.jpg' },
    ]);

    await expect(handler.execute(command)).rejects.toThrow();
    expect(mockLogger.error).toHaveBeenCalled();
  });
});
