package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.CreateUserCommand;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.AuthorizationPort;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("CreateUser Use Case")
class CreateUserTest {

    @Mock private UserRepository userRepository;
    @Mock private RoleRepository roleRepository;
    @Mock private PasswordHasher passwordHasher;
    @Mock private AuditLogger auditLogger;
    @Mock private AuthorizationPort authPort;
    @Mock private UnitOfWork unitOfWork;

    private CreateUser createUser;
    private CreateUserCommand validCommand;
    private ActorId performedBy;
    private PasswordHash validPasswordHash;

    @BeforeEach
    void setUp() {
        lenient().when(unitOfWork.executeAtomically(any()))
                .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
        createUser = new CreateUser(userRepository, roleRepository, passwordHasher, auditLogger, authPort, unitOfWork);
        performedBy = ActorId.of("admin-user");
        validPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
        validCommand = new CreateUserCommand("john.doe", "john@example.com", "Password123!", Set.of(RoleName.PRODUCTION_WORKER), "branch-1");
    }

    @Test
    @DisplayName("should_CreateUser_When_ValidCommandAndUniqueDetailsProvided")
    void should_CreateUser_When_ValidCommandAndUniqueDetailsProvided() {
        Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker");

        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
        when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
        when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
        when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
        when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().username()).isEqualTo("john.doe");
        verify(userRepository).save(any());
        verify(auditLogger).log(eq(AuditEvent.USER_CREATED), anyString(), eq(performedBy));
    }

    @Test
    @DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
    void should_FailWithUnauthorized_When_ActorLacksPermission() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(false);

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithInvalidInput_When_EmptyRoleNamesProvided")
    void should_FailWithInvalidInput_When_EmptyRoleNamesProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        var cmd = new CreateUserCommand("john.doe", "john@example.com", "Password123!", Set.of(), "branch-1");

        var result = createUser.execute(cmd, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
    }

    @Test
    @DisplayName("should_FailWithInvalidPassword_When_WeakPasswordProvided")
    void should_FailWithInvalidPassword_When_WeakPasswordProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(false);

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidPassword.class);
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithUsernameExists_When_DuplicateUsernameProvided")
    void should_FailWithUsernameExists_When_DuplicateUsernameProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
        when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(true));

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.UsernameAlreadyExists.class);
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithEmailExists_When_DuplicateEmailProvided")
    void should_FailWithEmailExists_When_DuplicateEmailProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
        when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
        when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(true));

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.EmailAlreadyExists.class);
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithRoleNotFound_When_RoleNotFound")
    void should_FailWithRoleNotFound_When_RoleNotFound() {
        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
        when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
        when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
        when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
        when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.empty()));

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.RoleNotFound.class);
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_CreateActiveUser_When_UserCreatedSuccessfully")
    void should_CreateActiveUser_When_UserCreatedSuccessfully() {
        Role role = Role.reconstitute(RoleId.generate(), RoleName.ADMIN, new HashSet<>(), "Admin");

        when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
        when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
        when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
        when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
        when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
        when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        var result = createUser.execute(validCommand, performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.ACTIVE);
    }
}
