package com.walkguide.service;

import com.walkguide.dto.request.LoginRequest;
import com.walkguide.dto.request.RegisterRequest;
import com.walkguide.dto.response.AuthDataResponse;
import com.walkguide.entity.RefreshToken;
import com.walkguide.entity.User;
import com.walkguide.entity.UserSettings;
import com.walkguide.enums.ActivityLogType;
import com.walkguide.repository.RefreshTokenRepository;
import com.walkguide.repository.UserRepository;
import com.walkguide.repository.UserSettingsRepository;
import com.walkguide.security.JwtUtil;
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.security.crypto.password.PasswordEncoder;

import java.time.LocalDateTime;
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("AuthService Unit Tests")
class AuthServiceTest {

    @Mock UserRepository userRepository;
    @Mock RefreshTokenRepository refreshTokenRepository;
    @Mock UserSettingsRepository userSettingsRepository;
    @Mock ActivityLogService activityLogService;
    @Mock JwtUtil jwtUtil;
    @Mock PasswordEncoder passwordEncoder;

    @InjectMocks AuthService authService;

    private User sampleUser;
    private User sampleGuardian;

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

        sampleGuardian = User.builder()
                .id(2L)
                .email("guardian@test.com")
                .password("encodedPassword")
                .role("ROLE_GUARDIAN")
                .displayName("Test Guardian")
                .build();
    }

    // ===== REGISTER TESTS =====

    @Test
    @DisplayName("register - ROLE_USER: harus generate uniqueUserId dan buat UserSettings")
    void register_asUser_shouldGenerateUniqueUserIdAndCreateSettings() {
        RegisterRequest req = new RegisterRequest();
        req.setEmail("new@test.com");
        req.setPassword("password123");
        req.setDisplayName("New User");
        req.setRole("USER");

        when(userRepository.existsByEmail("new@test.com")).thenReturn(false);
        when(userRepository.findByUniqueUserId(anyString())).thenReturn(Optional.empty());
        when(passwordEncoder.encode("password123")).thenReturn("hashed");
        when(userRepository.save(any(User.class))).thenAnswer(inv -> {
            User u = inv.getArgument(0);
            u.setId(10L);
            return u;
        });
        when(jwtUtil.generateAccessToken(anyString(), anyString(), anyLong()))
                .thenReturn("access-token");
        when(jwtUtil.generateRefreshToken()).thenReturn("refresh-token");
        when(refreshTokenRepository.save(any())).thenReturn(new RefreshToken());

        AuthDataResponse result = authService.register(req);

        assertThat(result).isNotNull();
        assertThat(result.getRole()).isEqualTo("ROLE_USER");
        assertThat(result.getAccessToken()).isEqualTo("access-token");

        // Verifikasi UserSettings dibuat untuk ROLE_USER
        verify(userSettingsRepository, times(1)).save(any(UserSettings.class));
    }

    @Test
    @DisplayName("register - ROLE_GUARDIAN: tidak boleh generate uniqueUserId dan tidak buat UserSettings")
    void register_asGuardian_shouldNotCreateUserSettings() {
        RegisterRequest req = new RegisterRequest();
        req.setEmail("guardian2@test.com");
        req.setPassword("password123");
        req.setDisplayName("New Guardian");
        req.setRole("GUARDIAN");

        when(userRepository.existsByEmail("guardian2@test.com")).thenReturn(false);
        when(passwordEncoder.encode("password123")).thenReturn("hashed");
        when(userRepository.save(any(User.class))).thenAnswer(inv -> {
            User u = inv.getArgument(0);
            u.setId(11L);
            return u;
        });
        when(jwtUtil.generateAccessToken(anyString(), anyString(), anyLong()))
                .thenReturn("access-token");
        when(jwtUtil.generateRefreshToken()).thenReturn("refresh-token");
        when(refreshTokenRepository.save(any())).thenReturn(new RefreshToken());

        AuthDataResponse result = authService.register(req);

        assertThat(result.getRole()).isEqualTo("ROLE_GUARDIAN");
        // Tidak boleh buat UserSettings untuk Guardian
        verify(userSettingsRepository, never()).save(any());

        // Verifikasi uniqueUserId null pada Guardian
        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        verify(userRepository).save(userCaptor.capture());
        assertThat(userCaptor.getValue().getUniqueUserId()).isNull();
    }

    @Test
    @DisplayName("register - email duplikat harus throw RuntimeException")
    void register_duplicateEmail_shouldThrow() {
        RegisterRequest req = new RegisterRequest();
        req.setEmail("user@test.com");
        req.setPassword("password123");
        req.setDisplayName("User");
        req.setRole("USER");

        when(userRepository.existsByEmail("user@test.com")).thenReturn(true);

        assertThatThrownBy(() -> authService.register(req))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Email sudah terdaftar");

        verify(userRepository, never()).save(any());
    }

    // ===== LOGIN TESTS =====

    @Test
    @DisplayName("login - credentials valid harus return AuthDataResponse")
    void login_validCredentials_shouldReturnToken() {
        LoginRequest req = new LoginRequest();
        req.setEmail("user@test.com");
        req.setPassword("correctPassword");

        when(userRepository.findByEmail("user@test.com")).thenReturn(Optional.of(sampleUser));
        when(passwordEncoder.matches("correctPassword", "encodedPassword")).thenReturn(true);
        when(jwtUtil.generateAccessToken(anyString(), anyString(), anyLong()))
                .thenReturn("access-token");
        when(jwtUtil.generateRefreshToken()).thenReturn("refresh-token");
        when(refreshTokenRepository.save(any())).thenReturn(new RefreshToken());

        AuthDataResponse result = authService.login(req);

        assertThat(result).isNotNull();
        assertThat(result.getAccessToken()).isEqualTo("access-token");
        assertThat(result.getRefreshToken()).isEqualTo("refresh-token");
        assertThat(result.getUserId()).isEqualTo(1L);

        // Harus hapus refresh token lama sebelum buat yang baru
        verify(refreshTokenRepository).deleteByUserId(1L);
        // Harus catat activity log LOGIN
        verify(activityLogService).createLog(
                eq(sampleUser), eq(ActivityLogType.LOGIN), anyString(), any());
    }

    @Test
    @DisplayName("login - email tidak terdaftar harus throw RuntimeException")
    void login_emailNotFound_shouldThrow() {
        LoginRequest req = new LoginRequest();
        req.setEmail("ghost@test.com");
        req.setPassword("any");

        when(userRepository.findByEmail("ghost@test.com")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> authService.login(req))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Email tidak terdaftar");
    }

    @Test
    @DisplayName("login - password salah harus throw RuntimeException")
    void login_wrongPassword_shouldThrow() {
        LoginRequest req = new LoginRequest();
        req.setEmail("user@test.com");
        req.setPassword("wrongPassword");

        when(userRepository.findByEmail("user@test.com")).thenReturn(Optional.of(sampleUser));
        when(passwordEncoder.matches("wrongPassword", "encodedPassword")).thenReturn(false);

        assertThatThrownBy(() -> authService.login(req))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Password salah");
    }

    // ===== REFRESH TOKEN TESTS =====

    @Test
    @DisplayName("refreshToken - token valid harus return access token baru")
    void refreshToken_validToken_shouldReturnNewAccessToken() {
        RefreshToken storedToken = RefreshToken.builder()
                .token("valid-refresh-token")
                .userId(1L)
                .expiresAt(LocalDateTime.now().plusDays(10))
                .build();

        when(refreshTokenRepository.findByToken("valid-refresh-token"))
                .thenReturn(Optional.of(storedToken));
        when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));
        when(jwtUtil.generateAccessToken(anyString(), anyString(), anyLong()))
                .thenReturn("new-access-token");

        AuthDataResponse result = authService.refreshToken("valid-refresh-token");

        assertThat(result.getAccessToken()).isEqualTo("new-access-token");
        // Refresh token tidak diganti — tetap sama
        assertThat(result.getRefreshToken()).isEqualTo("valid-refresh-token");
    }

    @Test
    @DisplayName("refreshToken - token tidak ada di DB harus throw RuntimeException")
    void refreshToken_invalidToken_shouldThrow() {
        when(refreshTokenRepository.findByToken("fake-token")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> authService.refreshToken("fake-token"))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Refresh token tidak valid");
    }

    @Test
    @DisplayName("refreshToken - token kadaluarsa harus throw dan hapus dari DB")
    void refreshToken_expiredToken_shouldThrowAndDelete() {
        RefreshToken expiredToken = RefreshToken.builder()
                .token("expired-token")
                .userId(1L)
                .expiresAt(LocalDateTime.now().minusDays(1)) // sudah lewat
                .build();

        when(refreshTokenRepository.findByToken("expired-token"))
                .thenReturn(Optional.of(expiredToken));

        assertThatThrownBy(() -> authService.refreshToken("expired-token"))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("kadaluarsa");

        // Token kadaluarsa harus dihapus dari DB
        verify(refreshTokenRepository).delete(expiredToken);
    }

    // ===== LOGOUT TESTS =====

    @Test
    @DisplayName("logout - harus hapus refresh token dan catat activity log")
    void logout_shouldDeleteTokenAndLog() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));

        authService.logout(1L);

        verify(refreshTokenRepository).deleteByUserId(1L);
        verify(activityLogService).createLog(
                eq(sampleUser), eq(ActivityLogType.LOGOUT), anyString(), any());
    }

    // ===== UPDATE FCM TOKEN TESTS =====

    @Test
    @DisplayName("updateFcmToken - harus update dan simpan fcmToken user")
    void updateFcmToken_shouldSaveNewToken() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(sampleUser));

        authService.updateFcmToken(1L, "new-fcm-token-xyz");

        ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
        verify(userRepository).save(captor.capture());
        assertThat(captor.getValue().getFcmToken()).isEqualTo("new-fcm-token-xyz");
    }
}