package com.walkguide.service;

import com.walkguide.dto.request.LocationUpdateRequest;
import com.walkguide.dto.response.LocationResponse;
import com.walkguide.entity.GeofenceConfig;
import com.walkguide.entity.LocationHistory;
import com.walkguide.entity.PairingRelation;
import com.walkguide.entity.User;
import com.walkguide.enums.PairingStatus;
import com.walkguide.repository.*;
import com.walkguide.websocket.LocationBroadcaster;
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 org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("LocationService Unit Tests")
class LocationServiceTest {

    @Mock LocationHistoryRepository locationHistoryRepository;
    @Mock GeofenceConfigRepository geofenceConfigRepository;
    @Mock PairingRelationRepository pairingRelationRepository;
    @Mock UserRepository userRepository;
    @Mock ActivityLogService activityLogService;
    @Mock FcmService fcmService;
    @Mock LocationBroadcaster locationBroadcaster;

    @InjectMocks LocationService locationService;

    private User guardian;
    private User user;
    private PairingRelation activePairing;
    private LocationHistory sampleLocation;

    @BeforeEach
    void setUp() {
        guardian = User.builder()
                .id(1L).email("guardian@test.com").displayName("Guardian Test")
                .fcmToken("guardian-fcm-token").build();

        user = User.builder()
                .id(2L).email("user@test.com").displayName("User Test")
                .fcmToken("user-fcm-token").build();

        activePairing = PairingRelation.builder()
                .id(1L).guardian(guardian).user(user).status(PairingStatus.ACTIVE).build();

        sampleLocation = LocationHistory.builder()
                .id(100L).userId(2L).lat(-7.257).lng(112.752)
                .accuracy(10.0).speed(1.5).heading(90.0)
                .createdAt(LocalDateTime.now()).build();
    }

    // ===== updateLocation TESTS =====

    @Test
    @DisplayName("updateLocation - harus simpan lokasi dan broadcast via WebSocket")
    void updateLocation_shouldSaveAndBroadcast() {
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.257);
        req.setLng(112.752);
        req.setAccuracy(10.0);
        req.setSpeed(1.5);
        req.setHeading(90.0);

        when(locationHistoryRepository.save(any(LocationHistory.class))).thenReturn(sampleLocation);
        when(geofenceConfigRepository.findByUserId(2L)).thenReturn(Optional.empty());

        LocationResponse result = locationService.updateLocation(2L, req);

        assertThat(result).isNotNull();
        assertThat(result.getLat()).isEqualTo(-7.257);
        assertThat(result.getLng()).isEqualTo(112.752);
        verify(locationHistoryRepository).save(any(LocationHistory.class));
        verify(locationBroadcaster).broadcastLocation(eq(2L), any(LocationResponse.class));
    }

    @Test
    @DisplayName("updateLocation - user dalam geofence: tidak boleh kirim FCM ke guardian")
    void updateLocation_insideGeofence_shouldNotTriggerFcm() {
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.257); // tepat di center
        req.setLng(112.752);

        GeofenceConfig cfg = GeofenceConfig.builder()
                .userId(2L).centerLat(-7.257).centerLng(112.752)
                .radiusMeters(500.0).enabled(true).build();

        when(locationHistoryRepository.save(any())).thenReturn(sampleLocation);
        when(geofenceConfigRepository.findByUserId(2L)).thenReturn(Optional.of(cfg));

        locationService.updateLocation(2L, req);

        verify(fcmService, never()).sendHighPriority(any(), any(), any(), any());
    }

    @Test
    @DisplayName("updateLocation - user keluar geofence: harus kirim FCM ke guardian")
    void updateLocation_outsideGeofence_shouldSendFcmToGuardian() {
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.500); // jauh dari center
        req.setLng(113.000);

        GeofenceConfig cfg = GeofenceConfig.builder()
                .userId(2L).centerLat(-7.257).centerLng(112.752)
                .radiusMeters(100.0).enabled(true).build();

        LocationHistory savedLoc = LocationHistory.builder()
                .id(101L).userId(2L).lat(-7.500).lng(113.000)
                .createdAt(LocalDateTime.now()).build();

        when(locationHistoryRepository.save(any())).thenReturn(savedLoc);
        when(geofenceConfigRepository.findByUserId(2L)).thenReturn(Optional.of(cfg));
        when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        doNothing().when(activityLogService).createLog(any(), any(), any(), any());

        locationService.updateLocation(2L, req);

        verify(fcmService).sendHighPriority(
                eq("guardian-fcm-token"),
                contains("Keluar Area Aman"),
                anyString(),
                anyMap()
        );
    }

    @Test
    @DisplayName("updateLocation - geofence disabled: tidak boleh cek geofence")
    void updateLocation_geofenceDisabled_shouldSkipGeofenceCheck() {
        LocationUpdateRequest req = new LocationUpdateRequest();
        req.setLat(-7.500);
        req.setLng(113.000);

        GeofenceConfig cfg = GeofenceConfig.builder()
                .userId(2L).centerLat(-7.257).centerLng(112.752)
                .radiusMeters(100.0).enabled(false).build(); // disabled

        when(locationHistoryRepository.save(any())).thenReturn(sampleLocation);
        when(geofenceConfigRepository.findByUserId(2L)).thenReturn(Optional.of(cfg));

        locationService.updateLocation(2L, req);

        verify(fcmService, never()).sendHighPriority(any(), any(), any(), any());
    }

    // ===== getLastLocation TESTS =====

    @Test
    @DisplayName("getLastLocation - ada history: harus return Optional berisi lokasi terakhir")
    void getLastLocation_historyExists_shouldReturnLocation() {
        when(locationHistoryRepository.findTopByUserIdOrderByCreatedAtDesc(2L))
                .thenReturn(Optional.of(sampleLocation));

        Optional<LocationResponse> result = locationService.getLastLocation(2L);

        assertThat(result).isPresent();
        assertThat(result.get().getId()).isEqualTo(100L);
        assertThat(result.get().getLat()).isEqualTo(-7.257);
    }

    @Test
    @DisplayName("getLastLocation - belum ada history: harus return Optional kosong")
    void getLastLocation_noHistory_shouldReturnEmpty() {
        when(locationHistoryRepository.findTopByUserIdOrderByCreatedAtDesc(99L))
                .thenReturn(Optional.empty());

        Optional<LocationResponse> result = locationService.getLastLocation(99L);

        assertThat(result).isEmpty();
    }

    // ===== getLastLocationForGuardian TESTS =====

    @Test
    @DisplayName("getLastLocationForGuardian - ada pairing aktif: harus return lokasi user yang dipair")
    void getLastLocationForGuardian_activePairing_shouldReturnUserLocation() {
        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.of(activePairing));
        when(locationHistoryRepository.findTopByUserIdOrderByCreatedAtDesc(2L))
                .thenReturn(Optional.of(sampleLocation));

        Optional<LocationResponse> result = locationService.getLastLocationForGuardian(1L);

        assertThat(result).isPresent();
        assertThat(result.get().getLat()).isEqualTo(-7.257);
    }

    @Test
    @DisplayName("getLastLocationForGuardian - tidak ada pairing aktif: harus return Optional kosong")
    void getLastLocationForGuardian_noPairing_shouldReturnEmpty() {
        when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
                .thenReturn(Optional.empty());

        Optional<LocationResponse> result = locationService.getLastLocationForGuardian(1L);

        assertThat(result).isEmpty();
        verify(locationHistoryRepository, never()).findTopByUserIdOrderByCreatedAtDesc(any());
    }

    // ===== getLocationHistory TESTS =====

    @Test
    @DisplayName("getLocationHistory - harus return halaman lokasi sesuai pageable")
    void getLocationHistory_shouldReturnPagedLocations() {
        Pageable pageable = PageRequest.of(0, 10);
        Page<LocationHistory> page = new PageImpl<>(List.of(sampleLocation), pageable, 1);

        when(locationHistoryRepository.findByUserIdOrderByCreatedAtDesc(2L, pageable)).thenReturn(page);

        Page<LocationResponse> result = locationService.getLocationHistory(2L, pageable);

        assertThat(result).isNotNull();
        assertThat(result.getContent()).hasSize(1);
        assertThat(result.getContent().get(0).getLat()).isEqualTo(-7.257);
    }

    // ===== haversineMeters TESTS =====

    @Test
    @DisplayName("haversineMeters - titik sama: jarak harus 0")
    void haversineMeters_samePoint_shouldReturnZero() {
        double dist = LocationService.haversineMeters(-7.257, 112.752, -7.257, 112.752);
        assertThat(dist).isEqualTo(0.0);
    }

    @Test
    @DisplayName("haversineMeters - dua titik berbeda: harus menghitung jarak yang masuk akal")
    void haversineMeters_differentPoints_shouldReturnPositiveDistance() {
        // Surabaya ke Sidoarjo ~ 20km
        double dist = LocationService.haversineMeters(-7.257, 112.752, -7.447, 112.718);
        assertThat(dist).isGreaterThan(10_000).isLessThan(30_000);
    }

    @Test
    @DisplayName("haversineMeters - jarak simetris: A→B sama dengan B→A")
    void haversineMeters_isSymmetric() {
        double d1 = LocationService.haversineMeters(-7.257, 112.752, -7.300, 112.800);
        double d2 = LocationService.haversineMeters(-7.300, 112.800, -7.257, 112.752);
        assertThat(d1).isCloseTo(d2, within(0.001));
    }
}