package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.ChangePasswordCommand;
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("ChangePassword Use Case")
class ChangePasswordTest {

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

    private ChangePassword changePassword;
    private User testUser;
    private PasswordHash oldPasswordHash;
    private PasswordHash newPasswordHash;
    private ChangePasswordCommand validCommand;
    private ActorId performedBy;

    @BeforeEach
    void setUp() {
        lenient().when(unitOfWork.executeAtomically(any()))
                .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
        changePassword = new ChangePassword(userRepository, passwordHasher, auditLogger, authPort, unitOfWork);
        oldPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
        newPasswordHash = new PasswordHash("$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");

        testUser = User.reconstitute(
                UserId.of("user-123"), "john.doe", "john@example.com", oldPasswordHash,
                new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
        );

        validCommand = new ChangePasswordCommand("user-123", "OldPassword123!", "NewPassword456!");
        performedBy = ActorId.of("user-123"); // self-service
    }

    @Test
    @DisplayName("should_ChangePassword_When_ValidCurrentPasswordProvided_SelfService")
    void should_ChangePassword_When_ValidCurrentPasswordProvided_SelfService() {
        // Self-service: no authPort check needed
        when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
        when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
        when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);

        assertThat(result.isSuccess()).isTrue();
        verify(userRepository).save(any(User.class));
        verify(auditLogger).log(eq(AuditEvent.PASSWORD_CHANGED), eq("user-123"), eq(performedBy));
    }

    @Test
    @DisplayName("should_FailWithUnauthorized_When_DifferentActorWithoutPermission")
    void should_FailWithUnauthorized_When_DifferentActorWithoutPermission() {
        ActorId otherActor = ActorId.of("other-user");
        when(authPort.can(otherActor, UserManagementAction.PASSWORD_CHANGE)).thenReturn(false);

        Result<UserError, Void> result = changePassword.execute(validCommand, otherActor);

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

    @Test
    @DisplayName("should_FailWithUserNotFound_When_UserIdDoesNotExist")
    void should_FailWithUserNotFound_When_UserIdDoesNotExist() {
        when(userRepository.findById(UserId.of("nonexistent-id"))).thenReturn(Result.success(Optional.empty()));
        ChangePasswordCommand command = new ChangePasswordCommand("nonexistent-id", "OldPassword123!", "NewPassword456!");
        ActorId actor = ActorId.of("nonexistent-id");

        Result<UserError, Void> result = changePassword.execute(command, actor);

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

    @Test
    @DisplayName("should_FailWithInvalidCredentials_When_CurrentPasswordIncorrect")
    void should_FailWithInvalidCredentials_When_CurrentPasswordIncorrect() {
        when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("WrongPassword", oldPasswordHash)).thenReturn(false);
        ChangePasswordCommand command = new ChangePasswordCommand("user-123", "WrongPassword", "NewPassword456!");

        Result<UserError, Void> result = changePassword.execute(command, performedBy);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
        verify(auditLogger).log(eq(AuditEvent.PASSWORD_CHANGE_FAILED), eq("user-123"), eq(performedBy));
        verify(userRepository, never()).save(any());
    }

    @Test
    @DisplayName("should_FailWithInvalidPassword_When_NewPasswordTooWeak")
    void should_FailWithInvalidPassword_When_NewPasswordTooWeak() {
        when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
        when(passwordHasher.isValidPassword("weak")).thenReturn(false);
        ChangePasswordCommand command = new ChangePasswordCommand("user-123", "OldPassword123!", "weak");

        Result<UserError, Void> result = changePassword.execute(command, performedBy);

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

    @Test
    @DisplayName("should_FailWithInvalidInput_When_BlankUserId")
    void should_FailWithInvalidInput_When_BlankUserId() {
        ChangePasswordCommand command = new ChangePasswordCommand("", "OldPassword123!", "NewPassword456!");

        Result<UserError, Void> result = changePassword.execute(command, performedBy);

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