import { PaymentEntity } from '../../domain/entities/payment.entity';
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
import { Money } from '../../domain/value-objects/money.vo';
import { HandleCallbackCommand } from '../commands/handle-callback/handle-callback.command';
import { HandleCallbackHandler } from '../commands/handle-callback/handle-callback.handler';

function createPaymentEntity(status: 'PENDING' | 'PROCESSING' | 'COMPLETED' = 'PROCESSING'): PaymentEntity {
  const money = Money.create(500_000n).unwrap();
  const payment = PaymentEntity.createNew('pay-1', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
  if (status === 'PROCESSING') payment.markProcessing('vnpay-tx-1');
  if (status === 'COMPLETED') {
    payment.markProcessing('vnpay-tx-1');
    payment.markCompleted({ verified: true });
  }
  payment.clearDomainEvents();
  return payment;
}

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

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

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

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

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

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

  it('handles successful callback', async () => {
    const payment = createPaymentEntity('PROCESSING');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'vnpay-tx-1',
      isSuccess: true,
      rawData: { responseCode: '00' },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);

    const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });
    const result = await handler.execute(command);

    expect(result.isSuccess).toBe(true);
    expect(result.paymentId).toBe('pay-1');
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('handles failed callback', async () => {
    const payment = createPaymentEntity('PROCESSING');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'vnpay-tx-1',
      isSuccess: false,
      rawData: { responseCode: '24' },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);

    const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '24' });
    const result = await handler.execute(command);

    expect(result.isSuccess).toBe(false);
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('throws ValidationException for invalid callback signature', async () => {
    mockGateway.verifyCallback.mockReturnValue({
      isValid: false,
      orderId: '',
      providerTxId: '',
      isSuccess: false,
      rawData: {},
    });

    const command = new HandleCallbackCommand('VNPAY', { tampered: 'true' });

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

  it('returns idempotent response for already processed payment', async () => {
    const completedPayment = createPaymentEntity('COMPLETED');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'vnpay-tx-1',
      isSuccess: true,
      rawData: {},
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
    mockPaymentRepo.findById.mockResolvedValue(completedPayment);

    const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });
    const result = await handler.execute(command);

    expect(result.paymentId).toBe('pay-1');
    expect(result.status).toBe('COMPLETED');
    expect(result.isSuccess).toBe(true);
  });

  it('throws NotFoundException when payment not found after failed update', async () => {
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'nonexistent',
      providerTxId: 'tx-1',
      isSuccess: true,
      rawData: {},
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
    mockPaymentRepo.findById.mockResolvedValue(null);

    const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });

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