package com.walkguide.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.walkguide.dto.ApiResponse;
import com.walkguide.dto.request.*;
import com.walkguide.dto.response.*;
import com.walkguide.entity.User;
import com.walkguide.enums.ActivityLogType;
import com.walkguide.repository.UserRepository;
import com.walkguide.security.SecurityHelper;
import com.walkguide.service.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.walkguide.security.JwtAuthFilter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Optional;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@AutoConfigureMockMvc(addFilters = false)
@WebMvcTest(UserController.class)
@WithMockUser(username = "1", roles = "USER")
@DisplayName("UserController Unit Tests")
class UserControllerTest {

    @MockBean private JwtAuthFilter jwtAuthFilter;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean private LocationService locationService;
    @MockBean private ObstacleLogService obstacleLogService;
    @MockBean private SosService sosService;
    @MockBean private ActivityLogService activityLogService;
    @MockBean private NotificationService notificationService;
    @MockBean private UserSettingsService userSettingsService;
    @MockBean private AiConfigService aiConfigService;
    @MockBean private VoiceCommandService voiceCommandService;
    @MockBean private HardwareShortcutService hardwareShortcutService;
    @MockBean private UserRepository userRepository;

    private User sampleUser;

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

    // ===== PROFILE =====

    @Test
    @DisplayName("GET /api/v1/user/profile - harus return profil user")
    void getProfile_shouldReturn200WithProfile() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));

            mockMvc.perform(get("/api/v1/user/profile"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.data.email").value("user@test.com"))
                    .andExpect(jsonPath("$.data.uniqueUserId").value("ABC123DEF456"));
        }
    }

    @Test
    @DisplayName("GET /api/v1/user/profile - user tidak ditemukan harus return 500")
    void getProfile_userNotFound_shouldReturn500() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(99L);
            when(userRepository.findById(99L)).thenReturn(Optional.empty());

            mockMvc.perform(get("/api/v1/user/profile"))
                    .andExpect(status().isInternalServerError());
        }
    }

    // ===== SETTINGS =====

    @Test
    @DisplayName("GET /api/v1/user/settings - harus return settings user")
    void getSettings_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            UserSettingsResponse settings = new UserSettingsResponse();
            when(userSettingsService.getSettings(1L)).thenReturn(settings);

            mockMvc.perform(get("/api/v1/user/settings"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Settings user"));
        }
    }

    @Test
    @DisplayName("PUT /api/v1/user/settings - harus update dan return settings baru")
    void updateSettings_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
            UserSettingsResponse updated = new UserSettingsResponse();
            when(userSettingsService.updateSettings(eq(1L), any())).thenReturn(updated);

            mockMvc.perform(put("/api/v1/user/settings")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Settings diperbarui"));

            verify(userSettingsService).updateSettings(eq(1L), any(UserSettingsUpdateRequest.class));
        }
    }

    // ===== VOICE COMMANDS =====

    @Test
    @DisplayName("GET /api/v1/user/voice-commands - harus return list voice commands")
    void getVoiceCommands_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            VoiceCommandResponse cmd = new VoiceCommandResponse();
            when(voiceCommandService.getAll(1L)).thenReturn(List.of(cmd));

            mockMvc.perform(get("/api/v1/user/voice-commands"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.data").isArray());
        }
    }

    // ===== SHORTCUTS =====

    @Test
    @DisplayName("GET /api/v1/user/shortcuts - harus return list shortcuts")
    void getShortcuts_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(hardwareShortcutService.getAll(1L)).thenReturn(List.of());

            mockMvc.perform(get("/api/v1/user/shortcuts"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true));
        }
    }

    @Test
    @DisplayName("PUT /api/v1/user/shortcuts - harus update shortcut")
    void updateShortcut_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            HardwareShortcutUpdateRequest req = new HardwareShortcutUpdateRequest();
            HardwareShortcutResponse resp = new HardwareShortcutResponse();
            when(hardwareShortcutService.update(eq(1L), any())).thenReturn(resp);

            mockMvc.perform(put("/api/v1/user/shortcuts")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Shortcut diperbarui"));
        }
    }

    // ===== AI CONFIG =====

    @Test
    @DisplayName("GET /api/v1/user/ai-config - harus return AI config")
    void getAiConfig_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(aiConfigService.getConfig(1L)).thenReturn(new AiConfigResponse());

            mockMvc.perform(get("/api/v1/user/ai-config"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("AI config"));
        }
    }

    // ===== LOCATION =====

    @Test
    @DisplayName("POST /api/v1/user/location - harus update lokasi dan return LocationResponse")
    void updateLocation_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            LocationUpdateRequest req = new LocationUpdateRequest();
            req.setLat(-7.2575);
            req.setLng(112.7521);

            LocationResponse resp = new LocationResponse();
            when(locationService.updateLocation(eq(1L), any())).thenReturn(resp);

            mockMvc.perform(post("/api/v1/user/location")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("Lokasi diperbarui"));

            verify(locationService).updateLocation(eq(1L), any(LocationUpdateRequest.class));
        }
    }

    // ===== OBSTACLE =====

    @Test
    @DisplayName("POST /api/v1/user/obstacle - harus mencatat obstacle dan return response")
    void logObstacle_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            ObstacleLogRequest req = new ObstacleLogRequest();
            ObstacleLogResponse resp = new ObstacleLogResponse();
            when(obstacleLogService.saveObstacle(eq(1L), any())).thenReturn(resp);

            mockMvc.perform(post("/api/v1/user/obstacle")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Obstacle dicatat"));
        }
    }

    // ===== SOS =====

    @Test
    @DisplayName("POST /api/v1/user/sos - harus trigger SOS dan return SosEventResponse")
    void triggerSos_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            SosRequest req = new SosRequest();
            SosEventResponse resp = new SosEventResponse();
            when(sosService.triggerSos(eq(1L), any())).thenReturn(resp);

            mockMvc.perform(post("/api/v1/user/sos")
                            .with(csrf())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.message").value("SOS dikirim! Guardian sudah diberitahu."));

            verify(sosService).triggerSos(eq(1L), any(SosRequest.class));
        }
    }

    // ===== ACTIVITY LOGS =====

    @Test
    @DisplayName("GET /api/v1/user/activity-logs - harus return paginated activity logs")
    void getActivityLogs_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            Page<ActivityLogResponse> page = new PageImpl<>(List.of());
            when(activityLogService.getLogs(eq(1L), any(PageRequest.class))).thenReturn(page);

            mockMvc.perform(get("/api/v1/user/activity-logs")
                            .param("page", "0")
                            .param("size", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Log aktivitas"));
        }
    }

    // ===== NOTIFICATIONS =====

    @Test
    @DisplayName("GET /api/v1/user/notifications - harus return paginated notifikasi")
    void getNotifications_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);

            Page<NotificationResponse> page = new PageImpl<>(List.of());
            when(notificationService.getNotifications(eq(1L), any())).thenReturn(page);

            mockMvc.perform(get("/api/v1/user/notifications"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Notifikasi"));
        }
    }

    @Test
    @DisplayName("GET /api/v1/user/notifications/unread-count - harus return jumlah belum dibaca")
    void getUnreadCount_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(notificationService.getUnreadCount(1L)).thenReturn(5L);

            mockMvc.perform(get("/api/v1/user/notifications/unread-count"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").value(5))
                    .andExpect(jsonPath("$.message").value("Jumlah notifikasi belum dibaca"));
        }
    }

    @Test
    @DisplayName("PUT /api/v1/user/notifications/mark-all-read - harus mark semua sebagai dibaca")
    void markAllRead_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            doNothing().when(notificationService).markAllRead(1L);

            mockMvc.perform(put("/api/v1/user/notifications/mark-all-read")
                            .with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("Semua notifikasi ditandai sudah dibaca"));
        }
    }

    @Test
    @DisplayName("PUT /api/v1/user/notifications/{id}/read - harus mark satu notifikasi sebagai dibaca")
    void markOneRead_shouldReturn200() throws Exception {
        doNothing().when(notificationService).markOneRead(42L);

        mockMvc.perform(put("/api/v1/user/notifications/42/read")
                        .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Notifikasi ditandai sudah dibaca"));

        verify(notificationService).markOneRead(42L);
    }

    // ===== WALKGUIDE START / STOP =====

    @Test
    @DisplayName("POST /api/v1/user/walkguide/start - harus catat log WALKGUIDE_START")
    void walkGuideStart_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));
            doNothing().when(activityLogService).createLog(any(), eq(ActivityLogType.WALKGUIDE_START), anyString(), any());

            mockMvc.perform(post("/api/v1/user/walkguide/start")
                            .with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("WalkGuide dimulai"));

            verify(activityLogService).createLog(eq(sampleUser), eq(ActivityLogType.WALKGUIDE_START), anyString(), any());
        }
    }

    @Test
    @DisplayName("POST /api/v1/user/walkguide/stop - harus catat log WALKGUIDE_STOP")
    void walkGuideStop_shouldReturn200() throws Exception {
        try (MockedStatic<SecurityHelper> sh = mockStatic(SecurityHelper.class)) {
            sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L);
            when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));
            doNothing().when(activityLogService).createLog(any(), eq(ActivityLogType.WALKGUIDE_STOP), anyString(), any());

            mockMvc.perform(post("/api/v1/user/walkguide/stop")
                            .with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("WalkGuide dihentikan"));

            verify(activityLogService).createLog(eq(sampleUser), eq(ActivityLogType.WALKGUIDE_STOP), anyString(), any());
        }
    }
}
