package com.walkguide.service;

import com.walkguide.dto.request.ObstacleLogRequest;
import com.walkguide.dto.response.ObstacleLogResponse;
import com.walkguide.entity.ObstacleLog;
import com.walkguide.entity.User;
import com.walkguide.enums.ActivityLogType;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.ObstacleLogRepository;
import com.walkguide.repository.UserRepository;
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("ObstacleLogService Unit Tests")
class ObstacleLogServiceTest {

    @Mock
    ObstacleLogRepository obstacleLogRepository;

    @Mock
    UserRepository userRepository;

    @Mock
    ActivityLogService activityLogService;

    @InjectMocks
    ObstacleLogService obstacleLogService;

    private User sampleUser;
    private ObstacleLogRequest sampleRequest;

    @BeforeEach
    void setUp() {
        sampleUser = User.builder()
                .id(5L)
                .email("user@test.com")
                .role("ROLE_USER")
                .displayName("Test User")
                .build();

        sampleRequest = new ObstacleLogRequest();
        sampleRequest.setLabel("Sepeda motor");
        sampleRequest.setConfidence(0.92);
        sampleRequest.setDirection("kiri");
        sampleRequest.setEstimatedDist("1.5 meter");
        sampleRequest.setLat(-7.2575);
        sampleRequest.setLng(112.7521);
    }

    // ===== saveObstacle TESTS =====

    @Test
    @DisplayName("saveObstacle - harus simpan log dan return ObstacleLogResponse yang benar")
    void saveObstacle_validRequest_shouldSaveAndReturnResponse() {
        ObstacleLog savedLog = ObstacleLog.builder()
                .id(1L)
                .userId(5L)
                .label("Sepeda motor")
                .confidence(0.92)
                .direction("kiri")
                .estimatedDist("1.5 meter")
                .lat(-7.2575)
                .lng(112.7521)
                .createdAt(LocalDateTime.now())
                .build();

        when(obstacleLogRepository.save(any(ObstacleLog.class))).thenReturn(savedLog);
        when(userRepository.findById(5L)).thenReturn(Optional.of(sampleUser));

        ObstacleLogResponse result = obstacleLogService.saveObstacle(5L, sampleRequest);

        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getLabel()).isEqualTo("Sepeda motor");
        assertThat(result.getConfidence()).isEqualTo(0.92);
        assertThat(result.getDirection()).isEqualTo("kiri");
        assertThat(result.getEstimatedDist()).isEqualTo("1.5 meter");
        assertThat(result.getLat()).isEqualTo(-7.2575);
        assertThat(result.getLng()).isEqualTo(112.7521);
    }

    @Test
    @DisplayName("saveObstacle - harus simpan ObstacleLog dengan data dari request")
    void saveObstacle_shouldPersistCorrectData() {
        when(obstacleLogRepository.save(any(ObstacleLog.class)))
                .thenAnswer(inv -> {
                    ObstacleLog log = inv.getArgument(0);
                    log = ObstacleLog.builder()
                            .id(10L).userId(log.getUserId()).label(log.getLabel())
                            .confidence(log.getConfidence()).direction(log.getDirection())
                            .estimatedDist(log.getEstimatedDist())
                            .lat(log.getLat()).lng(log.getLng()).build();
                    return log;
                });
        when(userRepository.findById(5L)).thenReturn(Optional.of(sampleUser));

        obstacleLogService.saveObstacle(5L, sampleRequest);

        ArgumentCaptor<ObstacleLog> captor = ArgumentCaptor.forClass(ObstacleLog.class);
        verify(obstacleLogRepository).save(captor.capture());

        ObstacleLog captured = captor.getValue();
        assertThat(captured.getUserId()).isEqualTo(5L);
        assertThat(captured.getLabel()).isEqualTo("Sepeda motor");
        assertThat(captured.getConfidence()).isEqualTo(0.92);
        assertThat(captured.getDirection()).isEqualTo("kiri");
    }

    @Test
    @DisplayName("saveObstacle - harus mencatat OBSTACLE_DETECTED ke ActivityLog")
    void saveObstacle_shouldCreateActivityLog() {
        when(obstacleLogRepository.save(any(ObstacleLog.class)))
                .thenReturn(ObstacleLog.builder().id(1L).label("Sepeda motor").direction("kiri").build());
        when(userRepository.findById(5L)).thenReturn(Optional.of(sampleUser));

        obstacleLogService.saveObstacle(5L, sampleRequest);

        verify(activityLogService).createLog(
                eq(sampleUser),
                eq(ActivityLogType.OBSTACLE_DETECTED),
                contains("Sepeda motor"),
                anyString()
        );
    }

    @Test
    @DisplayName("saveObstacle - metadata ActivityLog harus mengandung label, direction, dan confidence")
    void saveObstacle_activityLogMeta_shouldContainObstacleDetails() {
        when(obstacleLogRepository.save(any(ObstacleLog.class)))
                .thenReturn(ObstacleLog.builder().id(1L).label("Lubang").direction("depan").build());
        when(userRepository.findById(5L)).thenReturn(Optional.of(sampleUser));

        sampleRequest.setLabel("Lubang");
        sampleRequest.setDirection("depan");
        sampleRequest.setConfidence(0.85);
        obstacleLogService.saveObstacle(5L, sampleRequest);

        ArgumentCaptor<String> metaCaptor = ArgumentCaptor.forClass(String.class);
        verify(activityLogService).createLog(any(), any(), any(), metaCaptor.capture());

        String meta = metaCaptor.getValue();
        assertThat(meta).contains("Lubang");
        assertThat(meta).contains("depan");
        assertThat(meta).contains("0.85");
    }

    @Test
    @DisplayName("saveObstacle - user tidak ditemukan harus throw ResourceNotFoundException")
    void saveObstacle_userNotFound_shouldThrow() {
        when(obstacleLogRepository.save(any(ObstacleLog.class)))
                .thenReturn(ObstacleLog.builder().id(1L).build());
        when(userRepository.findById(99L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> obstacleLogService.saveObstacle(99L, sampleRequest))
                .isInstanceOf(ResourceNotFoundException.class)
                .hasMessageContaining("User tidak ditemukan");
    }

    // ===== getObstacleLogs TESTS =====

    @Test
    @DisplayName("getObstacleLogs - harus return page dari ObstacleLogResponse")
    void getObstacleLogs_shouldReturnPagedResponse() {
        ObstacleLog log1 = ObstacleLog.builder().id(1L).userId(5L).label("Trotoar").confidence(0.80)
                .direction("kanan").estimatedDist("2 meter").lat(-7.25).lng(112.75).build();
        ObstacleLog log2 = ObstacleLog.builder().id(2L).userId(5L).label("Pohon").confidence(0.95)
                .direction("depan").estimatedDist("1 meter").lat(-7.26).lng(112.76).build();

        Page<ObstacleLog> page = new PageImpl<>(List.of(log1, log2));
        Pageable pageable = PageRequest.of(0, 10);

        when(obstacleLogRepository.findByUserIdOrderByCreatedAtDesc(5L, pageable)).thenReturn(page);

        Page<ObstacleLogResponse> result = obstacleLogService.getObstacleLogs(5L, pageable);

        assertThat(result.getContent()).hasSize(2);
        assertThat(result.getContent().get(0).getLabel()).isEqualTo("Trotoar");
        assertThat(result.getContent().get(1).getLabel()).isEqualTo("Pohon");
    }

    @Test
    @DisplayName("getObstacleLogs - halaman kosong harus return page kosong tanpa error")
    void getObstacleLogs_emptyPage_shouldReturnEmptyPage() {
        Pageable pageable = PageRequest.of(0, 10);
        when(obstacleLogRepository.findByUserIdOrderByCreatedAtDesc(5L, pageable))
                .thenReturn(Page.empty());

        Page<ObstacleLogResponse> result = obstacleLogService.getObstacleLogs(5L, pageable);

        assertThat(result.getContent()).isEmpty();
        assertThat(result.getTotalElements()).isZero();
    }
}