package org.raddatz.familienarchiv.user;

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.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.user.AdminUpdateUserRequest;
import org.raddatz.familienarchiv.user.ChangePasswordDTO;
import org.raddatz.familienarchiv.user.CreateUserRequest;
import org.raddatz.familienarchiv.user.UpdateProfileDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.UserGroup;
import org.raddatz.familienarchiv.user.AppUserRepository;
import org.raddatz.familienarchiv.user.UserGroupRepository;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

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

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock AppUserRepository userRepository;
    @Mock UserGroupRepository groupRepository;
    @Mock InviteTokenRepository inviteTokenRepository;
    @Mock PasswordEncoder passwordEncoder;
    @Mock AuditService auditService;
    @InjectMocks UserService userService;

    // ─── findByEmail ──────────────────────────────────────────────────────────

    @Test
    void findByEmail_throwsNotFound_whenMissing() {
        when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.findByEmail("ghost@example.com"))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void findByEmail_returnsUser_whenFound() {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("admin@example.com").build();
        when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user));

        assertThat(userService.findByEmail("admin@example.com")).isEqualTo(user);
    }

    // ─── findByEmailOptional ──────────────────────────────────────────────────

    @Test
    void findByEmailOptional_returnsEmpty_whenMissing() {
        when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());

        assertThat(userService.findByEmailOptional("ghost@example.com")).isEmpty();
    }

    @Test
    void findByEmailOptional_returnsUser_whenFound() {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("admin@example.com").build();
        when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user));

        assertThat(userService.findByEmailOptional("admin@example.com")).contains(user);
    }

    // ─── save ─────────────────────────────────────────────────────────────────

    @Test
    void save_delegatesToRepository() {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
        when(userRepository.save(user)).thenReturn(user);

        assertThat(userService.save(user)).isEqualTo(user);
        verify(userRepository).save(user);
    }

    // ─── deleteUser ───────────────────────────────────────────────────────────

    @Test
    void deleteUser_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(userRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.deleteUser(UUID.randomUUID(), id))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void deleteUser_deletesUser_whenFound() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("gast@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));

        userService.deleteUser(UUID.randomUUID(), id);

        verify(userRepository).delete(user);
    }

    // ─── createUserOrUpdate ───────────────────────────────────────────────────

    @Test
    void createUserOrUpdate_createsNewUser_whenNotExists() {
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("new@example.com");
        req.setInitialPassword("secret");
        req.setGroupIds(List.of());

        when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("secret")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        AppUser result = userService.createUserOrUpdate(UUID.randomUUID(), req);

        assertThat(result).isEqualTo(saved);
        verify(userRepository).save(any());
    }

    @Test
    void createUserOrUpdate_updatesExistingUser_whenFound() {
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("existing@example.com");
        req.setInitialPassword("newpass");
        req.setGroupIds(List.of());

        AppUser existing = AppUser.builder().id(UUID.randomUUID()).email("existing@example.com").build();
        when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(existing));
        when(passwordEncoder.encode(any())).thenReturn("encoded");
        when(userRepository.save(any())).thenReturn(existing);

        userService.createUserOrUpdate(UUID.randomUUID(), req);

        verify(userRepository, times(1)).save(existing);
    }

    // ─── getById ──────────────────────────────────────────────────────────────

    @Test
    void getById_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(userRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.getById(id))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void getById_returnsUser_whenFound() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));

        assertThat(userService.getById(id)).isEqualTo(user);
    }

    // ─── updateProfile ────────────────────────────────────────────────────────

    @Test
    void updateProfile_updatesFields() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.empty());
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setFirstName("Max"); dto.setLastName("Müller"); dto.setEmail("max@example.com");
        AppUser result = userService.updateProfile(id, dto);

        assertThat(result.getFirstName()).isEqualTo("Max");
        assertThat(result.getLastName()).isEqualTo("Müller");
        assertThat(result.getEmail()).isEqualTo("max@example.com");
    }

    @Test
    void updateProfile_throwsConflict_whenEmailTakenByAnotherUser() {
        UUID id = UUID.randomUUID();
        UUID otherId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        AppUser other = AppUser.builder().id(otherId).email("taken@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setEmail("taken@example.com");

        assertThatThrownBy(() -> userService.updateProfile(id, dto))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("E-Mail");
    }

    @Test
    void updateProfile_allowsSameEmailForSameUser() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setEmail("max@example.com");
        dto.setFirstName("Max");

        assertThat(userService.updateProfile(id, dto).getEmail()).isEqualTo("max@example.com");
    }

    // ─── changePassword ───────────────────────────────────────────────────────

    @Test
    void changePassword_throwsBadRequest_whenCurrentPasswordWrong() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").password("hashed").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(passwordEncoder.matches("wrong", "hashed")).thenReturn(false);

        ChangePasswordDTO dto = new ChangePasswordDTO();
        dto.setCurrentPassword("wrong"); dto.setNewPassword("newpass");

        assertThatThrownBy(() -> userService.changePassword(id, dto))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("Passwort");
    }

    @Test
    void changePassword_updatesHash_whenCurrentPasswordCorrect() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").password("hashed").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(passwordEncoder.matches("correct", "hashed")).thenReturn(true);
        when(passwordEncoder.encode("newpass")).thenReturn("newHash");
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        ChangePasswordDTO dto = new ChangePasswordDTO();
        dto.setCurrentPassword("correct"); dto.setNewPassword("newpass");
        userService.changePassword(id, dto);

        verify(userRepository).save(argThat(u -> "newHash".equals(u.getPassword())));
    }

    // ─── adminUpdateUser ──────────────────────────────────────────────────────

    @Test
    void adminUpdateUser_updatesNameFields() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setFirstName("Ada"); dto.setLastName("Lovelace");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getFirstName()).isEqualTo("Ada");
        assertThat(result.getLastName()).isEqualTo("Lovelace");
    }

    @Test
    void adminUpdateUser_preservesGroups_whenGroupIdsIsNull() {
        UUID id = UUID.randomUUID();
        UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").groups(Set.of(adminGroup)).build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setFirstName("Ada");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getGroups()).containsExactly(adminGroup);
    }

    @Test
    void adminUpdateUser_updatesGroups_whenGroupIdsProvided() {
        UUID id = UUID.randomUUID();
        UserGroup oldGroup = UserGroup.builder().id(UUID.randomUUID()).name("Viewers").build();
        UserGroup newGroup = UserGroup.builder().id(UUID.randomUUID()).name("Editors").build();
        AppUser user = AppUser.builder().id(id).email("max@example.com").groups(Set.of(oldGroup)).build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(groupRepository.findAllById(List.of(newGroup.getId()))).thenReturn(List.of(newGroup));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setGroupIds(List.of(newGroup.getId()));

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getGroups()).containsExactly(newGroup);
    }

    @Test
    void adminUpdateUser_clearsGroups_whenGroupIdsIsEmptyList() {
        UUID id = UUID.randomUUID();
        UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").groups(Set.of(adminGroup)).build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(groupRepository.findAllById(List.of())).thenReturn(List.of());
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setGroupIds(List.of());

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getGroups()).isEmpty();
    }

    // ─── getGroupById ─────────────────────────────────────────────────────────

    @Test
    void getGroupById_throwsNotFound_whenMissing() {
        UUID id = UUID.randomUUID();
        when(groupRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.getGroupById(id))
                .isInstanceOf(DomainException.class);
    }

    // ─── createUserOrUpdate — groups loaded ───────────────────────────────────

    @Test
    void createUserOrUpdate_loadsGroups_whenGroupIdsNonEmpty() {
        UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins").build();
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("u@example.com");
        req.setInitialPassword("pass");
        req.setGroupIds(List.of(group.getId()));

        when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.empty());
        when(groupRepository.findAllById(List.of(group.getId()))).thenReturn(List.of(group));
        when(passwordEncoder.encode("pass")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        AppUser result = userService.createUserOrUpdate(UUID.randomUUID(), req);

        assertThat(result).isEqualTo(saved);
        verify(groupRepository).findAllById(List.of(group.getId()));
    }

    // ─── updateProfile — blank email ──────────────────────────────────────────

    @Test
    void updateProfile_throwsBadRequest_whenEmailIsBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("old@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setEmail("   ");

        assertThatThrownBy(() -> userService.updateProfile(id, dto))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("blank");
    }

    @Test
    void updateProfile_doesNotChangeEmail_whenEmailDtoIsNull() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("keep@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setEmail(null);

        AppUser result = userService.updateProfile(id, dto);

        assertThat(result.getEmail()).isEqualTo("keep@example.com");
    }

    @Test
    void updateProfile_setsContactToNull_whenContactIsBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setContact("  ");

        AppUser result = userService.updateProfile(id, dto);

        assertThat(result.getContact()).isNull();
    }

    // ─── adminUpdateUser — password and email branches ────────────────────────

    @Test
    void adminUpdateUser_setsPassword_whenNewPasswordProvided() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").password("old").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(passwordEncoder.encode("newSecret")).thenReturn("newHashed");
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setNewPassword("newSecret");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getPassword()).isEqualTo("newHashed");
    }

    @Test
    void adminUpdateUser_doesNotChangePassword_whenNewPasswordIsBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").password("original").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setNewPassword("  ");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getPassword()).isEqualTo("original");
        verify(passwordEncoder, never()).encode(any());
    }

    @Test
    void adminUpdateUser_throwsBadRequest_whenEmailIsBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("old@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setEmail("   ");

        assertThatThrownBy(() -> userService.adminUpdateUser(UUID.randomUUID(), id, dto))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("blank");
    }

    @Test
    void adminUpdateUser_throwsConflict_whenEmailTakenByAnotherUser() {
        UUID id = UUID.randomUUID();
        UUID otherId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
        AppUser other = AppUser.builder().id(otherId).email("taken@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setEmail("taken@example.com");

        assertThatThrownBy(() -> userService.adminUpdateUser(UUID.randomUUID(), id, dto))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("E-Mail");
    }

    // ─── updateGroup ──────────────────────────────────────────────────────────

    @Test
    void updateGroup_updatesNameAndPermissions_whenBothProvided() {
        UUID id = UUID.randomUUID();
        UserGroup group = UserGroup.builder().id(id).name("OldName")
                .permissions(Set.of("READ_ALL")).build();
        when(groupRepository.findById(id)).thenReturn(Optional.of(group));
        when(groupRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        org.raddatz.familienarchiv.user.GroupDTO dto = new org.raddatz.familienarchiv.user.GroupDTO();
        dto.setName("NewName");
        dto.setPermissions(Set.of("WRITE_ALL"));

        UserGroup result = userService.updateGroup(id, dto);

        assertThat(result.getName()).isEqualTo("NewName");
        assertThat(result.getPermissions()).containsExactly("WRITE_ALL");
    }

    @Test
    void updateGroup_keepsExistingName_whenNameIsNull() {
        UUID id = UUID.randomUUID();
        UserGroup group = UserGroup.builder().id(id).name("Existing").build();
        when(groupRepository.findById(id)).thenReturn(Optional.of(group));
        when(groupRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        org.raddatz.familienarchiv.user.GroupDTO dto = new org.raddatz.familienarchiv.user.GroupDTO();
        dto.setName(null);
        dto.setPermissions(Set.of("ADMIN"));

        UserGroup result = userService.updateGroup(id, dto);

        assertThat(result.getName()).isEqualTo("Existing");
    }

    @Test
    void updateGroup_keepsExistingPermissions_whenPermissionsAreNull() {
        UUID id = UUID.randomUUID();
        UserGroup group = UserGroup.builder().id(id).name("Group")
                .permissions(Set.of("READ_ALL")).build();
        when(groupRepository.findById(id)).thenReturn(Optional.of(group));
        when(groupRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        org.raddatz.familienarchiv.user.GroupDTO dto = new org.raddatz.familienarchiv.user.GroupDTO();
        dto.setName("NewName");
        dto.setPermissions(null);

        UserGroup result = userService.updateGroup(id, dto);

        assertThat(result.getPermissions()).containsExactly("READ_ALL");
    }

    // ─── createUserOrUpdate — empty groupIds ──────────────────────────────────

    @Test
    void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsEmpty() {
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("u@example.com");
        req.setInitialPassword("pass");
        req.setGroupIds(List.of());

        when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("pass")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        userService.createUserOrUpdate(UUID.randomUUID(), req);

        verify(groupRepository, never()).findAllById(any());
    }

    // ─── updateProfile — contact ──────────────────────────────────────────────

    @Test
    void updateProfile_setsTrimmedContact_whenContactIsNonBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setContact("  phone: 999  ");

        AppUser result = userService.updateProfile(id, dto);

        assertThat(result.getContact()).isEqualTo("phone: 999");
    }

    @Test
    void updateProfile_setsNullContact_whenContactIsNull() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("max@example.com").contact("old contact").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setContact(null);

        AppUser result = userService.updateProfile(id, dto);

        assertThat(result.getContact()).isNull();
    }

    @Test
    void updateProfile_allowsSameEmail_whenEmailBelongsToSameUser() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("me@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateProfileDTO dto = new UpdateProfileDTO();
        dto.setEmail("me@example.com");

        AppUser result = userService.updateProfile(id, dto);
        assertThat(result.getEmail()).isEqualTo("me@example.com");
    }

    // ─── adminUpdateUser — contact null and email null ────────────────────────

    @Test
    void adminUpdateUser_setsNullContact_whenContactIsNull() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").contact("old contact").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setContact(null);

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getContact()).isNull();
    }

    @Test
    void adminUpdateUser_setsNullContact_whenContactIsBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").contact("old").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setContact("   ");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getContact()).isNull();
    }

    @Test
    void adminUpdateUser_setsTrimmedContact_whenContactIsNonBlank() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setContact("  phone: 555  ");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getContact()).isEqualTo("phone: 555");
    }

    @Test
    void adminUpdateUser_doesNotModifyEmail_whenEmailIsNull() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("keep@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setEmail(null);

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);

        assertThat(result.getEmail()).isEqualTo("keep@example.com");
    }

    @Test
    void adminUpdateUser_allowsSameEmail_whenEmailBelongsToSameUser() {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("me@example.com").build();
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setEmail("me@example.com");

        AppUser result = userService.adminUpdateUser(UUID.randomUUID(), id, dto);
        assertThat(result.getEmail()).isEqualTo("me@example.com");
    }

    // ─── createUserOrUpdate — null groupIds ──────────────────────────────────

    @Test
    void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsNull() {
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("ng@example.com");
        req.setInitialPassword("pass");
        req.setGroupIds(null);

        when(userRepository.findByEmail("ng@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("pass")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("ng@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        userService.createUserOrUpdate(UUID.randomUUID(), req);

        verify(groupRepository, never()).findAllById(any());
    }

    // ─── createUser ───────────────────────────────────────────────────────────

    @Test
    void createUser_savesNewUser_withEncodedPassword() {
        when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("secret")).thenReturn("hashed");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        AppUser result = userService.createUser("new@example.com", "secret", "Max", "Muster", Set.of());

        assertThat(result).isEqualTo(saved);
        verify(passwordEncoder).encode("secret");
        verify(userRepository).save(any());
    }

    @Test
    void createUser_throwsConflict_whenEmailAlreadyExists() {
        AppUser existing = AppUser.builder().id(UUID.randomUUID()).email("dupe@example.com").build();
        when(userRepository.findByEmail("dupe@example.com")).thenReturn(Optional.of(existing));

        assertThatThrownBy(() -> userService.createUser("dupe@example.com", "pass", null, null, Set.of()))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.EMAIL_ALREADY_IN_USE);
    }

    @Test
    void createUser_assignsGroupsFromIds() {
        UUID groupId = UUID.randomUUID();
        UserGroup g = UserGroup.builder().id(groupId).name("Familie").build();
        when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.empty());
        when(groupRepository.findAllById(Set.of(groupId))).thenReturn(List.of(g));
        when(passwordEncoder.encode(any())).thenReturn("hashed");
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AppUser result = userService.createUser("u@example.com", "pass", null, null, Set.of(groupId));

        assertThat(result.getGroups()).contains(g);
    }

    // ─── findGroupsByIds ───────────────────────────────────────────────────────

    @Test
    void findGroupsByIds_delegatesToRepository() {
        UUID id = UUID.randomUUID();
        UserGroup g = UserGroup.builder().id(id).name("Admins").build();
        when(groupRepository.findAllById(List.of(id))).thenReturn(List.of(g));

        List<UserGroup> result = userService.findGroupsByIds(List.of(id));

        assertThat(result).containsExactly(g);
    }

    // ─── audit: GROUP_MEMBERSHIP_CHANGED ─────────────────────────────────────

    @Test
    void adminUpdateUser_logsGroupMembershipChanged_whenGroupSetChanges() {
        UUID actorId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        UserGroup oldGroup = UserGroup.builder().id(UUID.randomUUID()).name("Viewers").permissions(Set.of("READ_ALL")).build();
        UserGroup newGroup = UserGroup.builder().id(UUID.randomUUID()).name("Editors").permissions(Set.of("WRITE_ALL")).build();
        AppUser user = AppUser.builder().id(userId).email("u@example.com").groups(Set.of(oldGroup)).build();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(groupRepository.findAllById(List.of(newGroup.getId()))).thenReturn(List.of(newGroup));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setGroupIds(List.of(newGroup.getId()));

        userService.adminUpdateUser(actorId, userId, dto);

        @SuppressWarnings("unchecked")
        ArgumentCaptor<java.util.Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(java.util.Map.class);
        verify(auditService).logAfterCommit(
                org.mockito.ArgumentMatchers.eq(AuditKind.GROUP_MEMBERSHIP_CHANGED),
                org.mockito.ArgumentMatchers.eq(actorId),
                org.mockito.ArgumentMatchers.isNull(),
                payloadCaptor.capture());
        java.util.Map<String, Object> payload = payloadCaptor.getValue();
        assertThat(payload).containsEntry("email", "u@example.com");
        assertThat((java.util.List<String>) payload.get("addedGroups")).containsExactly("Editors");
        assertThat((java.util.List<String>) payload.get("removedGroups")).containsExactly("Viewers");
    }

    @Test
    void adminUpdateUser_doesNotLogGroupMembershipChanged_whenGroupsUnchanged() {
        UUID actorId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins").build();
        AppUser user = AppUser.builder().id(userId).email("u@example.com").groups(Set.of(group)).build();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(groupRepository.findAllById(List.of(group.getId()))).thenReturn(List.of(group));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        dto.setGroupIds(List.of(group.getId()));

        userService.adminUpdateUser(actorId, userId, dto);

        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    @Test
    void adminUpdateUser_doesNotLogGroupMembershipChanged_whenGroupIdsIsNull() {
        UUID actorId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins").build();
        AppUser user = AppUser.builder().id(userId).email("u@example.com").groups(Set.of(group)).build();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
        when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
        // groupIds not set → null

        userService.adminUpdateUser(actorId, userId, dto);

        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    // ─── audit: USER_DELETED ──────────────────────────────────────────────────

    @Test
    void deleteUser_logsUserDeleted_withEmailInPayload() {
        UUID actorId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        AppUser user = AppUser.builder().id(userId).email("gone@example.com").build();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        userService.deleteUser(actorId, userId);

        @SuppressWarnings("unchecked")
        ArgumentCaptor<java.util.Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(java.util.Map.class);
        verify(auditService).logAfterCommit(
                org.mockito.ArgumentMatchers.eq(AuditKind.USER_DELETED),
                org.mockito.ArgumentMatchers.eq(actorId),
                org.mockito.ArgumentMatchers.isNull(),
                payloadCaptor.capture());
        assertThat(payloadCaptor.getValue()).containsEntry("email", "gone@example.com");
        assertThat(payloadCaptor.getValue()).containsKey("userId");
    }

    // ─── audit: USER_CREATED ──────────────────────────────────────────────────

    @Test
    void createUserOrUpdate_logsUserCreated_whenUserIsNew() {
        UUID actorId = UUID.randomUUID();
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("new@example.com");
        req.setInitialPassword("secret");
        req.setGroupIds(List.of());

        when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("secret")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        userService.createUserOrUpdate(actorId, req);

        @SuppressWarnings("unchecked")
        ArgumentCaptor<java.util.Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(java.util.Map.class);
        verify(auditService).logAfterCommit(
                org.mockito.ArgumentMatchers.eq(AuditKind.USER_CREATED),
                org.mockito.ArgumentMatchers.eq(actorId),
                org.mockito.ArgumentMatchers.isNull(),
                payloadCaptor.capture());
        assertThat(payloadCaptor.getValue()).containsKey("userId");
        assertThat(payloadCaptor.getValue()).containsEntry("email", "new@example.com");
    }

    @Test
    void createUserOrUpdate_doesNotLogUserCreated_whenUserAlreadyExists() {
        UUID actorId = UUID.randomUUID();
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("existing@example.com");
        req.setInitialPassword("pass");
        req.setGroupIds(List.of());

        AppUser existing = AppUser.builder().id(UUID.randomUUID()).email("existing@example.com").build();
        when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(existing));
        when(passwordEncoder.encode(any())).thenReturn("encoded");
        when(userRepository.save(any())).thenReturn(existing);

        userService.createUserOrUpdate(actorId, req);

        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    // ─── createUserForBootstrap ───────────────────────────────────────────────

    @Test
    void createUserForBootstrap_createsUserWithoutAuditEvent() {
        CreateUserRequest req = new CreateUserRequest();
        req.setEmail("bootstrap@example.com");
        req.setInitialPassword("secret");
        req.setGroupIds(List.of());

        when(userRepository.findByEmail("bootstrap@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("secret")).thenReturn("encoded");
        AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("bootstrap@example.com").build();
        when(userRepository.save(any())).thenReturn(saved);

        AppUser result = userService.createUserForBootstrap(req);

        assertThat(result).isEqualTo(saved);
        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    // ─── createGroup ──────────────────────────────────────────────────────────

    @Test
    void createGroup_createsGroupWithNameAndPermissions() {
        org.raddatz.familienarchiv.user.GroupDTO dto = new org.raddatz.familienarchiv.user.GroupDTO();
        dto.setName("Familie");
        dto.setPermissions(Set.of("READ_ALL", "WRITE_ALL"));

        UserGroup saved = UserGroup.builder().id(UUID.randomUUID()).name("Familie")
                .permissions(Set.of("READ_ALL", "WRITE_ALL")).build();
        when(groupRepository.save(any())).thenReturn(saved);

        UserGroup result = userService.createGroup(dto);

        assertThat(result.getName()).isEqualTo("Familie");
        assertThat(result.getPermissions()).containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");
    }

    // ─── deleteGroup ──────────────────────────────────────────────────────────

    @Test
    void deleteGroup_throwsConflict_whenActiveInviteReferencesGroup() {
        UUID groupId = UUID.randomUUID();
        when(inviteTokenRepository.existsActiveWithGroupId(groupId)).thenReturn(true);

        assertThatThrownBy(() -> userService.deleteGroup(groupId))
                .isInstanceOf(DomainException.class)
                .extracting(e -> ((DomainException) e).getCode())
                .isEqualTo(ErrorCode.GROUP_HAS_ACTIVE_INVITES);
    }

    @Test
    void deleteGroup_deletesGroup_whenNoActiveInviteReferencesGroup() {
        UUID groupId = UUID.randomUUID();
        when(inviteTokenRepository.existsActiveWithGroupId(groupId)).thenReturn(false);

        userService.deleteGroup(groupId);

        verify(groupRepository).deleteById(groupId);
    }

    @Test
    void createGroup_withNullPermissions_savesGroupWithEmptyPermissionSet() {
        org.raddatz.familienarchiv.user.GroupDTO dto = new org.raddatz.familienarchiv.user.GroupDTO();
        dto.setName("Leser");
        dto.setPermissions(null);

        UserGroup saved = UserGroup.builder().id(UUID.randomUUID()).name("Leser").build();
        when(groupRepository.save(any())).thenReturn(saved);

        userService.createGroup(dto);

        verify(groupRepository).save(argThat(g -> g.getPermissions() != null && g.getPermissions().isEmpty()));
    }
}
