import { PaymentEntity } from '../../domain/entities/payment.entity';
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
import { Money } from '../../domain/value-objects/money.vo';
import { RefundPaymentCommand } from '../commands/refund-payment/refund-payment.command';
import { RefundPaymentHandler } from '../commands/refund-payment/refund-payment.handler';

function createCompletedPayment(): PaymentEntity {
  const money = Money.create(1_000_000n).unwrap();
  const payment = PaymentEntity.createNew('pay-1', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
  payment.markProcessing('vnpay-tx-1');
  payment.markCompleted({ verified: true });
  payment.clearDomainEvents();
  return payment;
}

describe('RefundPaymentHandler', () => {
  let handler: RefundPaymentHandler;
  let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
  let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
  let mockGateway: { refund: ReturnType<typeof vi.fn>; createPaymentUrl: ReturnType<typeof vi.fn>; verifyCallback: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPaymentRepo = {
      findById: vi.fn(),
      findByProviderTxId: vi.fn(),
      findByIdempotencyKey: vi.fn(),
      findByUserId: vi.fn(),
      save: vi.fn(),
      update: vi.fn().mockResolvedValue(undefined),
      updateIfStatus: vi.fn(),
    };

    mockGateway = {
      refund: vi.fn(),
      createPaymentUrl: vi.fn(),
      verifyCallback: vi.fn(),
    };

    mockGatewayFactory = { getGateway: vi.fn().mockReturnValue(mockGateway) };

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

    handler = new RefundPaymentHandler(
      mockPaymentRepo as any,
      mockGatewayFactory as any,
      mockLogger as any,
    );
  });

  it('refunds a completed payment successfully', async () => {
    const payment = createCompletedPayment();
    mockPaymentRepo.findById.mockResolvedValue(payment);
    mockGateway.refund.mockResolvedValue({ success: true, refundTxId: 'refund-tx-1' });

    const command = new RefundPaymentCommand('pay-1', 'Yêu cầu hoàn tiền', 'admin-1');
    const result = await handler.execute(command);

    expect(result.success).toBe(true);
    expect(result.refundTxId).toBe('refund-tx-1');
    expect(result.paymentId).toBe('pay-1');
    expect(mockPaymentRepo.update).toHaveBeenCalledTimes(1);
  });

  it('handles failed refund from gateway', async () => {
    const payment = createCompletedPayment();
    mockPaymentRepo.findById.mockResolvedValue(payment);
    mockGateway.refund.mockResolvedValue({ success: false, refundTxId: null });

    const command = new RefundPaymentCommand('pay-1', 'Hoàn tiền', 'admin-1');
    const result = await handler.execute(command);

    expect(result.success).toBe(false);
    expect(mockPaymentRepo.update).not.toHaveBeenCalled();
  });

  it('throws NotFoundException when payment not found', async () => {
    mockPaymentRepo.findById.mockResolvedValue(null);

    const command = new RefundPaymentCommand('nonexistent', 'reason', 'admin-1');

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

  it('throws ValidationException when payment is not completed', async () => {
    const money = Money.create(500_000n).unwrap();
    const payment = PaymentEntity.createNew('pay-2', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
    payment.clearDomainEvents();
    mockPaymentRepo.findById.mockResolvedValue(payment);

    const command = new RefundPaymentCommand('pay-2', 'reason', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow(/hoàn tất/);
  });

  it('throws ValidationException when no provider transaction id', async () => {
    const money = Money.create(500_000n).unwrap();
    const payment = PaymentEntity.createNew('pay-3', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
    // Manually mark completed without providerTxId by using internal hack
    (payment as any)._status = 'COMPLETED';
    (payment as any)._providerTxId = null;
    payment.clearDomainEvents();
    mockPaymentRepo.findById.mockResolvedValue(payment);

    const command = new RefundPaymentCommand('pay-3', 'reason', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow(/mã giao dịch/);
  });
});
