package com.flightticket.service;

import com.flightticket.dto.wallet.TransactionResult;
import com.flightticket.exception.InsufficientBalanceException;
import com.flightticket.exception.WalletNotFoundException;
import com.flightticket.model.entity.Wallet;
import com.flightticket.model.entity.WalletTransaction;
import com.flightticket.model.enums.TransactionCategory;
import com.flightticket.model.enums.TransactionType;
import com.flightticket.repository.WalletRepository;
import com.flightticket.repository.WalletTransactionRepository;
import com.flightticket.service.impl.WalletServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/**
 * Unit tests for WalletService modification-related methods.
 * 
 * Tests Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 6.3, 6.5, 6.6, 6.7, 7.4, 8.3
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("WalletService Modification Tests")
class WalletServiceModificationTest {
    
    @Mock
    private WalletRepository walletRepository;
    
    @Mock
    private WalletTransactionRepository transactionRepository;
    
    @InjectMocks
    private WalletServiceImpl walletService;
    
    private UUID agencyId;
    private UUID walletId;
    private UUID bookingId;
    private UUID userId;
    private String modificationId;
    private Wallet wallet;
    
    @BeforeEach
    void setUp() {
        agencyId = UUID.randomUUID();
        walletId = UUID.randomUUID();
        bookingId = UUID.randomUUID();
        userId = UUID.randomUUID();
        modificationId = "MOD-" + UUID.randomUUID();
        
        wallet = new Wallet();
        wallet.setId(walletId);
        wallet.setAgencyId(agencyId);
        wallet.setBalance(new BigDecimal("1000.00"));
        wallet.setReservedAmount(BigDecimal.ZERO);
    }
    
    // ========== recordModificationCharge Tests ==========
    
    @Test
    @DisplayName("Should successfully record modification charge when balance is sufficient")
    void testRecordModificationCharge_Success() {
        // Given
        BigDecimal chargeAmount = new BigDecimal("150.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        when(walletRepository.save(any(Wallet.class))).thenReturn(wallet);
        
        WalletTransaction savedTransaction = new WalletTransaction();
        savedTransaction.setId(UUID.randomUUID());
        when(transactionRepository.save(any(WalletTransaction.class))).thenReturn(savedTransaction);
        
        // When
        TransactionResult result = walletService.recordModificationCharge(
            agencyId, chargeAmount, modificationId, bookingId, userId
        );
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.isSuccess()).isTrue();
        assertThat(result.getBalanceBefore()).isEqualByComparingTo(new BigDecimal("1000.00"));
        assertThat(result.getBalanceAfter()).isEqualByComparingTo(new BigDecimal("850.00"));
        
        // Verify wallet was debited
        ArgumentCaptor<Wallet> walletCaptor = ArgumentCaptor.forClass(Wallet.class);
        verify(walletRepository).save(walletCaptor.capture());
        assertThat(walletCaptor.getValue().getBalance()).isEqualByComparingTo(new BigDecimal("850.00"));
        
        // Verify transaction was created with correct details
        ArgumentCaptor<WalletTransaction> transactionCaptor = ArgumentCaptor.forClass(WalletTransaction.class);
        verify(transactionRepository).save(transactionCaptor.capture());
        WalletTransaction transaction = transactionCaptor.getValue();
        
        assertThat(transaction.getWalletId()).isEqualTo(walletId);
        assertThat(transaction.getTransactionType()).isEqualTo(TransactionType.DEBIT_MODIFICATION);
        assertThat(transaction.getTransactionCategory()).isEqualTo(TransactionCategory.MODIFICATION);
        assertThat(transaction.getAmount()).isEqualByComparingTo(chargeAmount);
        assertThat(transaction.getModificationId()).isEqualTo(modificationId);
        assertThat(transaction.getBookingId()).isEqualTo(bookingId);
        assertThat(transaction.getCreatedBy()).isEqualTo(userId);
        assertThat(transaction.getReference()).startsWith("MOD-CHARGE-");
    }
    
    @Test
    @DisplayName("Should throw InsufficientBalanceException when balance is insufficient")
    void testRecordModificationCharge_InsufficientBalance() {
        // Given
        BigDecimal chargeAmount = new BigDecimal("1500.00"); // More than available
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, chargeAmount, modificationId, bookingId, userId
        ))
            .isInstanceOf(InsufficientBalanceException.class)
            .hasMessageContaining("Insufficient balance for modification");
        
        // Verify no transaction was created
        verify(transactionRepository, never()).save(any(WalletTransaction.class));
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when amount is null")
    void testRecordModificationCharge_NullAmount() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, null, modificationId, bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification charge amount must be positive");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when amount is zero")
    void testRecordModificationCharge_ZeroAmount() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, BigDecimal.ZERO, modificationId, bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification charge amount must be positive");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when amount is negative")
    void testRecordModificationCharge_NegativeAmount() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, new BigDecimal("-50.00"), modificationId, bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification charge amount must be positive");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when modification ID is null")
    void testRecordModificationCharge_NullModificationId() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, new BigDecimal("100.00"), null, bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification ID is required");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when modification ID is empty")
    void testRecordModificationCharge_EmptyModificationId() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, new BigDecimal("100.00"), "  ", bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification ID is required");
    }
    
    @Test
    @DisplayName("Should throw WalletNotFoundException when wallet does not exist")
    void testRecordModificationCharge_WalletNotFound() {
        // Given
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.empty());
        
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationCharge(
            agencyId, new BigDecimal("100.00"), modificationId, bookingId, userId
        ))
            .isInstanceOf(WalletNotFoundException.class)
            .hasMessageContaining("Wallet not found for agency");
    }
    
    // ========== recordModificationRefund Tests ==========
    
    @Test
    @DisplayName("Should successfully record modification refund")
    void testRecordModificationRefund_Success() {
        // Given
        BigDecimal refundAmount = new BigDecimal("75.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        when(walletRepository.save(any(Wallet.class))).thenReturn(wallet);
        
        WalletTransaction savedTransaction = new WalletTransaction();
        savedTransaction.setId(UUID.randomUUID());
        when(transactionRepository.save(any(WalletTransaction.class))).thenReturn(savedTransaction);
        
        // When
        TransactionResult result = walletService.recordModificationRefund(
            agencyId, refundAmount, modificationId, bookingId, userId
        );
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.isSuccess()).isTrue();
        assertThat(result.getBalanceBefore()).isEqualByComparingTo(new BigDecimal("1000.00"));
        assertThat(result.getBalanceAfter()).isEqualByComparingTo(new BigDecimal("1075.00"));
        
        // Verify wallet was credited
        ArgumentCaptor<Wallet> walletCaptor = ArgumentCaptor.forClass(Wallet.class);
        verify(walletRepository).save(walletCaptor.capture());
        assertThat(walletCaptor.getValue().getBalance()).isEqualByComparingTo(new BigDecimal("1075.00"));
        
        // Verify transaction was created with correct details
        ArgumentCaptor<WalletTransaction> transactionCaptor = ArgumentCaptor.forClass(WalletTransaction.class);
        verify(transactionRepository).save(transactionCaptor.capture());
        WalletTransaction transaction = transactionCaptor.getValue();
        
        assertThat(transaction.getTransactionType()).isEqualTo(TransactionType.CREDIT_MODIFICATION);
        assertThat(transaction.getTransactionCategory()).isEqualTo(TransactionCategory.MODIFICATION);
        assertThat(transaction.getAmount()).isEqualByComparingTo(refundAmount);
        assertThat(transaction.getModificationId()).isEqualTo(modificationId);
        assertThat(transaction.getReference()).startsWith("MOD-REFUND-");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when refund amount is invalid")
    void testRecordModificationRefund_InvalidAmount() {
        // When/Then
        assertThatThrownBy(() -> walletService.recordModificationRefund(
            agencyId, BigDecimal.ZERO, modificationId, bookingId, userId
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Modification refund amount must be positive");
    }
    
    // ========== recordModificationCredit Tests ==========
    
    @Test
    @DisplayName("Should successfully record modification credit")
    void testRecordModificationCredit_Success() {
        // Given
        BigDecimal creditAmount = new BigDecimal("200.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        when(walletRepository.save(any(Wallet.class))).thenReturn(wallet);
        
        WalletTransaction savedTransaction = new WalletTransaction();
        savedTransaction.setId(UUID.randomUUID());
        when(transactionRepository.save(any(WalletTransaction.class))).thenReturn(savedTransaction);
        
        // When
        TransactionResult result = walletService.recordModificationCredit(
            agencyId, creditAmount, modificationId, bookingId, userId
        );
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.isSuccess()).isTrue();
        assertThat(result.getBalanceBefore()).isEqualByComparingTo(new BigDecimal("1000.00"));
        assertThat(result.getBalanceAfter()).isEqualByComparingTo(new BigDecimal("1200.00"));
        
        // Verify transaction was created with correct details
        ArgumentCaptor<WalletTransaction> transactionCaptor = ArgumentCaptor.forClass(WalletTransaction.class);
        verify(transactionRepository).save(transactionCaptor.capture());
        WalletTransaction transaction = transactionCaptor.getValue();
        
        assertThat(transaction.getTransactionType()).isEqualTo(TransactionType.CREDIT_MODIFICATION);
        assertThat(transaction.getTransactionCategory()).isEqualTo(TransactionCategory.MODIFICATION);
        assertThat(transaction.getDescription()).contains("fare decrease");
        assertThat(transaction.getReference()).startsWith("MOD-CREDIT-");
    }
    
    // ========== hasSufficientBalance Tests ==========
    
    @Test
    @DisplayName("Should return true when balance is sufficient")
    void testHasSufficientBalance_Sufficient() {
        // Given
        BigDecimal requiredAmount = new BigDecimal("500.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When
        boolean result = walletService.hasSufficientBalance(agencyId, requiredAmount);
        
        // Then
        assertThat(result).isTrue();
    }
    
    @Test
    @DisplayName("Should return true when balance exactly equals required amount")
    void testHasSufficientBalance_ExactMatch() {
        // Given
        BigDecimal requiredAmount = new BigDecimal("1000.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When
        boolean result = walletService.hasSufficientBalance(agencyId, requiredAmount);
        
        // Then
        assertThat(result).isTrue();
    }
    
    @Test
    @DisplayName("Should return false when balance is insufficient")
    void testHasSufficientBalance_Insufficient() {
        // Given
        BigDecimal requiredAmount = new BigDecimal("1500.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When
        boolean result = walletService.hasSufficientBalance(agencyId, requiredAmount);
        
        // Then
        assertThat(result).isFalse();
    }
    
    @Test
    @DisplayName("Should return true when required amount is zero")
    void testHasSufficientBalance_ZeroAmount() {
        // Given
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When
        boolean result = walletService.hasSufficientBalance(agencyId, BigDecimal.ZERO);
        
        // Then
        assertThat(result).isTrue();
    }
    
    @Test
    @DisplayName("Should consider reserved amount when checking balance")
    void testHasSufficientBalance_WithReservedAmount() {
        // Given
        wallet.setReservedAmount(new BigDecimal("300.00"));
        BigDecimal requiredAmount = new BigDecimal("800.00");
        when(walletRepository.findByAgencyId(agencyId)).thenReturn(Optional.of(wallet));
        
        // When
        boolean result = walletService.hasSufficientBalance(agencyId, requiredAmount);
        
        // Then
        assertThat(result).isFalse(); // Available = 1000 - 300 = 700, required = 800
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when amount is negative")
    void testHasSufficientBalance_NegativeAmount() {
        // When/Then
        assertThatThrownBy(() -> walletService.hasSufficientBalance(
            agencyId, new BigDecimal("-100.00")
        ))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Amount must be non-negative");
    }
    
    // ========== rollbackModificationTransaction Tests ==========
    
    @Test
    @DisplayName("Should successfully rollback debit modification transaction")
    void testRollbackModificationTransaction_DebitRollback() {
        // Given
        UUID transactionId = UUID.randomUUID();
        WalletTransaction originalTransaction = new WalletTransaction();
        originalTransaction.setId(transactionId);
        originalTransaction.setWalletId(walletId);
        originalTransaction.setTransactionType(TransactionType.DEBIT_MODIFICATION);
        originalTransaction.setTransactionCategory(TransactionCategory.MODIFICATION);
        originalTransaction.setAmount(new BigDecimal("150.00"));
        originalTransaction.setModificationId(modificationId);
        originalTransaction.setBookingId(bookingId);
        originalTransaction.setCreatedBy(userId);
        
        wallet.setBalance(new BigDecimal("850.00")); // After debit
        
        when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(originalTransaction));
        when(walletRepository.findById(walletId)).thenReturn(Optional.of(wallet));
        when(walletRepository.save(any(Wallet.class))).thenReturn(wallet);
        
        // When
        walletService.rollbackModificationTransaction(transactionId);
        
        // Then
        // Verify wallet was credited back
        ArgumentCaptor<Wallet> walletCaptor = ArgumentCaptor.forClass(Wallet.class);
        verify(walletRepository).save(walletCaptor.capture());
        assertThat(walletCaptor.getValue().getBalance()).isEqualByComparingTo(new BigDecimal("1000.00"));
        
        // Verify rollback transaction was created
        ArgumentCaptor<WalletTransaction> transactionCaptor = ArgumentCaptor.forClass(WalletTransaction.class);
        verify(transactionRepository).save(transactionCaptor.capture());
        WalletTransaction rollbackTransaction = transactionCaptor.getValue();
        
        assertThat(rollbackTransaction.getTransactionType()).isEqualTo(TransactionType.CREDIT_MODIFICATION);
        assertThat(rollbackTransaction.getTransactionCategory()).isEqualTo(TransactionCategory.MODIFICATION);
        assertThat(rollbackTransaction.getAmount()).isEqualByComparingTo(new BigDecimal("150.00"));
        assertThat(rollbackTransaction.getReference()).startsWith("ROLLBACK-");
        assertThat(rollbackTransaction.getDescription()).contains("Rollback of modification transaction");
    }
    
    @Test
    @DisplayName("Should successfully rollback credit modification transaction")
    void testRollbackModificationTransaction_CreditRollback() {
        // Given
        UUID transactionId = UUID.randomUUID();
        WalletTransaction originalTransaction = new WalletTransaction();
        originalTransaction.setId(transactionId);
        originalTransaction.setWalletId(walletId);
        originalTransaction.setTransactionType(TransactionType.CREDIT_MODIFICATION);
        originalTransaction.setTransactionCategory(TransactionCategory.MODIFICATION);
        originalTransaction.setAmount(new BigDecimal("200.00"));
        originalTransaction.setModificationId(modificationId);
        originalTransaction.setBookingId(bookingId);
        originalTransaction.setCreatedBy(userId);
        
        wallet.setBalance(new BigDecimal("1200.00")); // After credit
        
        when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(originalTransaction));
        when(walletRepository.findById(walletId)).thenReturn(Optional.of(wallet));
        when(walletRepository.save(any(Wallet.class))).thenReturn(wallet);
        
        // When
        walletService.rollbackModificationTransaction(transactionId);
        
        // Then
        // Verify wallet was debited back
        ArgumentCaptor<Wallet> walletCaptor = ArgumentCaptor.forClass(Wallet.class);
        verify(walletRepository).save(walletCaptor.capture());
        assertThat(walletCaptor.getValue().getBalance()).isEqualByComparingTo(new BigDecimal("1000.00"));
        
        // Verify rollback transaction was created with opposite type
        ArgumentCaptor<WalletTransaction> transactionCaptor = ArgumentCaptor.forClass(WalletTransaction.class);
        verify(transactionRepository).save(transactionCaptor.capture());
        WalletTransaction rollbackTransaction = transactionCaptor.getValue();
        
        assertThat(rollbackTransaction.getTransactionType()).isEqualTo(TransactionType.DEBIT_MODIFICATION);
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when transaction is not found")
    void testRollbackModificationTransaction_TransactionNotFound() {
        // Given
        UUID transactionId = UUID.randomUUID();
        when(transactionRepository.findById(transactionId)).thenReturn(Optional.empty());
        
        // When/Then
        assertThatThrownBy(() -> walletService.rollbackModificationTransaction(transactionId))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Transaction not found");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when transaction is not a modification transaction")
    void testRollbackModificationTransaction_NotModificationTransaction() {
        // Given
        UUID transactionId = UUID.randomUUID();
        WalletTransaction transaction = new WalletTransaction();
        transaction.setId(transactionId);
        transaction.setTransactionType(TransactionType.DEBIT_TICKET); // Not a modification type
        transaction.setTransactionCategory(TransactionCategory.BOOKING);
        
        when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(transaction));
        
        // When/Then
        assertThatThrownBy(() -> walletService.rollbackModificationTransaction(transactionId))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Transaction is not a modification transaction");
    }
    
    @Test
    @DisplayName("Should throw IllegalArgumentException when transaction category is not MODIFICATION")
    void testRollbackModificationTransaction_WrongCategory() {
        // Given
        UUID transactionId = UUID.randomUUID();
        WalletTransaction transaction = new WalletTransaction();
        transaction.setId(transactionId);
        transaction.setTransactionType(TransactionType.DEBIT_MODIFICATION);
        transaction.setTransactionCategory(TransactionCategory.BOOKING); // Wrong category
        
        when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(transaction));
        
        // When/Then
        assertThatThrownBy(() -> walletService.rollbackModificationTransaction(transactionId))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Transaction category is not MODIFICATION");
    }
}
