package com.flightticket.service;

import com.flightticket.model.entity.PricingConfig;
import com.flightticket.model.enums.RequestType;
import com.flightticket.repository.PricingConfigRepository;
import com.flightticket.service.impl.PricingConfigServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
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 PricingConfigService.
 * Tests pricing configuration management with global defaults and client-specific overrides.
 * 
 * Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 7.4
 */
@ExtendWith(MockitoExtension.class)
class PricingConfigServiceTest {
    
    @Mock
    private PricingConfigRepository pricingConfigRepository;
    
    @InjectMocks
    private PricingConfigServiceImpl pricingConfigService;
    
    private UUID testClientId;
    private RequestType testRequestType;
    private String testCurrency;
    private BigDecimal testPrice;
    
    @BeforeEach
    void setUp() {
        testClientId = UUID.randomUUID();
        testRequestType = RequestType.FLIGHT_SEARCH;
        testCurrency = "USD";
        testPrice = new BigDecimal("0.002");
    }
    
    // ========== getPrice() Tests ==========
    
    @Test
    void shouldReturnClientSpecificPriceWhenExists() {
        // Given
        PricingConfig clientConfig = createPricingConfig(testClientId, testRequestType, testPrice, testCurrency);
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.of(clientConfig));
        
        // When
        BigDecimal price = pricingConfigService.getPrice(testClientId, testRequestType, testCurrency);
        
        // Then
        assertThat(price).isEqualTo(testPrice);
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository, never()).findByClientIdIsNullAndRequestTypeAndCurrency(any(), any());
    }
    
    @Test
    void shouldReturnGlobalPriceWhenClientSpecificDoesNotExist() {
        // Given
        PricingConfig globalConfig = createPricingConfig(null, testRequestType, testPrice, testCurrency);
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.empty());
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency))
            .thenReturn(Optional.of(globalConfig));
        
        // When
        BigDecimal price = pricingConfigService.getPrice(testClientId, testRequestType, testCurrency);
        
        // Then
        assertThat(price).isEqualTo(testPrice);
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository).findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency);
    }
    
    @Test
    void shouldReturnZeroAndLogWarningWhenNoPricingExists() {
        // Given
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.empty());
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency))
            .thenReturn(Optional.empty());
        
        // When
        BigDecimal price = pricingConfigService.getPrice(testClientId, testRequestType, testCurrency);
        
        // Then
        assertThat(price).isEqualTo(BigDecimal.ZERO);
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository).findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency);
    }
    
    @Test
    void shouldPreferClientSpecificOverGlobalPrice() {
        // Given
        BigDecimal clientPrice = new BigDecimal("0.005");
        BigDecimal globalPrice = new BigDecimal("0.002");
        
        PricingConfig clientConfig = createPricingConfig(testClientId, testRequestType, clientPrice, testCurrency);
        PricingConfig globalConfig = createPricingConfig(null, testRequestType, globalPrice, testCurrency);
        
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.of(clientConfig));
        
        // When
        BigDecimal price = pricingConfigService.getPrice(testClientId, testRequestType, testCurrency);
        
        // Then
        assertThat(price).isEqualTo(clientPrice);
        assertThat(price).isNotEqualTo(globalPrice);
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository, never()).findByClientIdIsNullAndRequestTypeAndCurrency(any(), any());
    }
    
    // ========== setGlobalPrice() Tests ==========
    
    @Test
    void shouldCreateNewGlobalPriceWhenNotExists() {
        // Given
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency))
            .thenReturn(Optional.empty());
        
        PricingConfig savedConfig = createPricingConfig(null, testRequestType, testPrice, testCurrency);
        when(pricingConfigRepository.save(any(PricingConfig.class))).thenReturn(savedConfig);
        
        // When
        PricingConfig result = pricingConfigService.setGlobalPrice(testRequestType, testPrice, testCurrency);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getClientId()).isNull();
        assertThat(result.getRequestType()).isEqualTo(testRequestType);
        assertThat(result.getUnitPrice()).isEqualTo(testPrice);
        assertThat(result.getCurrency()).isEqualTo(testCurrency);
        
        verify(pricingConfigRepository).findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency);
        verify(pricingConfigRepository).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldUpdateExistingGlobalPrice() {
        // Given
        BigDecimal newPrice = new BigDecimal("0.003");
        PricingConfig existingConfig = createPricingConfig(null, testRequestType, testPrice, testCurrency);
        
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency))
            .thenReturn(Optional.of(existingConfig));
        when(pricingConfigRepository.save(any(PricingConfig.class))).thenReturn(existingConfig);
        
        // When
        PricingConfig result = pricingConfigService.setGlobalPrice(testRequestType, newPrice, testCurrency);
        
        // Then
        assertThat(result).isNotNull();
        verify(pricingConfigRepository).findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, testCurrency);
        verify(pricingConfigRepository).save(argThat(config -> 
            config.getUnitPrice().equals(newPrice) && config.getClientId() == null
        ));
    }
    
    @Test
    void shouldRejectNegativePriceForGlobalPricing() {
        // Given
        BigDecimal negativePrice = new BigDecimal("-0.002");
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, negativePrice, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("must be positive");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectZeroPriceForGlobalPricing() {
        // Given
        BigDecimal zeroPrice = BigDecimal.ZERO;
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, zeroPrice, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("must be positive");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectNullPriceForGlobalPricing() {
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, null, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("cannot be null");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectInvalidCurrencyCodeForGlobalPricing() {
        // Given - invalid currency codes
        String invalidCurrency1 = "ZZZ"; // Not a real currency
        String invalidCurrency2 = "US";  // Too short
        String invalidCurrency3 = "USDD"; // Too long
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, testPrice, invalidCurrency1))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Invalid currency code");
        
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, testPrice, invalidCurrency2))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Invalid currency code");
        
        assertThatThrownBy(() -> pricingConfigService.setGlobalPrice(testRequestType, testPrice, invalidCurrency3))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Invalid currency code");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldAcceptValidIsoCurrencyCodesForGlobalPricing() {
        // Given - valid ISO 4217 currency codes
        String[] validCurrencies = {"USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD"};
        
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(any(), any()))
            .thenReturn(Optional.empty());
        when(pricingConfigRepository.save(any(PricingConfig.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));
        
        // When & Then - should not throw for valid currencies
        for (String currency : validCurrencies) {
            assertThatCode(() -> pricingConfigService.setGlobalPrice(testRequestType, testPrice, currency))
                .doesNotThrowAnyException();
        }
    }
    
    // ========== setClientPrice() Tests ==========
    
    @Test
    void shouldCreateNewClientSpecificPriceWhenNotExists() {
        // Given
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.empty());
        
        PricingConfig savedConfig = createPricingConfig(testClientId, testRequestType, testPrice, testCurrency);
        when(pricingConfigRepository.save(any(PricingConfig.class))).thenReturn(savedConfig);
        
        // When
        PricingConfig result = pricingConfigService.setClientPrice(testClientId, testRequestType, testPrice, testCurrency);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getClientId()).isEqualTo(testClientId);
        assertThat(result.getRequestType()).isEqualTo(testRequestType);
        assertThat(result.getUnitPrice()).isEqualTo(testPrice);
        assertThat(result.getCurrency()).isEqualTo(testCurrency);
        
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldUpdateExistingClientSpecificPrice() {
        // Given
        BigDecimal newPrice = new BigDecimal("0.005");
        PricingConfig existingConfig = createPricingConfig(testClientId, testRequestType, testPrice, testCurrency);
        
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency))
            .thenReturn(Optional.of(existingConfig));
        when(pricingConfigRepository.save(any(PricingConfig.class))).thenReturn(existingConfig);
        
        // When
        PricingConfig result = pricingConfigService.setClientPrice(testClientId, testRequestType, newPrice, testCurrency);
        
        // Then
        assertThat(result).isNotNull();
        verify(pricingConfigRepository).findByClientIdAndRequestTypeAndCurrency(testClientId, testRequestType, testCurrency);
        verify(pricingConfigRepository).save(argThat(config -> 
            config.getUnitPrice().equals(newPrice) && config.getClientId().equals(testClientId)
        ));
    }
    
    @Test
    void shouldRejectNegativePriceForClientPricing() {
        // Given
        BigDecimal negativePrice = new BigDecimal("-0.005");
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, negativePrice, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("must be positive");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectZeroPriceForClientPricing() {
        // Given
        BigDecimal zeroPrice = BigDecimal.ZERO;
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, zeroPrice, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("must be positive");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectNullPriceForClientPricing() {
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, null, testCurrency))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("cannot be null");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldRejectInvalidCurrencyCodeForClientPricing() {
        // Given - invalid currency codes
        String invalidCurrency1 = "ZZZ"; // Not a real currency
        String invalidCurrency2 = "12$"; // Contains special characters
        
        // When & Then
        assertThatThrownBy(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, testPrice, invalidCurrency1))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Invalid currency code");
        
        assertThatThrownBy(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, testPrice, invalidCurrency2))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Invalid currency code");
        
        verify(pricingConfigRepository, never()).save(any(PricingConfig.class));
    }
    
    @Test
    void shouldAcceptValidIsoCurrencyCodesForClientPricing() {
        // Given - valid ISO 4217 currency codes
        String[] validCurrencies = {"USD", "EUR", "GBP", "JPY", "CHF"};
        
        when(pricingConfigRepository.findByClientIdAndRequestTypeAndCurrency(any(), any(), any()))
            .thenReturn(Optional.empty());
        when(pricingConfigRepository.save(any(PricingConfig.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));
        
        // When & Then - should not throw for valid currencies
        for (String currency : validCurrencies) {
            assertThatCode(() -> pricingConfigService.setClientPrice(testClientId, testRequestType, testPrice, currency))
                .doesNotThrowAnyException();
        }
    }
    
    // ========== getAllGlobalPrices() Tests ==========
    
    @Test
    void shouldReturnAllGlobalPrices() {
        // Given
        List<PricingConfig> globalPrices = Arrays.asList(
            createPricingConfig(null, RequestType.FLIGHT_SEARCH, new BigDecimal("0.002"), "USD"),
            createPricingConfig(null, RequestType.PRICE_CHECK, new BigDecimal("0.01"), "USD"),
            createPricingConfig(null, RequestType.PNR_CREATION, new BigDecimal("2.50"), "USD")
        );
        
        when(pricingConfigRepository.findByClientIdIsNull()).thenReturn(globalPrices);
        
        // When
        List<PricingConfig> result = pricingConfigService.getAllGlobalPrices();
        
        // Then
        assertThat(result).hasSize(3);
        assertThat(result).allMatch(config -> config.getClientId() == null);
        verify(pricingConfigRepository).findByClientIdIsNull();
    }
    
    @Test
    void shouldReturnEmptyListWhenNoGlobalPricesExist() {
        // Given
        when(pricingConfigRepository.findByClientIdIsNull()).thenReturn(List.of());
        
        // When
        List<PricingConfig> result = pricingConfigService.getAllGlobalPrices();
        
        // Then
        assertThat(result).isEmpty();
        verify(pricingConfigRepository).findByClientIdIsNull();
    }
    
    // ========== getClientPrices() Tests ==========
    
    @Test
    void shouldReturnAllClientSpecificPrices() {
        // Given
        List<PricingConfig> clientPrices = Arrays.asList(
            createPricingConfig(testClientId, RequestType.FLIGHT_SEARCH, new BigDecimal("0.003"), "USD"),
            createPricingConfig(testClientId, RequestType.PRICE_CHECK, new BigDecimal("0.015"), "USD")
        );
        
        when(pricingConfigRepository.findByClientId(testClientId)).thenReturn(clientPrices);
        
        // When
        List<PricingConfig> result = pricingConfigService.getClientPrices(testClientId);
        
        // Then
        assertThat(result).hasSize(2);
        assertThat(result).allMatch(config -> config.getClientId().equals(testClientId));
        verify(pricingConfigRepository).findByClientId(testClientId);
    }
    
    @Test
    void shouldReturnEmptyListWhenNoClientSpecificPricesExist() {
        // Given
        when(pricingConfigRepository.findByClientId(testClientId)).thenReturn(List.of());
        
        // When
        List<PricingConfig> result = pricingConfigService.getClientPrices(testClientId);
        
        // Then
        assertThat(result).isEmpty();
        verify(pricingConfigRepository).findByClientId(testClientId);
    }
    
    // ========== Edge Cases and Integration Scenarios ==========
    
    @Test
    void shouldHandleMultipleCurrenciesForSameRequestType() {
        // Given
        String usdCurrency = "USD";
        String eurCurrency = "EUR";
        BigDecimal usdPrice = new BigDecimal("0.002");
        BigDecimal eurPrice = new BigDecimal("0.0018");
        
        PricingConfig usdConfig = createPricingConfig(null, testRequestType, usdPrice, usdCurrency);
        PricingConfig eurConfig = createPricingConfig(null, testRequestType, eurPrice, eurCurrency);
        
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, usdCurrency))
            .thenReturn(Optional.of(usdConfig));
        when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(testRequestType, eurCurrency))
            .thenReturn(Optional.of(eurConfig));
        
        // When
        BigDecimal usdResult = pricingConfigService.getPrice(testClientId, testRequestType, usdCurrency);
        BigDecimal eurResult = pricingConfigService.getPrice(testClientId, testRequestType, eurCurrency);
        
        // Then
        assertThat(usdResult).isEqualTo(usdPrice);
        assertThat(eurResult).isEqualTo(eurPrice);
    }
    
    @Test
    void shouldHandleAllRequestTypes() {
        // Test that service works with all RequestType enum values
        for (RequestType requestType : RequestType.values()) {
            // Given
            PricingConfig config = createPricingConfig(null, requestType, testPrice, testCurrency);
            when(pricingConfigRepository.findByClientIdIsNullAndRequestTypeAndCurrency(requestType, testCurrency))
                .thenReturn(Optional.of(config));
            
            // When
            BigDecimal price = pricingConfigService.getPrice(testClientId, requestType, testCurrency);
            
            // Then
            assertThat(price).isEqualTo(testPrice);
        }
    }
    
    // ========== Helper Methods ==========
    
    private PricingConfig createPricingConfig(UUID clientId, RequestType requestType, BigDecimal price, String currency) {
        PricingConfig config = new PricingConfig();
        config.setId(UUID.randomUUID());
        config.setClientId(clientId);
        config.setRequestType(requestType);
        config.setUnitPrice(price);
        config.setCurrency(currency);
        config.setCreatedAt(Instant.now());
        config.setUpdatedAt(Instant.now());
        return config;
    }
}
