package com.flightticket.service;

import com.flightticket.client.expedia.ExpediaClient;
import com.flightticket.client.expedia.ExpediaLinkCache;
import com.flightticket.client.expedia.LinkType;
import com.flightticket.dto.expedia.*;
import com.flightticket.dto.wallet.TransactionResult;
import com.flightticket.exception.PriceChangedException;
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.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

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

/**
 * Unit tests for ExpediaBookingService
 * Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.6
 */
@ExtendWith(MockitoExtension.class)
class ExpediaBookingServiceTest {
    
    @Mock
    private ExpediaClient expediaClient;
    
    @Mock
    private ExpediaLinkCache linkCache;
    
    @Mock
    private WalletService walletService;
    
    @InjectMocks
    private ExpediaBookingServiceImpl expediaBookingService;
    
    private ExpediaShopRequest shopRequest;
    private ExpediaShopResponse shopResponse;
    private ExpediaPriceCheckResponse priceCheckResponse;
    private ExpediaHoldResponse holdResponse;
    private ExpediaBookingResponse bookingResponse;
    private ExpediaBookingRequest bookingRequest;
    
    @BeforeEach
    void setUp() {
        // Setup shop request
        shopRequest = ExpediaShopRequest.builder()
            .location("LAX")
            .checkIn(LocalDate.now().plusDays(7))
            .checkOut(LocalDate.now().plusDays(10))
            .adults(2)
            .children(0)
            .currency("USD")
            .build();
        
        // Setup shop response with rooms
        ExpediaLinks shopLinks = ExpediaLinks.builder().build();
        shopLinks.addLink("price_check", ExpediaLink.builder()
            .href("https://api.expedia.com/price-check/123")
            .method("GET")
            .build());
        
        ExpediaShopResponse.ExpediaRoom room = ExpediaShopResponse.ExpediaRoom.builder()
            .roomId("room-123")
            .roomType("Deluxe King")
            .price(ExpediaPrice.builder()
                .total(new BigDecimal("250.00"))
                .currency("USD")
                .build())
            .links(shopLinks)
            .build();
        
        shopResponse = ExpediaShopResponse.builder()
            .rooms(Arrays.asList(room))
            .build();
        
        // Setup price check response (matched)
        ExpediaLinks priceCheckLinks = ExpediaLinks.builder().build();
        priceCheckLinks.addLink("book", ExpediaLink.builder()
            .href("https://api.expedia.com/book/123")
            .method("POST")
            .build());
        
        priceCheckResponse = ExpediaPriceCheckResponse.builder()
            .status("matched")
            .roomId("room-123")
            .originalPrice(ExpediaPrice.builder()
                .total(new BigDecimal("250.00"))
                .currency("USD")
                .build())
            .currentPrice(ExpediaPrice.builder()
                .total(new BigDecimal("250.00"))
                .currency("USD")
                .build())
            .links(priceCheckLinks)
            .build();
        
        // Setup hold response
        ExpediaLinks holdLinks = ExpediaLinks.builder().build();
        holdLinks.addLink("resume", ExpediaLink.builder()
            .href("https://api.expedia.com/resume/123")
            .method("POST")
            .build());
        holdLinks.addLink("cancel", ExpediaLink.builder()
            .href("https://api.expedia.com/cancel/123")
            .method("POST")
            .build());
        
        holdResponse = ExpediaHoldResponse.builder()
            .bookingId("booking-123")
            .status("held")
            .roomId("room-123")
            .price(ExpediaPrice.builder()
                .total(new BigDecimal("250.00"))
                .currency("USD")
                .build())
            .links(holdLinks)
            .build();
        
        // Setup booking response
        bookingResponse = ExpediaBookingResponse.builder()
            .bookingId("booking-123")
            .confirmationNumber("CONF-123456")
            .status("confirmed")
            .build();
        
        // Setup booking request
        bookingRequest = ExpediaBookingRequest.builder()
            .email("test@example.com")
            .phone("+1234567890")
            .build();
    }
    
    @Test
    void searchAvailability_shouldCachePriceCheckLinks() {
        // Arrange
        when(expediaClient.searchAvailability(shopRequest)).thenReturn(shopResponse);
        
        // Act
        List<ExpediaShopResponse.ExpediaRoom> rooms = expediaBookingService.searchAvailability(shopRequest);
        
        // Assert
        assertThat(rooms).isNotNull();
        assertThat(rooms).hasSize(1);
        assertThat(rooms.get(0).getRoomId()).isEqualTo("room-123");
        
        verify(expediaClient).searchAvailability(shopRequest);
        verify(linkCache).storePriceCheckLink(eq("room-123"), eq("https://api.expedia.com/price-check/123"), eq(900L));
    }
    
    @Test
    void validatePrice_shouldCacheBookLinkWhenPriceMatched() throws PriceChangedException {
        // Arrange
        when(linkCache.getLink("room-123", LinkType.PRICE_CHECK))
            .thenReturn("https://api.expedia.com/price-check/123");
        when(expediaClient.priceCheck("https://api.expedia.com/price-check/123"))
            .thenReturn(priceCheckResponse);
        
        // Act
        ExpediaPriceCheckResponse result = expediaBookingService.validatePrice("room-123");
        
        // Assert
        assertThat(result).isNotNull();
        assertThat(result.getStatus()).isEqualTo("matched");
        
        verify(linkCache).getLink("room-123", LinkType.PRICE_CHECK);
        verify(expediaClient).priceCheck("https://api.expedia.com/price-check/123");
        verify(linkCache).storeBookLink(eq("room-123"), eq("https://api.expedia.com/book/123"), eq(900L));
    }
    
    @Test
    void validatePrice_shouldThrowPriceChangedExceptionWhenPriceChanged() {
        // Arrange
        ExpediaPriceCheckResponse changedPriceResponse = ExpediaPriceCheckResponse.builder()
            .status("price_changed")
            .roomId("room-123")
            .originalPrice(ExpediaPrice.builder()
                .total(new BigDecimal("250.00"))
                .currency("USD")
                .build())
            .currentPrice(ExpediaPrice.builder()
                .total(new BigDecimal("275.00"))
                .currency("USD")
                .build())
            .build();
        
        when(linkCache.getLink("room-123", LinkType.PRICE_CHECK))
            .thenReturn("https://api.expedia.com/price-check/123");
        when(expediaClient.priceCheck("https://api.expedia.com/price-check/123"))
            .thenReturn(changedPriceResponse);
        
        // Act & Assert
        assertThatThrownBy(() -> expediaBookingService.validatePrice("room-123"))
            .isInstanceOf(PriceChangedException.class)
            .hasMessageContaining("250.00")
            .hasMessageContaining("275.00");
        
        verify(linkCache).getLink("room-123", LinkType.PRICE_CHECK);
        verify(expediaClient).priceCheck("https://api.expedia.com/price-check/123");
        verify(linkCache, never()).storeBookLink(anyString(), anyString(), anyLong());
    }
    
    @Test
    void bookRoom_shouldCompleteSuccessfulBookingFlow() {
        // Arrange
        UUID agencyId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        
        when(linkCache.getLink("room-123", LinkType.BOOK))
            .thenReturn("https://api.expedia.com/book/123");
        when(expediaClient.createHoldBooking("https://api.expedia.com/book/123", bookingRequest))
            .thenReturn(holdResponse);
        when(walletService.reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123")))
            .thenReturn(new TransactionResult(true, UUID.randomUUID(), new BigDecimal("1000.00"), new BigDecimal("750.00"), "Success"));
        when(walletService.debitWallet(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"), eq(userId)))
            .thenReturn(new TransactionResult(true, UUID.randomUUID(), new BigDecimal("1000.00"), new BigDecimal("750.00"), "Success"));
        when(linkCache.getLink("booking-123", LinkType.RESUME))
            .thenReturn("https://api.expedia.com/resume/123");
        when(expediaClient.resumeBooking("https://api.expedia.com/resume/123"))
            .thenReturn(bookingResponse);
        
        // Act
        ExpediaBookingResponse result = expediaBookingService.bookRoom("room-123", bookingRequest, agencyId, userId);
        
        // Assert
        assertThat(result).isNotNull();
        assertThat(result.getConfirmationNumber()).isEqualTo("CONF-123456");
        assertThat(result.getStatus()).isEqualTo("confirmed");
        
        verify(linkCache).getLink("room-123", LinkType.BOOK);
        verify(expediaClient).createHoldBooking("https://api.expedia.com/book/123", bookingRequest);
        verify(linkCache).storeResumeLink(eq("booking-123"), eq("https://api.expedia.com/resume/123"), eq(1800L));
        verify(linkCache).storeCancelLink(eq("booking-123"), eq("https://api.expedia.com/cancel/123"), eq(1800L));
        verify(walletService).reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"));
        verify(walletService).debitWallet(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"), eq(userId));
        verify(expediaClient).resumeBooking("https://api.expedia.com/resume/123");
        verify(linkCache).cleanupLinks("booking-123");
    }
    
    @Test
    void bookRoom_shouldCancelHoldWhenPaymentFails() {
        // Arrange
        UUID agencyId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        
        when(linkCache.getLink("room-123", LinkType.BOOK))
            .thenReturn("https://api.expedia.com/book/123");
        when(expediaClient.createHoldBooking("https://api.expedia.com/book/123", bookingRequest))
            .thenReturn(holdResponse);
        when(walletService.reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123")))
            .thenReturn(new TransactionResult(true, UUID.randomUUID(), new BigDecimal("1000.00"), new BigDecimal("750.00"), "Success"));
        when(walletService.debitWallet(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"), eq(userId)))
            .thenReturn(new TransactionResult(false, null, new BigDecimal("1000.00"), new BigDecimal("1000.00"), "Insufficient funds"));
        when(linkCache.getLink("booking-123", LinkType.CANCEL))
            .thenReturn("https://api.expedia.com/cancel/123");
        
        // Act & Assert
        assertThatThrownBy(() -> expediaBookingService.bookRoom("room-123", bookingRequest, agencyId, userId))
            .isInstanceOf(RuntimeException.class)
            .hasMessageContaining("Payment failed");
        
        verify(linkCache).getLink("room-123", LinkType.BOOK);
        verify(expediaClient).createHoldBooking("https://api.expedia.com/book/123", bookingRequest);
        verify(walletService).reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"));
        verify(walletService).debitWallet(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"), eq(userId));
        verify(expediaClient, times(2)).cancelHoldBooking("https://api.expedia.com/cancel/123"); // Called twice: once in main flow, once in catch block
        verify(expediaClient, never()).resumeBooking(anyString());
        verify(linkCache).cleanupLinks("booking-123");
    }
    
    @Test
    void bookRoom_shouldCancelHoldWhenReserveFundsFails() {
        // Arrange
        UUID agencyId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        
        when(linkCache.getLink("room-123", LinkType.BOOK))
            .thenReturn("https://api.expedia.com/book/123");
        when(expediaClient.createHoldBooking("https://api.expedia.com/book/123", bookingRequest))
            .thenReturn(holdResponse);
        when(walletService.reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123")))
            .thenReturn(new TransactionResult(false, null, new BigDecimal("1000.00"), new BigDecimal("1000.00"), "Insufficient balance"));
        when(linkCache.getLink("booking-123", LinkType.CANCEL))
            .thenReturn("https://api.expedia.com/cancel/123");
        
        // Act & Assert
        assertThatThrownBy(() -> expediaBookingService.bookRoom("room-123", bookingRequest, agencyId, userId))
            .isInstanceOf(RuntimeException.class)
            .hasMessageContaining("Failed to reserve funds");
        
        verify(linkCache).getLink("room-123", LinkType.BOOK);
        verify(expediaClient).createHoldBooking("https://api.expedia.com/book/123", bookingRequest);
        verify(walletService).reserveFunds(eq(agencyId), eq(new BigDecimal("250.00")), eq("booking-123"));
        verify(walletService, never()).debitWallet(any(), any(), any(), any());
        verify(expediaClient, times(2)).cancelHoldBooking("https://api.expedia.com/cancel/123"); // Called twice: once in main flow, once in catch block
        verify(expediaClient, never()).resumeBooking(anyString());
        verify(linkCache).cleanupLinks("booking-123");
    }
}
