package com.saas.admin.service.impl;

import com.saas.admin.entity.Role;
import com.saas.admin.entity.User;
import com.saas.admin.repository.RoleRepository;
import com.saas.admin.repository.UserRepository;
import com.saas.shared.exception.BusinessException;
import com.saas.shared.exception.ErrorCode;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/**
 * Unit Tests for RBACServiceImpl
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("RBACService Unit Tests")
class RBACServiceImplTest {

    @Mock
    private RoleRepository roleRepository;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private RBACServiceImpl rbacService;

    private User testUser;
    private Role testRole;
    private Role adminRole;

    @BeforeEach
    void setUp() {
        testRole = Role.builder()
            .id(1L)
            .name("TENANT_USER")
            .description("Tenant user role")
            .isActive(true)
            .build();

        adminRole = Role.builder()
            .id(2L)
            .name("SYSTEM_ADMIN")
            .description("System administrator role")
            .isActive(true)
            .build();

        testUser = User.builder()
            .id(100L)
            .email("user@test.com")
            .firstName("Test")
            .lastName("User")
            .roles(new HashSet<>())
            .build();
    }

    @Test
    @DisplayName("Should create role successfully")
    void testCreateRole() {
        // Arrange
        when(roleRepository.findByName("NEW_ROLE")).thenReturn(Optional.empty());
        when(roleRepository.save(any(Role.class))).thenReturn(testRole);

        // Act
        Role result = rbacService.createRole("NEW_ROLE", "New role description");

        // Assert
        assertNotNull(result);
        verify(roleRepository, times(1)).findByName("NEW_ROLE");
        verify(roleRepository, times(1)).save(any(Role.class));
    }

    @Test
    @DisplayName("Should throw exception when creating duplicate role")
    void testCreateRoleDuplicate() {
        // Arrange
        when(roleRepository.findByName("TENANT_USER")).thenReturn(Optional.of(testRole));

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> rbacService.createRole("TENANT_USER", "Description"));
        
        assertEquals(ErrorCode.INVALID_INPUT, exception.getErrorCode());
        verify(roleRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should assign roles to user successfully")
    void testAssignRolesToUser() {
        // Arrange
        Set<Long> roleIds = Set.of(1L, 2L);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));
        when(roleRepository.findAllById(roleIds)).thenReturn(List.of(testRole, adminRole));
        when(userRepository.save(testUser)).thenReturn(testUser);

        // Act
        User result = rbacService.assignRolesToUser(100L, roleIds);

        // Assert
        assertNotNull(result);
        assertEquals(2, result.getRoles().size());
        verify(userRepository, times(1)).findById(100L);
        verify(roleRepository, times(1)).findAllById(roleIds);
        verify(userRepository, times(1)).save(testUser);
    }

    @Test
    @DisplayName("Should throw exception when user not found for role assignment")
    void testAssignRolesToUserNotFound() {
        // Arrange
        when(userRepository.findById(999L)).thenReturn(Optional.empty());

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> rbacService.assignRolesToUser(999L, Set.of(1L)));
        
        assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode());
    }

    @Test
    @DisplayName("Should throw exception when role not found")
    void testAssignRolesToUserRoleNotFound() {
        // Arrange
        Set<Long> roleIds = Set.of(1L, 999L);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));
        when(roleRepository.findAllById(roleIds)).thenReturn(List.of(testRole)); // Only 1 found

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> rbacService.assignRolesToUser(100L, roleIds));
        
        assertEquals(ErrorCode.INVALID_INPUT, exception.getErrorCode());
    }

    @Test
    @DisplayName("Should remove role from user successfully")
    void testRemoveRoleFromUser() {
        // Arrange
        testUser.getRoles().add(testRole);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));
        when(roleRepository.findById(1L)).thenReturn(Optional.of(testRole));
        when(userRepository.save(testUser)).thenReturn(testUser);

        // Act
        User result = rbacService.removeRoleFromUser(100L, 1L);

        // Assert
        assertNotNull(result);
        assertFalse(result.getRoles().contains(testRole));
        verify(userRepository, times(1)).save(testUser);
    }

    @Test
    @DisplayName("Should throw exception when removing non-assigned role")
    void testRemoveRoleNotAssigned() {
        // Arrange
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));
        when(roleRepository.findById(1L)).thenReturn(Optional.of(testRole));

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> rbacService.removeRoleFromUser(100L, 1L));
        
        assertEquals(ErrorCode.INVALID_INPUT, exception.getErrorCode());
    }

    @Test
    @DisplayName("Should get user roles successfully")
    void testGetUserRoles() {
        // Arrange
        testUser.getRoles().add(testRole);
        testUser.getRoles().add(adminRole);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));

        // Act
        Set<Role> result = rbacService.getUserRoles(100L);

        // Assert
        assertNotNull(result);
        assertEquals(2, result.size());
        verify(userRepository, times(1)).findById(100L);
    }

    @Test
    @DisplayName("Should check if user has specific role")
    void testHasRole() {
        // Arrange
        testUser.getRoles().add(testRole);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));

        // Act
        boolean result = rbacService.hasRole(100L, "TENANT_USER");

        // Assert
        assertTrue(result);
        verify(userRepository, times(1)).findById(100L);
    }

    @Test
    @DisplayName("Should return false when user does not have role")
    void testHasRoleNegative() {
        // Arrange
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));

        // Act
        boolean result = rbacService.hasRole(100L, "SYSTEM_ADMIN");

        // Assert
        assertFalse(result);
    }

    @Test
    @DisplayName("Should check if user has any of given roles")
    void testHasAnyRole() {
        // Arrange
        testUser.getRoles().add(testRole);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));

        // Act
        boolean result = rbacService.hasAnyRole(100L, Set.of("TENANT_USER", "SYSTEM_ADMIN"));

        // Assert
        assertTrue(result);
    }

    @Test
    @DisplayName("Should prevent duplicate role assignments")
    void testAssignDuplicateRoles() {
        // Arrange
        testUser.getRoles().add(testRole); // Already has this role
        Set<Long> roleIds = Set.of(1L);
        when(userRepository.findById(100L)).thenReturn(Optional.of(testUser));
        when(roleRepository.findAllById(roleIds)).thenReturn(List.of(testRole));
        when(userRepository.save(testUser)).thenReturn(testUser);

        // Act
        User result = rbacService.assignRolesToUser(100L, roleIds);

        // Assert
        assertEquals(1, result.getRoles().size()); // Still only 1 role
    }
}
