package com.petshop.backend.service;

import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.user.UserRequest;
import com.petshop.backend.dto.user.UserResponse;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.AppPrincipal;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock private UserRepository userRepository;
    @Mock private PasswordEncoder passwordEncoder;
    @Mock private StoreRepository storeRepository;

    private UserService userService;

    @BeforeEach
    void setUp() {
        userService = new UserService(userRepository, passwordEncoder, storeRepository);
    }

    @AfterEach
    void tearDown() {
        SecurityContextHolder.clearContext();
    }

    @Test
    void updateUserDeniesEditingAnotherAdmin() {
        User actor = user(1L, User.Role.ADMIN);
        User target = user(2L, User.Role.ADMIN);
        authenticate(actor);

        when(userRepository.findById(2L)).thenReturn(Optional.of(target));
        when(userRepository.findById(1L)).thenReturn(Optional.of(actor));

        UserRequest request = request(User.Role.ADMIN);

        assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request));
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void updateUserTreatsWrongScopedRoleAsNotFound() {
        User actor = user(1L, User.Role.ADMIN);
        User target = user(2L, User.Role.ADMIN);
        authenticate(actor);

        when(userRepository.findById(2L)).thenReturn(Optional.of(target));

        UserRequest request = request(User.Role.CUSTOMER);

        assertThrows(ResourceNotFoundException.class, () -> userService.updateUser(2L, request, User.Role.CUSTOMER));
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void deleteUserDeniesDeletingAnotherAdmin() {
        User actor = user(1L, User.Role.ADMIN);
        User target = user(2L, User.Role.ADMIN);
        authenticate(actor);

        when(userRepository.findById(2L)).thenReturn(Optional.of(target));

        assertThrows(AccessDeniedException.class, () -> userService.deleteUser(2L));
        verify(userRepository, never()).deleteById(anyLong());
    }

    @Test
    void deleteUserDeniesDeletingSelfAdminAccount() {
        User actor = user(1L, User.Role.ADMIN);
        authenticate(actor);

        when(userRepository.findById(1L)).thenReturn(Optional.of(actor));

        assertThrows(AccessDeniedException.class, () -> userService.deleteUser(1L));
        verify(userRepository, never()).deleteById(anyLong());
    }

    @Test
    void bulkDeleteUsersDeniesMixedAdminTargets() {
        User targetAdmin = user(2L, User.Role.ADMIN);
        User customer = user(3L, User.Role.CUSTOMER);

        when(userRepository.findAllById(eq(new HashSet<>(List.of(2L, 3L))))).thenReturn(List.of(targetAdmin, customer));

        BulkDeleteRequest request = new BulkDeleteRequest();
        request.setIds(List.of(2L, 3L));

        assertThrows(AccessDeniedException.class, () -> userService.bulkDeleteUsers(request));
        verify(userRepository, never()).deleteAllById(any());
    }

    @Test
    void bulkDeleteUsersThrowsWhenAnyIdMissing() {
        User customer = user(2L, User.Role.CUSTOMER);

        when(userRepository.findAllById(any())).thenReturn(List.of(customer));

        BulkDeleteRequest request = new BulkDeleteRequest();
        request.setIds(List.of(2L, 3L));

        assertThrows(ResourceNotFoundException.class, () -> userService.bulkDeleteUsers(request, User.Role.CUSTOMER));
        verify(userRepository, never()).deleteAllById(any());
    }

    @Test
    void createUserDeniesWrongRoleForScopedRoute() {
        UserRequest request = request(User.Role.ADMIN);

        assertThrows(AccessDeniedException.class, () -> userService.createUser(request, User.Role.CUSTOMER));
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void updateUserAllowsEditingOwnAdminAccount() {
        User actor = user(1L, User.Role.ADMIN);
        authenticate(actor);

        when(userRepository.findById(1L)).thenReturn(Optional.of(actor));
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        UserRequest request = request(User.Role.ADMIN);
        request.setFirstName("Updated");

        UserResponse response = userService.updateUser(1L, request);
        assertEquals("Updated", response.getFirstName());
    }

    @Test
    void updateUserDeniesPromotingAnotherUserToAdmin() {
        User actor = user(1L, User.Role.ADMIN);
        User target = user(2L, User.Role.CUSTOMER);
        authenticate(actor);

        when(userRepository.findById(2L)).thenReturn(Optional.of(target));
        when(userRepository.findById(1L)).thenReturn(Optional.of(actor));

        UserRequest request = request(User.Role.ADMIN);

        assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request));
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void scopedUpdateDeniesRoleEscalation() {
        User actor = user(1L, User.Role.ADMIN);
        User target = user(2L, User.Role.CUSTOMER);
        authenticate(actor);

        when(userRepository.findById(2L)).thenReturn(Optional.of(target));

        UserRequest request = request(User.Role.ADMIN);

        assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request, User.Role.CUSTOMER));
        verify(userRepository, never()).save(any(User.class));
    }

    private void authenticate(User user) {
        AppPrincipal principal = new AppPrincipal(user.getId(), user.getUsername(), user.getRole(), user.getTokenVersion());
        var context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(new UsernamePasswordAuthenticationToken(principal, "n/a", principal.getAuthorities()));
        SecurityContextHolder.setContext(context);
    }

    private User user(Long id, User.Role role) {
        User user = new User();
        user.setId(id);
        user.setUsername("user" + id);
        user.setFirstName("First");
        user.setLastName("Last");
        user.setEmail("user" + id + "@example.com");
        user.setRole(role);
        user.setActive(true);
        user.setTokenVersion(0);
        return user;
    }

    private UserRequest request(User.Role role) {
        UserRequest request = new UserRequest();
        request.setUsername("updated-user");
        request.setFirstName("First");
        request.setLastName("Last");
        request.setEmail("updated@example.com");
        request.setRole(role);
        request.setActive(true);
        return request;
    }

}
