package com.walkguide.service;

import com.walkguide.dto.request.UserSettingsUpdateRequest;
import com.walkguide.dto.response.UserSettingsResponse;
import com.walkguide.entity.UserSettings;
import com.walkguide.repository.UserSettingsRepository;
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 java.util.Optional;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("UserSettingsService Unit Tests")
class UserSettingsServiceTest {

    @Mock
    UserSettingsRepository userSettingsRepository;

    @InjectMocks
    UserSettingsService userSettingsService;

    private UserSettings existingSettings;

    @BeforeEach
    void setUp() {
        existingSettings = UserSettings.builder()
                .id(1L)
                .userId(10L)
                .ttsLanguage("id-ID")
                .ttsPitch(1.0)
                .ttsSpeed(0.9)
                .warnNoGuardian(true)
                .hapticEnabled(true)
                .build();
    }

    // ===== getSettings TESTS =====

    @Test
    @DisplayName("getSettings - harus return settings yang ada di DB")
    void getSettings_existingSettings_shouldReturnExistingData() {
        when(userSettingsRepository.findByUserId(10L)).thenReturn(Optional.of(existingSettings));

        UserSettingsResponse result = userSettingsService.getSettings(10L);

        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getTtsLanguage()).isEqualTo("id-ID");
        assertThat(result.getTtsPitch()).isEqualTo(1.0);
        assertThat(result.getTtsSpeed()).isEqualTo(0.9);
        assertThat(result.getWarnNoGuardian()).isTrue();
        assertThat(result.getHapticEnabled()).isTrue();

        // Tidak boleh membuat settings baru jika sudah ada
        verify(userSettingsRepository, never()).save(any());
    }

    @Test
    @DisplayName("getSettings - jika belum ada, harus auto-create settings default dan simpan ke DB")
    void getSettings_noExistingSettings_shouldCreateDefaultAndSave() {
        UserSettings defaultSettings = UserSettings.builder()
                .id(2L).userId(20L)
                .ttsLanguage("id-ID").ttsPitch(1.0).ttsSpeed(0.9)
                .warnNoGuardian(true).hapticEnabled(true).build();

        when(userSettingsRepository.findByUserId(20L)).thenReturn(Optional.empty());
        when(userSettingsRepository.save(any(UserSettings.class))).thenReturn(defaultSettings);

        UserSettingsResponse result = userSettingsService.getSettings(20L);

        assertThat(result).isNotNull();
        verify(userSettingsRepository).save(any(UserSettings.class));
    }

    @Test
    @DisplayName("getSettings - settings yang di-auto-create harus memiliki userId yang benar")
    void getSettings_autoCreate_shouldHaveCorrectUserId() {
        when(userSettingsRepository.findByUserId(30L)).thenReturn(Optional.empty());
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        userSettingsService.getSettings(30L);

        ArgumentCaptor<UserSettings> captor = ArgumentCaptor.forClass(UserSettings.class);
        verify(userSettingsRepository).save(captor.capture());
        assertThat(captor.getValue().getUserId()).isEqualTo(30L);
    }

    // ===== updateSettings TESTS =====

    @Test
    @DisplayName("updateSettings - semua field diisi harus mengupdate semua nilai")
    void updateSettings_allFields_shouldUpdateAllValues() {
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        req.setTtsLanguage("en-US");
        req.setTtsPitch(1.5);
        req.setTtsSpeed(1.2);
        req.setWarnNoGuardian(false);
        req.setHapticEnabled(false);

        when(userSettingsRepository.findByUserId(10L)).thenReturn(Optional.of(existingSettings));
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        UserSettingsResponse result = userSettingsService.updateSettings(10L, req);

        assertThat(result.getTtsLanguage()).isEqualTo("en-US");
        assertThat(result.getTtsPitch()).isEqualTo(1.5);
        assertThat(result.getTtsSpeed()).isEqualTo(1.2);
        assertThat(result.getWarnNoGuardian()).isFalse();
        assertThat(result.getHapticEnabled()).isFalse();
    }

    @Test
    @DisplayName("updateSettings - field null tidak boleh mengubah nilai yang sudah ada (partial update)")
    void updateSettings_partialUpdate_nullFieldsShouldNotOverride() {
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        req.setTtsLanguage("en-US"); // hanya ini yang diubah
        req.setTtsPitch(null);
        req.setTtsSpeed(null);
        req.setWarnNoGuardian(null);
        req.setHapticEnabled(null);

        when(userSettingsRepository.findByUserId(10L)).thenReturn(Optional.of(existingSettings));
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        UserSettingsResponse result = userSettingsService.updateSettings(10L, req);

        assertThat(result.getTtsLanguage()).isEqualTo("en-US");
        // Nilai lama harus tetap
        assertThat(result.getTtsPitch()).isEqualTo(1.0);
        assertThat(result.getTtsSpeed()).isEqualTo(0.9);
        assertThat(result.getWarnNoGuardian()).isTrue();
        assertThat(result.getHapticEnabled()).isTrue();
    }

    @Test
    @DisplayName("updateSettings - jika belum ada settings, harus buat baru dan update")
    void updateSettings_noExistingSettings_shouldCreateNewAndUpdate() {
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        req.setTtsLanguage("ja-JP");
        req.setHapticEnabled(false);

        when(userSettingsRepository.findByUserId(40L)).thenReturn(Optional.empty());
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        UserSettingsResponse result = userSettingsService.updateSettings(40L, req);

        ArgumentCaptor<UserSettings> captor = ArgumentCaptor.forClass(UserSettings.class);
        verify(userSettingsRepository).save(captor.capture());

        assertThat(captor.getValue().getUserId()).isEqualTo(40L);
        assertThat(captor.getValue().getTtsLanguage()).isEqualTo("ja-JP");
        assertThat(captor.getValue().getHapticEnabled()).isFalse();
    }

    @Test
    @DisplayName("updateSettings - harus memanggil repository.save dan return hasil yang diupdate")
    void updateSettings_shouldPersistAndReturnUpdatedResponse() {
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        req.setTtsSpeed(0.5);

        when(userSettingsRepository.findByUserId(10L)).thenReturn(Optional.of(existingSettings));
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        userSettingsService.updateSettings(10L, req);

        verify(userSettingsRepository, times(1)).save(any(UserSettings.class));
    }

    @Test
    @DisplayName("updateSettings - semua field null harus tidak mengubah apapun")
    void updateSettings_allNull_shouldNotChangeAnything() {
        UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
        // semua null

        when(userSettingsRepository.findByUserId(10L)).thenReturn(Optional.of(existingSettings));
        when(userSettingsRepository.save(any(UserSettings.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        UserSettingsResponse result = userSettingsService.updateSettings(10L, req);

        assertThat(result.getTtsLanguage()).isEqualTo("id-ID");
        assertThat(result.getTtsPitch()).isEqualTo(1.0);
        assertThat(result.getTtsSpeed()).isEqualTo(0.9);
        assertThat(result.getWarnNoGuardian()).isTrue();
        assertThat(result.getHapticEnabled()).isTrue();
    }
}