package com.flightticket.integration;

import com.flightticket.dto.booking.CreateBookingRequest;
import com.flightticket.dto.flight.FlightSearchRequest;
import com.flightticket.model.entity.*;
import com.flightticket.model.enums.*;
import com.flightticket.repository.*;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MvcResult;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration tests for flight search and booking flows with Amadeus API mocking
 * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6
 */
class FlightAndBookingIntegrationTest extends BaseIntegrationTest {
    
    @Autowired
    private AgencyRepository agencyRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private WalletRepository walletRepository;
    
    @Autowired
    private BookingRepository bookingRepository;
    
    @Autowired
    private BookingPassengerRepository bookingPassengerRepository;
    
    @Autowired
    private BookingSegmentRepository bookingSegmentRepository;
    
    private Agency testAgency;
    private User agentUser;
    private Wallet testWallet;
    
    @BeforeEach
    void setupTestData() {
        bookingSegmentRepository.deleteAll();
        bookingPassengerRepository.deleteAll();
        bookingRepository.deleteAll();
        walletRepository.deleteAll();
        userRepository.deleteAll();
        agencyRepository.deleteAll();
        
        // Create test agency
        testAgency = new Agency();
        testAgency.setId(UUID.randomUUID());
        testAgency.setName("Test Agency");
        testAgency.setMatriculeNumber("FLIGHT001");
        testAgency.setContactEmail("flight@test.com");
        testAgency.setContactPhone("+1234567890");
        testAgency.setStatus(AgencyStatus.ACTIVE);
        testAgency.setIsVerified(true);
        testAgency.setSubscriptionType(SubscriptionType.PAY_AS_YOU_GO);
        testAgency.setSubscriptionStartDate(LocalDate.now());
        testAgency.setNextPaymentDueDate(LocalDate.now().plusMonths(1));
        testAgency.setMarkupPercentage(BigDecimal.valueOf(10.0));
        testAgency.setCreatedAt(LocalDateTime.now());
        testAgency = agencyRepository.save(testAgency);
        
        // Create wallet with sufficient balance
        testWallet = new Wallet();
        testWallet.setId(UUID.randomUUID());
        testWallet.setAgencyId(testAgency.getId());
        testWallet.setBalance(BigDecimal.valueOf(5000.00));
        testWallet.setReservedAmount(BigDecimal.ZERO);
        testWallet.setCurrency("DINAR");
        testWallet.setCreatedAt(LocalDateTime.now());
        testWallet.setUpdatedAt(LocalDateTime.now());
        testWallet = walletRepository.save(testWallet);
        
        // Create agent user
        agentUser = new User();
        agentUser.setId(UUID.randomUUID());
        agentUser.setUsername("agent");
        agentUser.setEmail("agent@test.com");
        agentUser.setAgencyId(testAgency.getId());
        agentUser.setRoles(Set.of(Role.AGENT));
        agentUser.setStatus(UserStatus.ACTIVE);
        agentUser.setCreatedAt(LocalDateTime.now());
        agentUser = userRepository.save(agentUser);
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldSearchFlightsWithMarkupApplied() throws Exception {
        // Given - Mock Amadeus API response
        String amadeusResponse = """
                {
                    "data": [
                        {
                            "id": "OFFER-001",
                            "type": "flight-offer",
                            "price": {
                                "total": "500.00",
                                "base": "400.00",
                                "currency": "USD"
                            },
                            "itineraries": [
                                {
                                    "segments": [
                                        {
                                            "departure": {
                                                "iataCode": "JFK",
                                                "at": "2026-02-01T10:00:00"
                                            },
                                            "arrival": {
                                                "iataCode": "LAX",
                                                "at": "2026-02-01T13:00:00"
                                            },
                                            "carrierCode": "AA",
                                            "number": "100"
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
                """;
        
        wireMockServer.stubFor(WireMock.post(urlPathEqualTo("/v2/shopping/flight-offers"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody(amadeusResponse)));
        
        // When
        FlightSearchRequest request = new FlightSearchRequest();
        request.setOrigin("JFK");
        request.setDestination("LAX");
        request.setDepartureDate(LocalDate.of(2026, 2, 1));
        request.setAdults(1);
        
        mockMvc.perform(post("/api/v1/flights/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.offers").isArray())
                .andExpect(jsonPath("$.offers[0].offerId").exists())
                .andExpect(jsonPath("$.offers[0].price.markup").exists())
                .andExpect(jsonPath("$.offers[0].price.total").exists());
        
        // Then - Verify Amadeus API was called
        wireMockServer.verify(postRequestedFor(urlPathEqualTo("/v2/shopping/flight-offers")));
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldCacheFlightSearchResults() throws Exception {
        // Given - Mock Amadeus API response
        String amadeusResponse = """
                {
                    "data": [
                        {
                            "id": "OFFER-002",
                            "type": "flight-offer",
                            "price": {
                                "total": "600.00",
                                "base": "500.00",
                                "currency": "USD"
                            }
                        }
                    ]
                }
                """;
        
        wireMockServer.stubFor(WireMock.post(urlPathEqualTo("/v2/shopping/flight-offers"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody(amadeusResponse)));
        
        FlightSearchRequest request = new FlightSearchRequest();
        request.setOrigin("JFK");
        request.setDestination("LAX");
        request.setDepartureDate(LocalDate.of(2026, 2, 1));
        request.setAdults(1);
        
        // When - First search
        MvcResult result1 = mockMvc.perform(post("/api/v1/flights/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.searchId").exists())
                .andReturn();
        
        // When - Second search with same parameters (should use cache)
        MvcResult result2 = mockMvc.perform(post("/api/v1/flights/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().isOk())
                .andReturn();
        
        // Then - Verify Amadeus API was called only once (cached on second call)
        wireMockServer.verify(1, postRequestedFor(urlPathEqualTo("/v2/shopping/flight-offers")));
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldGetFareRules() throws Exception {
        // Given - Mock Amadeus fare rules response
        String fareRulesResponse = """
                {
                    "data": {
                        "flightOfferId": "OFFER-001",
                        "fareRules": {
                            "cancellation": {
                                "allowed": true,
                                "penalty": "50.00 USD"
                            },
                            "change": {
                                "allowed": true,
                                "penalty": "75.00 USD"
                            },
                            "baggage": {
                                "checked": "2 pieces",
                                "cabin": "1 piece"
                            }
                        }
                    }
                }
                """;
        
        wireMockServer.stubFor(WireMock.get(urlPathMatching("/v1/shopping/flight-offers/.*/fare-rules"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody(fareRulesResponse)));
        
        // When & Then
        mockMvc.perform(get("/api/v1/flights/fare-rules/OFFER-001"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.cancellation").exists())
                .andExpect(jsonPath("$.change").exists())
                .andExpect(jsonPath("$.baggage").exists());
        
        wireMockServer.verify(getRequestedFor(urlPathMatching("/v1/shopping/flight-offers/.*/fare-rules")));
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldCreateBookingWithWalletValidation() throws Exception {
        // Given - Mock Amadeus PNR creation response
        String pnrResponse = """
                {
                    "data": {
                        "id": "PNR123456",
                        "type": "flight-order",
                        "associatedRecords": [
                            {
                                "reference": "ABC123",
                                "creationDate": "2026-01-19T10:00:00"
                            }
                        ],
                        "flightOffers": [
                            {
                                "price": {
                                    "total": "500.00",
                                    "base": "400.00",
                                    "currency": "USD"
                                }
                            }
                        ]
                    }
                }
                """;
        
        wireMockServer.stubFor(WireMock.post(urlPathEqualTo("/v1/booking/flight-orders"))
                .willReturn(aResponse()
                        .withStatus(201)
                        .withHeader("Content-Type", "application/json")
                        .withBody(pnrResponse)));
        
        // When
        CreateBookingRequest request = new CreateBookingRequest();
        request.setOfferId("OFFER-001");
        request.setPassengers(List.of(createPassengerRequest()));
        
        BigDecimal initialBalance = testWallet.getBalance();
        
        mockMvc.perform(post("/api/v1/bookings")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").exists())
                .andExpect(jsonPath("$.pnr").value("ABC123"))
                .andExpect(jsonPath("$.status").value("CONFIRMED"))
                .andExpect(jsonPath("$.agencyId").value(testAgency.getId().toString()));
        
        // Then - Verify funds were reserved
        Wallet updatedWallet = walletRepository.findById(testWallet.getId()).orElseThrow();
        assertThat(updatedWallet.getReservedAmount()).isGreaterThan(BigDecimal.ZERO);
        
        wireMockServer.verify(postRequestedFor(urlPathEqualTo("/v1/booking/flight-orders")));
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldRejectBookingWithInsufficientBalance() throws Exception {
        // Given - Set wallet balance to insufficient amount
        testWallet.setBalance(BigDecimal.valueOf(10.00));
        walletRepository.save(testWallet);
        
        CreateBookingRequest request = new CreateBookingRequest();
        request.setOfferId("OFFER-001");
        request.setPassengers(List.of(createPassengerRequest()));
        
        // When & Then
        mockMvc.perform(post("/api/v1/bookings")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("Insufficient")));
        
        // Verify Amadeus API was not called
        wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/v1/booking/flight-orders")));
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldReleaseFundsWhenPnrCreationFails() throws Exception {
        // Given - Mock Amadeus API failure
        wireMockServer.stubFor(WireMock.post(urlPathEqualTo("/v1/booking/flight-orders"))
                .willReturn(aResponse()
                        .withStatus(500)
                        .withBody("{\"error\": \"Internal server error\"}")));
        
        CreateBookingRequest request = new CreateBookingRequest();
        request.setOfferId("OFFER-001");
        request.setPassengers(List.of(createPassengerRequest()));
        
        BigDecimal initialBalance = testWallet.getBalance();
        BigDecimal initialReserved = testWallet.getReservedAmount();
        
        // When
        mockMvc.perform(post("/api/v1/bookings")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().is5xxServerError());
        
        // Then - Verify funds were released (balance unchanged)
        Wallet updatedWallet = walletRepository.findById(testWallet.getId()).orElseThrow();
        assertThat(updatedWallet.getBalance()).isEqualByComparingTo(initialBalance);
        assertThat(updatedWallet.getReservedAmount()).isEqualByComparingTo(initialReserved);
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldGetBookingsByAgency() throws Exception {
        // Given - Create test bookings
        Booking booking1 = createBooking("PNR001", BookingStatus.CONFIRMED);
        Booking booking2 = createBooking("PNR002", BookingStatus.TICKETED);
        bookingRepository.saveAll(List.of(booking1, booking2));
        
        // When & Then
        mockMvc.perform(get("/api/v1/bookings/agency/" + testAgency.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[*].pnr").exists());
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldCancelBookingAndReleaseFunds() throws Exception {
        // Given - Create a booking with reserved funds
        Booking booking = createBooking("PNR003", BookingStatus.CONFIRMED);
        booking = bookingRepository.save(booking);
        
        testWallet.setReservedAmount(BigDecimal.valueOf(500.00));
        walletRepository.save(testWallet);
        
        BigDecimal initialReserved = testWallet.getReservedAmount();
        
        // When
        mockMvc.perform(delete("/api/v1/bookings/" + booking.getId() + "/cancel"))
                .andExpect(status().isOk());
        
        // Then - Verify funds were released
        Wallet updatedWallet = walletRepository.findById(testWallet.getId()).orElseThrow();
        assertThat(updatedWallet.getReservedAmount()).isLessThan(initialReserved);
        
        // Verify booking status
        Booking cancelledBooking = bookingRepository.findById(booking.getId()).orElseThrow();
        assertThat(cancelledBooking.getStatus()).isEqualTo(BookingStatus.CANCELLED);
    }
    
    @Test
    @WithMockUser(username = "agent", roles = {"AGENT"})
    void shouldHandleAmadeusApiTimeout() throws Exception {
        // Given - Mock Amadeus API with delay
        wireMockServer.stubFor(WireMock.post(urlPathEqualTo("/v2/shopping/flight-offers"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withFixedDelay(30000) // 30 second delay
                        .withBody("{}")));
        
        FlightSearchRequest request = new FlightSearchRequest();
        request.setOrigin("JFK");
        request.setDestination("LAX");
        request.setDepartureDate(LocalDate.of(2026, 2, 1));
        request.setAdults(1);
        
        // When & Then - Should timeout and return error
        mockMvc.perform(post("/api/v1/flights/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
                .andExpect(status().is5xxServerError());
    }
    
    private CreateBookingRequest.PassengerInfo createPassengerRequest() {
        CreateBookingRequest.PassengerInfo passenger = 
                new CreateBookingRequest.PassengerInfo();
        passenger.setFirstName("John");
        passenger.setLastName("Doe");
        passenger.setDateOfBirth(LocalDate.of(1990, 1, 1));
        passenger.setGender("MALE");
        passenger.setPassportNumber("P123456789");
        passenger.setNationality("US");
        passenger.setPassportExpiry(LocalDate.of(2030, 12, 31));
        return passenger;
    }
    
    private Booking createBooking(String pnr, BookingStatus status) {
        Booking booking = new Booking();
        booking.setId(UUID.randomUUID());
        booking.setPnr(pnr);
        booking.setAgencyId(testAgency.getId());
        booking.setAgentId(agentUser.getId());
        booking.setStatus(status);
        booking.setBaseFare(BigDecimal.valueOf(400.00));
        booking.setTaxes(BigDecimal.valueOf(50.00));
        booking.setMarkup(BigDecimal.valueOf(50.00));
        booking.setTotalPrice(BigDecimal.valueOf(500.00));
        booking.setCurrency("USD");
        booking.setCreatedAt(LocalDateTime.now());
        booking.setExpiresAt(LocalDateTime.now().plusHours(24));
        booking.setUpdatedAt(LocalDateTime.now());
        return booking;
    }
}
