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 { SubmitKycCommand } from '../commands/submit-kyc/submit-kyc.command';
import { SubmitKycHandler } from '../commands/submit-kyc/submit-kyc.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('SubmitKycHandler', () => {
  let handler: SubmitKycHandler;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockMediaStorage: { [K in keyof IMediaStorageService]: ReturnType<typeof vi.fn> };
  let mockCache: { invalidate: ReturnType<typeof vi.fn>; buildKey: 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),
    };
    mockCache = {
      invalidate: vi.fn().mockResolvedValue(undefined),
      buildKey: vi.fn(),
    };
    mockLogger = {
      error: vi.fn(),
      warn: vi.fn(),
      log: vi.fn(),
    };

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

  describe('presigned URL flow', () => {
    it('submits KYC with presigned image URLs', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        {
          frontImageUrl: 'https://minio/kyc/user-1/front.jpg',
          backImageUrl: 'https://minio/kyc/user-1/back.jpg',
          selfieUrl: 'https://minio/kyc/user-1/selfie.jpg',
        },
      );

      const result = await handler.execute(command);

      expect(result).toEqual({ message: expect.any(String) });
      expect(mockUserRepo.update).toHaveBeenCalledWith(user);
      expect(user.kycStatus).toBe('PENDING');
      expect(mockCache.invalidate).toHaveBeenCalled();
    });

    it('submits KYC with only front image URL', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/kyc/user-1/front.jpg' },
      );

      const result = await handler.execute(command);

      expect(result.message).toBeTruthy();
      expect(user.kycStatus).toBe('PENDING');
      expect(user.kycData).toMatchObject({
        idType: 'CCCD',
        idNumber: '012345678901',
        frontImageUrl: 'https://minio/kyc/user-1/front.jpg',
        backImageUrl: null,
        selfieUrl: null,
      });
    });

    it('allows resubmission when kycStatus is REJECTED', async () => {
      const user = createTestUser({ kycStatus: 'REJECTED' });
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);

      const command = new SubmitKycCommand(
        'user-1',
        'PASSPORT',
        'B12345678',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/kyc/user-1/front.jpg' },
      );

      const result = await handler.execute(command);
      expect(result.message).toBeTruthy();
      expect(user.kycStatus).toBe('PENDING');
    });

    it('rejects untrusted image URL hosts', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockMediaStorage.isTrustedUrl.mockImplementation((url: string) =>
        url.startsWith('https://minio/'),
      );

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://evil.example.com/kyc/front.jpg' },
      );

      await expect(handler.execute(command)).rejects.toThrow();
      expect(mockUserRepo.update).not.toHaveBeenCalled();
    });

    it('rejects image URL that belongs to a different user namespace', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      // host is trusted but path is /bucket/kyc/user-2/...
      mockMediaStorage.isTrustedUrl.mockReturnValue(true);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/bucket/kyc/user-2/front.jpg' },
      );

      await expect(handler.execute(command)).rejects.toThrow(
        /khong thuoc ve nguoi dung/i,
      );
      expect(mockUserRepo.update).not.toHaveBeenCalled();
    });

    it('rejects image URL outside the kyc folder', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockMediaStorage.isTrustedUrl.mockReturnValue(true);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/bucket/listings/user-1/front.jpg' },
      );

      await expect(handler.execute(command)).rejects.toThrow();
      expect(mockUserRepo.update).not.toHaveBeenCalled();
    });

    it('accepts URL inside the caller kyc namespace across all three images', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);
      mockMediaStorage.isTrustedUrl.mockReturnValue(true);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        {
          frontImageUrl: 'https://minio/bucket/kyc/user-1/1-front.jpg',
          backImageUrl: 'https://minio/bucket/kyc/user-1/1-back.jpg',
          selfieUrl: 'https://minio/bucket/kyc/user-1/1-selfie.jpg',
        },
      );

      const result = await handler.execute(command);
      expect(result.message).toBeTruthy();
      expect(user.kycStatus).toBe('PENDING');
    });

    it('rejects when any of the back/selfie URLs escapes the namespace', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockMediaStorage.isTrustedUrl.mockReturnValue(true);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        {
          frontImageUrl: 'https://minio/bucket/kyc/user-1/front.jpg',
          backImageUrl: 'https://minio/bucket/kyc/user-2/back.jpg',
        },
      );

      await expect(handler.execute(command)).rejects.toThrow();
      expect(mockUserRepo.update).not.toHaveBeenCalled();
    });
  });

  describe('legacy file upload flow', () => {
    it('submits KYC with file buffers', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);
      mockMediaStorage.upload.mockResolvedValue('https://minio/kyc/user-1/front.jpg');

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        { buffer: Buffer.from('front'), mimetype: 'image/jpeg', originalname: 'front.jpg', size: 5 },
      );

      const result = await handler.execute(command);

      expect(result.message).toBeTruthy();
      expect(mockMediaStorage.upload).toHaveBeenCalledTimes(1);
      expect(user.kycStatus).toBe('PENDING');
    });

    it('uploads all optional files when provided', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.update.mockResolvedValue(undefined);
      mockMediaStorage.upload.mockResolvedValue('https://minio/kyc/user-1/file.jpg');

      const fileData = { buffer: Buffer.from('img'), mimetype: 'image/jpeg', originalname: 'img.jpg', size: 3 };

      const command = new SubmitKycCommand(
        'user-1',
        'CMND',
        '123456789',
        fileData,
        fileData,
        fileData,
      );

      await handler.execute(command);

      expect(mockMediaStorage.upload).toHaveBeenCalledTimes(3);
    });
  });

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

      const command = new SubmitKycCommand(
        'non-existent',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/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 SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/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 SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        undefined,
        undefined,
        undefined,
        { frontImageUrl: 'https://minio/front.jpg' },
      );

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

    it('throws ValidationException when no images provided', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
      );

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

    it('throws when legacy file upload fails', async () => {
      const user = createTestUser();
      mockUserRepo.findById.mockResolvedValue(user);
      mockMediaStorage.upload.mockRejectedValue(new Error('S3 error'));

      const command = new SubmitKycCommand(
        'user-1',
        'CCCD',
        '012345678901',
        { buffer: Buffer.from('front'), mimetype: 'image/jpeg', originalname: 'front.jpg', size: 5 },
      );

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