import { PaymentEntity } from '../../domain/entities/payment.entity';
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
import { Money } from '../../domain/value-objects/money.vo';
import { ConfirmBankTransferCommand } from '../commands/confirm-bank-transfer/confirm-bank-transfer.command';
import { ConfirmBankTransferHandler } from '../commands/confirm-bank-transfer/confirm-bank-transfer.handler';

function createPendingBankTransferPayment(id = 'pay-bt-1'): PaymentEntity {
  const money = Money.create(1_000_000n).unwrap();
  const payment = PaymentEntity.createNew(id, 'user-1', 'BANK_TRANSFER', 'SUBSCRIPTION', money);
  payment.markProcessing('BT-' + id);
  payment.clearDomainEvents();
  return payment;
}

function _createCompletedBankTransferPayment(): PaymentEntity {
  const money = Money.create(1_000_000n).unwrap();
  const payment = PaymentEntity.createNew('pay-bt-done', 'user-1', 'BANK_TRANSFER', 'SUBSCRIPTION', money);
  payment.markProcessing('BT-pay-bt-done');
  payment.markCompleted({ confirmedBy: 'admin-prev' });
  payment.clearDomainEvents();
  return payment;
}

describe('ConfirmBankTransferHandler', () => {
  let handler: ConfirmBankTransferHandler;
  let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
  let mockBankTransferService: {
    generateConfirmationSignature: ReturnType<typeof vi.fn>;
    verifyCallback: 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(),
    };

    mockBankTransferService = {
      generateConfirmationSignature: vi.fn().mockReturnValue('valid-signature-hex'),
      verifyCallback: vi.fn().mockReturnValue({
        isValid: true,
        orderId: 'pay-bt-1',
        providerTxId: 'BT-pay-bt-1',
        isSuccess: true,
        rawData: { orderId: 'pay-bt-1', status: 'CONFIRMED' },
      }),
    };

    mockEventBus = { publish: vi.fn() };

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

    handler = new ConfirmBankTransferHandler(
      mockPaymentRepo as any,
      mockBankTransferService as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('should confirm a pending bank transfer payment', async () => {
    const payment = createPendingBankTransferPayment();
    mockPaymentRepo.findById.mockResolvedValue(payment);

    const updatedPayment = createPendingBankTransferPayment();
    (updatedPayment as any)._status = 'COMPLETED';
    mockPaymentRepo.updateIfStatus.mockResolvedValue(updatedPayment);

    const command = new ConfirmBankTransferCommand('pay-bt-1', 'admin-user-1', 'FT123456');
    const result = await handler.execute(command);

    expect(result.paymentId).toBe('pay-bt-1');
    expect(result.status).toBe('COMPLETED');
    expect(result.confirmedBy).toBe('admin-user-1');

    expect(mockBankTransferService.generateConfirmationSignature).toHaveBeenCalledWith(
      'pay-bt-1',
      'BT-pay-bt-1',
      'CONFIRMED',
      'admin-user-1',
    );

    expect(mockPaymentRepo.updateIfStatus).toHaveBeenCalledWith(
      'pay-bt-1',
      ['PENDING', 'PROCESSING'],
      expect.objectContaining({ status: 'COMPLETED' }),
    );

    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('should include bankReference in callback data when provided', async () => {
    const payment = createPendingBankTransferPayment();
    mockPaymentRepo.findById.mockResolvedValue(payment);

    const updatedPayment = createPendingBankTransferPayment();
    (updatedPayment as any)._status = 'COMPLETED';
    mockPaymentRepo.updateIfStatus.mockResolvedValue(updatedPayment);

    const command = new ConfirmBankTransferCommand('pay-bt-1', 'admin-1', 'FT999888');
    await handler.execute(command);

    // verifyCallback should have been called with bankReference included
    expect(mockBankTransferService.verifyCallback).toHaveBeenCalledWith(
      expect.objectContaining({
        bankReference: 'FT999888',
      }),
    );
  });

  it('should throw NotFoundException when payment does not exist', async () => {
    mockPaymentRepo.findById.mockResolvedValue(null);

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

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

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

    const command = new ConfirmBankTransferCommand('pay-vnpay', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow(/chuyển khoản ngân hàng/);
  });

  it('should handle already-completed payment idempotently', async () => {
    const payment = createPendingBankTransferPayment('pay-bt-1');

    // Create a completed version with the same ID
    const money = Money.create(1_000_000n).unwrap();
    const completedPayment = PaymentEntity.createNew('pay-bt-1', 'user-1', 'BANK_TRANSFER', 'SUBSCRIPTION', money);
    completedPayment.markProcessing('BT-pay-bt-1');
    completedPayment.markCompleted({ confirmedBy: 'admin-prev' });
    completedPayment.clearDomainEvents();

    // First call: find the payment for initial check
    // Second call: find again after updateIfStatus returns null
    mockPaymentRepo.findById
      .mockResolvedValueOnce(payment)
      .mockResolvedValueOnce(completedPayment);

    // updateIfStatus returns null — payment already in terminal state
    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);

    const command = new ConfirmBankTransferCommand('pay-bt-1', 'admin-1');
    const result = await handler.execute(command);

    expect(result.paymentId).toBe('pay-bt-1');
    expect(result.status).toBe('COMPLETED');
    expect(result.confirmedBy).toBe('admin-1');
  });

  it('should throw when payment is in non-confirmable terminal state', async () => {
    const money = Money.create(500_000n).unwrap();
    const failedPayment = PaymentEntity.createNew('pay-bt-failed', 'user-1', 'BANK_TRANSFER', 'SUBSCRIPTION', money);
    (failedPayment as any)._status = 'FAILED';
    (failedPayment as any)._providerTxId = 'BT-pay-bt-failed';
    failedPayment.clearDomainEvents();

    // First findById: initial check returns the failed payment
    // Second findById: re-check after updateIfStatus returns null
    mockPaymentRepo.findById
      .mockResolvedValueOnce(failedPayment)
      .mockResolvedValueOnce(failedPayment);

    mockPaymentRepo.updateIfStatus.mockResolvedValue(null);

    const command = new ConfirmBankTransferCommand('pay-bt-failed', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow(/trạng thái/);
  });
});
