package com.flightticket.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.flightticket.client.amadeus.AmadeusFlightSearchRequest;
import com.flightticket.client.amadeus.AmadeusFlightSearchResponse;
import com.flightticket.client.amadeus.AmadeusModificationRequest;
import com.flightticket.dto.modification.*;
import com.flightticket.exception.AmadeusServiceException;
import com.flightticket.exception.InsufficientBalanceException;
import com.flightticket.exception.ModificationNotAllowedException;
import com.flightticket.model.entity.ModificationRecord;
import com.flightticket.model.entity.Wallet;
import com.flightticket.model.enums.ModificationStatus;
import com.flightticket.model.enums.ModificationType;
import com.flightticket.repository.ModificationRepository;
import com.flightticket.service.ModificationService;
import com.flightticket.service.WalletService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration tests for ModificationController.
 * Tests REST endpoints for booking modification operations.
 * 
 * Requirements: 1.1, 2.1, 3.1, 6.4, 7.4
 */
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class ModificationControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private ModificationService modificationService;
    
    @MockBean
    private ModificationRepository modificationRepository;
    
    @MockBean
    private WalletService walletService;
    
    private UUID testBookingId;
    private UUID testAgentId;
    private UUID testAgencyId;
    private String testModificationId;
    
    @BeforeEach
    void setUp() {
        testBookingId = UUID.randomUUID();
        testAgentId = UUID.randomUUID();
        testAgencyId = UUID.randomUUID();
        testModificationId = "MOD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
    
    // ========== POST /api/modifications/check-eligibility Tests ==========
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldCheckEligibilitySuccessfully() throws Exception {
        // Given
        EligibilityRequest request = EligibilityRequest.builder()
                .bookingId(testBookingId)
                .build();
        
        EligibilityResult result = EligibilityResult.builder()
                .eligible(true)
                .allowedModifications(Arrays.asList(
                        ModificationType.PASSENGER_DETAILS,
                        ModificationType.FLIGHT_DATE,
                        ModificationType.CABIN_CLASS
                ))
                .restrictions(new ArrayList<>())
                .estimatedFees(ModificationFees.builder()
                        .passengerDetailsFee(new BigDecimal("50.00"))
                        .flightDateFee(new BigDecimal("150.00"))
                        .cabinClassFee(new BigDecimal("100.00"))
                        .build())
                .modificationDeadline(LocalDateTime.now().plusDays(2))
                .build();
        
        when(modificationService.checkEligibility(testBookingId)).thenReturn(result);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/check-eligibility")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.eligible").value(true))
                .andExpect(jsonPath("$.allowedModifications", hasSize(3)))
                .andExpect(jsonPath("$.allowedModifications[0]").value("PASSENGER_DETAILS"))
                .andExpect(jsonPath("$.estimatedFees.passengerDetailsFee").value(50.00));
        
        verify(modificationService).checkEligibility(testBookingId);
    }
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldReturnIneligibleWhenBookingNotModifiable() throws Exception {
        // Given
        EligibilityRequest request = EligibilityRequest.builder()
                .bookingId(testBookingId)
                .build();
        
        EligibilityResult result = EligibilityResult.builder()
                .eligible(false)
                .allowedModifications(new ArrayList<>())
                .restrictions(new ArrayList<>())
                .ineligibilityReason("Flight has already departed")
                .build();
        
        when(modificationService.checkEligibility(testBookingId)).thenReturn(result);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/check-eligibility")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.eligible").value(false))
                .andExpect(jsonPath("$.ineligibilityReason").value("Flight has already departed"));
        
        verify(modificationService).checkEligibility(testBookingId);
    }
    
    @Test
    void shouldRejectEligibilityCheckWithoutAuthentication() throws Exception {
        // Given
        EligibilityRequest request = EligibilityRequest.builder()
                .bookingId(testBookingId)
                .build();
        
        // When & Then
        mockMvc.perform(post("/api/modifications/check-eligibility")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized());
        
        verify(modificationService, never()).checkEligibility(any());
    }
    
    // ========== POST /api/modifications/search-alternatives Tests ==========
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldSearchAlternativesSuccessfully() throws Exception {
        // Given
        AmadeusFlightSearchRequest searchRequest = AmadeusFlightSearchRequest.builder()
                .originLocationCode("JFK")
                .destinationLocationCode("LAX")
                .departureDate(LocalDate.now().plusDays(7))
                .adults(1)
                .travelClass("ECONOMY")
                .currencyCode("USD")
                .max(10)
                .build();
        
        AmadeusFlightSearchResponse searchResponse = AmadeusFlightSearchResponse.builder()
                .data(new ArrayList<>())
                .build();
        
        when(modificationService.searchAlternatives(any())).thenReturn(searchResponse);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/search-alternatives")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(searchRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").isArray());
        
        verify(modificationService).searchAlternatives(any());
    }
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldHandleAmadeusApiErrorDuringSearch() throws Exception {
        // Given
        AmadeusFlightSearchRequest searchRequest = AmadeusFlightSearchRequest.builder()
                .originLocationCode("JFK")
                .destinationLocationCode("LAX")
                .departureDate(LocalDate.now().plusDays(7))
                .adults(1)
                .build();
        
        when(modificationService.searchAlternatives(any()))
                .thenThrow(new AmadeusServiceException("API unavailable", 
                        AmadeusServiceException.ErrorType.SERVICE_UNAVAILABLE));
        
        // When & Then
        mockMvc.perform(post("/api/modifications/search-alternatives")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(searchRequest)))
                .andExpect(status().isServiceUnavailable());
        
        verify(modificationService).searchAlternatives(any());
    }
    
    // ========== POST /api/modifications/calculate-cost Tests ==========
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldCalculateCostSuccessfully() throws Exception {
        // Given
        CostCalculationRequest request = CostCalculationRequest.builder()
                .bookingId(testBookingId)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        ModificationCost cost = ModificationCost.builder()
                .modificationFee(new BigDecimal("150.00"))
                .fareDifference(new BigDecimal("50.00"))
                .totalCost(new BigDecimal("200.00"))
                .transactionType(com.flightticket.model.enums.TransactionType.DEBIT_MODIFICATION)
                .currency("USD")
                .breakdown(CostBreakdown.builder()
                        .originalFare(new BigDecimal("400.00"))
                        .newFare(new BigDecimal("450.00"))
                        .airlineModificationFee(new BigDecimal("150.00"))
                        .build())
                .build();
        
        when(modificationService.calculateCost(eq(testBookingId), eq(new BigDecimal("450.00")), eq("FLIGHT_DATE")))
                .thenReturn(cost);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/calculate-cost")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.modificationFee").value(150.00))
                .andExpect(jsonPath("$.fareDifference").value(50.00))
                .andExpect(jsonPath("$.totalCost").value(200.00))
                .andExpect(jsonPath("$.transactionType").value("DEBIT_MODIFICATION"));
        
        verify(modificationService).calculateCost(testBookingId, new BigDecimal("450.00"), "FLIGHT_DATE");
    }
    
    // ========== POST /api/modifications/execute Tests ==========
    
    /**
     * Test successful passenger name correction flow.
     * Requirements: 1.1, 2.1
     */
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldExecutePassengerNameCorrectionSuccessfully() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("400.00"))
                .modificationType("PASSENGER_DETAILS")
                .build();
        
        ModificationRecord record = createModificationRecord(
                testModificationId,
                testBookingId,
                testAgentId,
                testAgencyId,
                ModificationType.PASSENGER_DETAILS,
                ModificationStatus.COMPLETED,
                new BigDecimal("50.00")
        );
        
        Wallet wallet = new Wallet();
        wallet.setBalance(new BigDecimal("950.00"));
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenReturn(record);
        when(walletService.getWalletByAgency(testAgencyId)).thenReturn(wallet);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.modificationId").value(testModificationId))
                .andExpect(jsonPath("$.status").value("COMPLETED"))
                .andExpect(jsonPath("$.newWalletBalance").value(950.00))
                .andExpect(jsonPath("$.message").value("Modification completed successfully"));
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    /**
     * Test successful flight date change flow.
     * Requirements: 3.1
     */
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldExecuteFlightDateChangeSuccessfully() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        ModificationRecord record = createModificationRecord(
                testModificationId,
                testBookingId,
                testAgentId,
                testAgencyId,
                ModificationType.FLIGHT_DATE,
                ModificationStatus.COMPLETED,
                new BigDecimal("200.00")
        );
        
        Wallet wallet = new Wallet();
        wallet.setBalance(new BigDecimal("800.00"));
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenReturn(record);
        when(walletService.getWalletByAgency(testAgencyId)).thenReturn(wallet);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.modificationId").value(testModificationId))
                .andExpect(jsonPath("$.status").value("COMPLETED"));
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    /**
     * Test failed modification due to insufficient balance.
     * Requirements: 6.4
     */
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldRejectModificationWithInsufficientBalance() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenThrow(new InsufficientBalanceException("Insufficient wallet balance"));
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isPaymentRequired());
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    /**
     * Test failed modification due to API error.
     * Requirements: 7.4
     */
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldHandleAmadeusApiErrorDuringExecution() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenThrow(new AmadeusServiceException("API error", 
                        AmadeusServiceException.ErrorType.UNKNOWN));
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadGateway());
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    /**
     * Test multi-segment route modification.
     * Requirements: 4.1
     */
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldExecuteMultiSegmentRouteModificationSuccessfully() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("600.00"))
                .modificationType("FLIGHT_ROUTE")
                .build();
        
        ModificationRecord record = createModificationRecord(
                testModificationId,
                testBookingId,
                testAgentId,
                testAgencyId,
                ModificationType.FLIGHT_ROUTE,
                ModificationStatus.COMPLETED,
                new BigDecimal("250.00")
        );
        
        Wallet wallet = new Wallet();
        wallet.setBalance(new BigDecimal("750.00"));
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenReturn(record);
        when(walletService.getWalletByAgency(testAgencyId)).thenReturn(wallet);
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.modificationId").value(testModificationId))
                .andExpect(jsonPath("$.status").value("COMPLETED"));
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    /**
     * Test authentication and authorization.
     * Requirements: 9.5
     */
    @Test
    void shouldRejectModificationExecutionWithoutAuthentication() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized());
        
        verify(modificationService, never()).executeModification(any(), any(), any(), any(), any());
    }
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldRejectModificationWhenNotAllowed() throws Exception {
        // Given
        AmadeusModificationRequest amadeusRequest = AmadeusModificationRequest.builder()
                .data(AmadeusModificationRequest.ModificationData.builder()
                        .type("flight-order")
                        .id("test-order-id")
                        .build())
                .build();
        
        ModificationExecutionRequest request = ModificationExecutionRequest.builder()
                .bookingId(testBookingId)
                .modificationRequest(amadeusRequest)
                .newFare(new BigDecimal("450.00"))
                .modificationType("FLIGHT_DATE")
                .build();
        
        when(modificationService.executeModification(any(), any(), any(), any(), any()))
                .thenThrow(new ModificationNotAllowedException("Flight has departed"));
        
        // When & Then
        mockMvc.perform(post("/api/modifications/execute")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnprocessableEntity());
        
        verify(modificationService).executeModification(any(), any(), any(), any(), any());
    }
    
    // ========== GET /api/modifications/{modificationId} Tests ==========
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldGetModificationDetailsSuccessfully() throws Exception {
        // Given
        ModificationRecord record = createModificationRecord(
                testModificationId,
                testBookingId,
                testAgentId,
                testAgencyId,
                ModificationType.PASSENGER_DETAILS,
                ModificationStatus.COMPLETED,
                new BigDecimal("50.00")
        );
        
        when(modificationRepository.findByModificationId(testModificationId))
                .thenReturn(Optional.of(record));
        
        // When & Then
        mockMvc.perform(get("/api/modifications/" + testModificationId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.modificationId").value(testModificationId))
                .andExpect(jsonPath("$.bookingId").value(testBookingId.toString()))
                .andExpect(jsonPath("$.type").value("PASSENGER_DETAILS"))
                .andExpect(jsonPath("$.status").value("COMPLETED"));
        
        verify(modificationRepository).findByModificationId(testModificationId);
    }
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldReturn404WhenModificationNotFound() throws Exception {
        // Given
        when(modificationRepository.findByModificationId(testModificationId))
                .thenReturn(Optional.empty());
        
        // When & Then
        mockMvc.perform(get("/api/modifications/" + testModificationId))
                .andExpect(status().isNotFound());
        
        verify(modificationRepository).findByModificationId(testModificationId);
    }
    
    // ========== GET /api/modifications/booking/{bookingId}/history Tests ==========
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldGetModificationHistorySuccessfully() throws Exception {
        // Given
        List<ModificationRecord> records = Arrays.asList(
                createModificationRecord(
                        "MOD-001",
                        testBookingId,
                        testAgentId,
                        testAgencyId,
                        ModificationType.PASSENGER_DETAILS,
                        ModificationStatus.COMPLETED,
                        new BigDecimal("50.00")
                ),
                createModificationRecord(
                        "MOD-002",
                        testBookingId,
                        testAgentId,
                        testAgencyId,
                        ModificationType.FLIGHT_DATE,
                        ModificationStatus.COMPLETED,
                        new BigDecimal("200.00")
                )
        );
        
        when(modificationRepository.findByBookingIdOrderByRequestedAtDesc(testBookingId))
                .thenReturn(records);
        
        // When & Then
        mockMvc.perform(get("/api/modifications/booking/" + testBookingId + "/history"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].modificationId").value("MOD-001"))
                .andExpect(jsonPath("$[1].modificationId").value("MOD-002"));
        
        verify(modificationRepository).findByBookingIdOrderByRequestedAtDesc(testBookingId);
    }
    
    @Test
    @WithMockUser(username = "test-agent-id", roles = "AGENT")
    void shouldReturnEmptyListWhenNoModificationHistory() throws Exception {
        // Given
        when(modificationRepository.findByBookingIdOrderByRequestedAtDesc(testBookingId))
                .thenReturn(new ArrayList<>());
        
        // When & Then
        mockMvc.perform(get("/api/modifications/booking/" + testBookingId + "/history"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(0)));
        
        verify(modificationRepository).findByBookingIdOrderByRequestedAtDesc(testBookingId);
    }
    
    // ========== Helper Methods ==========
    
    private ModificationRecord createModificationRecord(
            String modificationId,
            UUID bookingId,
            UUID agentId,
            UUID agencyId,
            ModificationType type,
            ModificationStatus status,
            BigDecimal cost) {
        
        ModificationRecord record = new ModificationRecord();
        record.setId(UUID.randomUUID());
        record.setModificationId(modificationId);
        record.setBookingId(bookingId);
        record.setAgentId(agentId);
        record.setAgencyId(agencyId);
        record.setType(type);
        record.setStatus(status);
        record.setModificationCost(cost);
        record.setCurrency("USD");
        record.setRequestedAt(LocalDateTime.now());
        record.setCompletedAt(status == ModificationStatus.COMPLETED ? LocalDateTime.now() : null);
        record.setBeforeState(new HashMap<>());
        record.setAfterState(status == ModificationStatus.COMPLETED ? new HashMap<>() : null);
        
        return record;
    }
}
