package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.UpdateUserCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
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.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
import java.util.function.Supplier;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("UpdateUser Use Case")
class UpdateUserTest {

    @Mock private UserRepository userRepository;
    @Mock private AuditLogger auditLogger;
    @Mock private AuthorizationPort authPort;
    @Mock private UnitOfWork unitOfWork;

    private UpdateUser updateUser;
    private ActorId performedBy;
    private User testUser;

    @BeforeEach
    void setUp() {
        lenient().when(unitOfWork.executeAtomically(any()))
                .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
        updateUser = new UpdateUser(userRepository, auditLogger, authPort, unitOfWork);
        performedBy = ActorId.of("admin-user");
        testUser = User.reconstitute(
                UserId.of("user-1"), "john.doe", "john@example.com",
                new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
                new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
        );
    }

    @Test
    @DisplayName("should_UpdateEmail_When_NewEmailProvided")
    void should_UpdateEmail_When_NewEmailProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
        when(userRepository.existsByEmail("newemail@example.com")).thenReturn(Result.success(false));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", "newemail@example.com", null), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().email()).isEqualTo("newemail@example.com");
        verify(userRepository).save(argThat(user -> "newemail@example.com".equals(user.email())));
        verify(auditLogger).log(eq(AuditEvent.USER_UPDATED), eq("user-1"), eq(performedBy));
    }

    @Test
    @DisplayName("should_UpdateBranch_When_NewBranchProvided")
    void should_UpdateBranch_When_NewBranchProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", null, "branch-2"), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().branchId()).isEqualTo("branch-2");
        verify(userRepository).save(argThat(user -> "branch-2".equals(user.branchId())));
    }

    @Test
    @DisplayName("should_UpdateEmailAndBranch_When_BothProvided")
    void should_UpdateEmailAndBranch_When_BothProvided() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
        when(userRepository.existsByEmail("new@example.com")).thenReturn(Result.success(false));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", "new@example.com", "branch-2"), performedBy);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().email()).isEqualTo("new@example.com");
        assertThat(result.unsafeGetValue().branchId()).isEqualTo("branch-2");
    }

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

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", "new@example.com", null), performedBy);

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

    @Test
    @DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
    void should_FailWithUserNotFound_When_UserDoesNotExist() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("nonexistent", "new@example.com", null), performedBy);

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

    @Test
    @DisplayName("should_FailWithEmailAlreadyExists_When_DuplicateEmail")
    void should_FailWithEmailAlreadyExists_When_DuplicateEmail() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
        when(userRepository.existsByEmail("taken@example.com")).thenReturn(Result.success(true));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", "taken@example.com", null), performedBy);

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

    @Test
    @DisplayName("should_NotCheckEmailUniqueness_When_EmailUnchanged")
    void should_NotCheckEmailUniqueness_When_EmailUnchanged() {
        when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
        when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, UserDTO> result = updateUser.execute(
                new UpdateUserCommand("user-1", "john@example.com", "branch-2"), performedBy);

        assertThat(result.isSuccess()).isTrue();
        verify(userRepository, never()).existsByEmail(anyString());
    }
}
