import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('../../services/amanpayService', () => ({
  isConfigured: vi.fn(),
  createCheckout: vi.fn(),
  refundTransaction: vi.fn(),
}))

vi.mock('../../services/paypalService', () => ({
  isConfigured: vi.fn(),
  createOrder: vi.fn(),
  captureOrder: vi.fn(),
  refundCapture: vi.fn(),
}))

vi.mock('./payment.repo', () => ({
  findByCompany: vi.fn(),
  findByReservation: vi.fn(),
  findByAmanpay: vi.fn(),
  findByPaypal: vi.fn(),
  findByPaypalForCompany: vi.fn(),
  findPaymentOrThrow: vi.fn(),
  findReservationOrThrow: vi.fn(),
  findReservation: vi.fn(),
  markPaymentSucceeded: vi.fn(),
  markPaymentFailed: vi.fn(),
  incrementReservationPaid: vi.fn(),
  createPayment: vi.fn(),
  updatePaypalCapture: vi.fn(),
  setReservationPaidAmount: vi.fn(),
  setReservationRefunded: vi.fn(),
  setPaymentRefunded: vi.fn(),
}))

import { ConflictError, ValidationError } from '../../http/errors'
import * as amanpay from '../../services/amanpayService'
import * as paypal from '../../services/paypalService'
import * as repo from './payment.repo'
import {
  capturePaypal,
  handleAmanpayWebhook,
  handlePaypalWebhook,
  initCharge,
  recordManualPayment,
  refundPayment,
} from './payment.service'

const reservation = {
  id: 'reservation_1',
  companyId: 'company_1',
  paymentStatus: 'UNPAID',
  depositAmount: 300,
  totalAmount: 1200,
  paidAmount: 200,
  rentalPayments: [],
  vehicle: { make: 'Dacia', model: 'Duster' },
  customer: { firstName: 'Nora', lastName: 'Driver', email: 'nora@example.com' },
}

describe('payment.service', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.useFakeTimers()
    vi.setSystemTime(new Date('2026-06-08T10:15:00.000Z'))
    delete process.env.API_URL
  })

  afterEach(() => {
    vi.useRealTimers()
    delete process.env.API_URL
  })

  it('creates an AmanPay deposit checkout using reservation, customer, and webhook details', async () => {
    vi.mocked(repo.findReservationOrThrow).mockResolvedValue(reservation as never)
    vi.mocked(amanpay.isConfigured).mockReturnValue(true)
    vi.mocked(amanpay.createCheckout).mockResolvedValue({ checkoutUrl: 'https://pay.example/checkout', transactionId: 'aman_txn_1' } as never)
    vi.mocked(repo.createPayment).mockResolvedValue({ id: 'payment_1', status: 'PENDING' } as never)

    const result = await initCharge('reservation_1', 'company_1', {
      provider: 'AMANPAY',
      type: 'DEPOSIT',
      currency: 'MAD',
      successUrl: 'https://app.example/success',
      failureUrl: 'https://app.example/failure',
    })

    expect(amanpay.createCheckout).toHaveBeenCalledWith(expect.objectContaining({
      amount: 300,
      currency: 'MAD',
      orderId: 'reservation_1-DEPOSIT-1780913700000',
      description: 'Deposit: Dacia Duster',
      customerEmail: 'nora@example.com',
      customerName: 'Nora Driver',
      webhookUrl: 'http://localhost:4000/api/v1/payments/webhooks/amanpay',
    }))
    expect(repo.createPayment).toHaveBeenCalledWith(expect.objectContaining({
      companyId: 'company_1',
      reservationId: 'reservation_1',
      amount: 300,
      status: 'PENDING',
      type: 'DEPOSIT',
      paymentProvider: 'AMANPAY',
      amanpayTransactionId: 'aman_txn_1',
      paypalCaptureId: null,
    }))
    expect(result).toEqual({ payment: { id: 'payment_1', status: 'PENDING' }, checkoutUrl: 'https://pay.example/checkout' })
  })

  it('refuses to initialize a charge for a fully paid reservation before touching gateways', async () => {
    vi.mocked(repo.findReservationOrThrow).mockResolvedValue({
      ...reservation,
      paymentStatus: 'PAID',
      rentalPayments: [{ type: 'CHARGE', status: 'SUCCEEDED', amount: 1200 }],
    } as never)

    await expect(initCharge('reservation_1', 'company_1', {
      provider: 'PAYPAL',
      type: 'CHARGE',
      currency: 'MAD',
      successUrl: 'https://app.example/success',
      failureUrl: 'https://app.example/failure',
    })).rejects.toBeInstanceOf(ConflictError)

    expect(paypal.isConfigured).not.toHaveBeenCalled()
    expect(repo.createPayment).not.toHaveBeenCalled()
  })

  it('allows an outstanding deposit when the rental invoice is fully paid', async () => {
    vi.mocked(repo.findReservationOrThrow).mockResolvedValue({
      ...reservation,
      paymentStatus: 'PAID',
      rentalPayments: [{ type: 'CHARGE', status: 'SUCCEEDED', amount: 1200 }],
    } as never)
    vi.mocked(amanpay.isConfigured).mockReturnValue(true)
    vi.mocked(amanpay.createCheckout).mockResolvedValue({ checkoutUrl: 'https://pay.example/checkout', transactionId: 'aman_txn_2' } as never)
    vi.mocked(repo.createPayment).mockResolvedValue({ id: 'payment_2', status: 'PENDING' } as never)

    await initCharge('reservation_1', 'company_1', {
      provider: 'AMANPAY',
      type: 'DEPOSIT',
      currency: 'MAD',
      successUrl: 'https://app.example/success',
      failureUrl: 'https://app.example/failure',
    })

    expect(amanpay.createCheckout).toHaveBeenCalledWith(expect.objectContaining({ amount: 300 }))
    expect(repo.createPayment).toHaveBeenCalledWith(expect.objectContaining({ type: 'DEPOSIT' }))
  })

  it('records manual payments and marks the reservation as partially or fully paid from the balance math', async () => {
    vi.mocked(repo.findReservation).mockResolvedValue({
      id: 'reservation_1',
      totalAmount: 1000,
      depositAmount: 300,
      paidAmount: 700,
      rentalPayments: [{ type: 'CHARGE', status: 'SUCCEEDED', amount: 700 }],
    } as never)
    vi.mocked(repo.createPayment).mockResolvedValue({ id: 'payment_1', amount: 300, status: 'SUCCEEDED' } as never)

    const payment = await recordManualPayment('reservation_1', 'company_1', {
      amount: 300,
      currency: 'MAD',
      type: 'CHARGE',
      paymentMethod: 'CASH',
    })

    expect(repo.createPayment).toHaveBeenCalledWith(expect.objectContaining({
      status: 'SUCCEEDED',
      paymentProvider: 'MANUAL',
      paymentMethod: 'CASH',
      paidAt: expect.any(Date),
    }))
    expect(repo.setReservationPaidAmount).toHaveBeenCalledWith('reservation_1', 1000, 'PAID')
    expect(payment).toEqual({ id: 'payment_1', amount: 300, status: 'SUCCEEDED' })
  })

  it('rejects manual overpayments instead of silently corrupting the reservation balance', async () => {
    vi.mocked(repo.findReservation).mockResolvedValue({
      id: 'reservation_1',
      totalAmount: 1000,
      depositAmount: 300,
      paidAmount: 950,
      rentalPayments: [{ type: 'CHARGE', status: 'SUCCEEDED', amount: 950 }],
    } as never)

    await expect(recordManualPayment('reservation_1', 'company_1', {
      amount: 100,
      currency: 'MAD',
      type: 'CHARGE',
      paymentMethod: 'BANK_TRANSFER',
    })).rejects.toBeInstanceOf(ValidationError)

    expect(repo.createPayment).not.toHaveBeenCalled()
    expect(repo.setReservationPaidAmount).not.toHaveBeenCalled()
  })

  it('applies paid AmanPay and denied PayPal webhook events to the matching records', async () => {
    vi.mocked(repo.findByAmanpay).mockResolvedValue({ id: 'payment_1', reservationId: 'reservation_1', amount: 450, type: 'CHARGE' } as never)
    vi.mocked(repo.findByPaypal).mockResolvedValue({ id: 'payment_2', reservationId: 'reservation_2', amount: 500, type: 'CHARGE' } as never)

    await handleAmanpayWebhook({ transaction_id: 'aman_txn_1', status: 'paid' })
    await handlePaypalWebhook({ event_type: 'PAYMENT.CAPTURE.COMPLETED', resource: { id: 'paypal_capture_1' } })
    await handlePaypalWebhook({ event_type: 'PAYMENT.CAPTURE.DENIED', resource: { id: 'paypal_capture_2' } })

    expect(repo.markPaymentSucceeded).toHaveBeenCalledWith('payment_1')
    expect(repo.incrementReservationPaid).toHaveBeenCalledWith('reservation_1', 450)
    expect(repo.markPaymentSucceeded).toHaveBeenCalledWith('payment_2')
    expect(repo.incrementReservationPaid).toHaveBeenCalledWith('reservation_2', 500)
    expect(repo.markPaymentFailed).toHaveBeenCalledWith({ paypalCaptureId: 'paypal_capture_2' })
  })

  it('captures PayPal orders, stores the capture id, and increments the original reservation payment', async () => {
    vi.mocked(repo.findByPaypalForCompany).mockResolvedValue({ id: 'payment_1', reservationId: 'reservation_1', amount: 800, type: 'CHARGE' } as never)
    vi.mocked(paypal.captureOrder).mockResolvedValue({ purchase_units: [{ payments: { captures: [{ id: 'capture_123' }] } }] } as never)
    vi.mocked(repo.updatePaypalCapture).mockResolvedValue({ id: 'payment_1', status: 'SUCCEEDED', paypalCaptureId: 'capture_123' } as never)

    const result = await capturePaypal('reservation_1', 'company_1', 'order_123')

    expect(repo.findByPaypalForCompany).toHaveBeenCalledWith('order_123', 'company_1')
    expect(repo.updatePaypalCapture).toHaveBeenCalledWith('payment_1', 'capture_123')
    expect(repo.incrementReservationPaid).toHaveBeenCalledWith('reservation_1', 800)
    expect(result).toEqual({ id: 'payment_1', status: 'SUCCEEDED', paypalCaptureId: 'capture_123' })
  })

  it('refunds gateway payments and only marks the reservation refunded on full refunds', async () => {
    vi.mocked(repo.findPaymentOrThrow).mockResolvedValue({
      id: 'payment_1',
      reservationId: 'reservation_1',
      status: 'SUCCEEDED',
      amount: 1000,
      currency: 'MAD',
      paymentProvider: 'PAYPAL',
      paypalCaptureId: 'capture_123',
      amanpayTransactionId: null,
    } as never)
    vi.mocked(repo.setPaymentRefunded).mockResolvedValue({ id: 'payment_1', status: 'PARTIALLY_REFUNDED' } as never)

    const result = await refundPayment('reservation_1', 'payment_1', 'company_1', 250, 'Customer request')

    expect(paypal.refundCapture).toHaveBeenCalledWith('capture_123', 250, 'MAD', 'Customer request')
    expect(repo.setPaymentRefunded).toHaveBeenCalledWith('payment_1', true)
    expect(repo.setReservationRefunded).not.toHaveBeenCalled()
    expect(result).toEqual({ id: 'payment_1', status: 'PARTIALLY_REFUNDED' })
  })
})
