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' | 'FAILED' = 'PROCESSING',
  provider: 'VNPAY' | 'MOMO' | 'ZALOPAY' = 'VNPAY',
): PaymentEntity {
  const money = Money.create(500_000n).unwrap();
  const payment = PaymentEntity.createNew('pay-1', 'user-1', provider, 'SUBSCRIPTION', money);
  if (status === 'PROCESSING') payment.markProcessing('provider-tx-1');
  if (status === 'COMPLETED') {
    payment.markProcessing('provider-tx-1');
    payment.markCompleted({ verified: true });
  }
  if (status === 'FAILED') {
    payment.markProcessing('provider-tx-1');
    payment.markFailed({ reason: 'declined' });
  }
  payment.clearDomainEvents();
  return payment;
}

describe('HandleCallbackHandler — edge cases', () => {
  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('returns idempotent response for duplicate success callback on COMPLETED payment', async () => {
    const completedPayment = createPaymentEntity('COMPLETED');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'provider-tx-1',
      isSuccess: true,
      rawData: { responseCode: '00' },
    });
    // updateIfStatus returns null because payment is already COMPLETED
    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);
    // No new events should be published for idempotent response
    expect(mockEventBus.publish).not.toHaveBeenCalled();
  });

  it('returns idempotent response for duplicate callback on FAILED payment', async () => {
    const failedPayment = createPaymentEntity('FAILED');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'provider-tx-1',
      isSuccess: false,
      rawData: { responseCode: '24' },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
    mockPaymentRepo.findById.mockResolvedValue(failedPayment);

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

    expect(result.paymentId).toBe('pay-1');
    expect(result.status).toBe('FAILED');
    expect(result.isSuccess).toBe(false);
    expect(mockEventBus.publish).not.toHaveBeenCalled();
  });

  it('uses correct gateway for MoMo provider', async () => {
    const payment = createPaymentEntity('PROCESSING', 'MOMO');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'momo-tx-1',
      isSuccess: true,
      rawData: { resultCode: 0 },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);

    const command = new HandleCallbackCommand('MOMO', { resultCode: 0 });
    await handler.execute(command);

    expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('MOMO');
  });

  it('uses correct gateway for ZaloPay provider', async () => {
    const payment = createPaymentEntity('PROCESSING', 'ZALOPAY');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'zalo-tx-1',
      isSuccess: true,
      rawData: { return_code: 1 },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);

    const command = new HandleCallbackCommand('ZALOPAY', { return_code: 1 });
    await handler.execute(command);

    expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('ZALOPAY');
  });

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

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

    expect(result.isSuccess).toBe(false);
    expect(mockEventBus.publish).toHaveBeenCalledWith(
      expect.objectContaining({ eventName: 'payment.failed' }),
    );
  });

  it('publishes PaymentCompletedEvent on successful callback', async () => {
    const payment = createPaymentEntity('PROCESSING');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'provider-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(mockEventBus.publish).toHaveBeenCalledWith(
      expect.objectContaining({ eventName: 'payment.completed' }),
    );
  });

  it('handles concurrent callbacks — second callback gets idempotent response', async () => {
    // Simulate: first callback succeeds, second callback arrives and updateIfStatus returns null
    const completedPayment = createPaymentEntity('COMPLETED');
    mockGateway.verifyCallback.mockReturnValue({
      isValid: true,
      orderId: 'pay-1',
      providerTxId: 'provider-tx-1',
      isSuccess: true,
      rawData: { responseCode: '00' },
    });
    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
    mockPaymentRepo.findById.mockResolvedValue(completedPayment);

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

    expect(result.status).toBe('COMPLETED');
    expect(mockEventBus.publish).not.toHaveBeenCalled();
  });
});
