package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.AuthenticateCommand;
import de.effigenix.application.usermanagement.dto.SessionToken;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId;
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.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("AuthenticateUser Use Case")
class AuthenticateUserTest {

    @Mock private UserRepository userRepository;
    @Mock private PasswordHasher passwordHasher;
    @Mock private SessionManager sessionManager;
    @Mock private AuditLogger auditLogger;
    @Mock private UnitOfWork unitOfWork;

    @InjectMocks
    private AuthenticateUser authenticateUser;

    private AuthenticateCommand validCommand;
    private User testUser;
    private PasswordHash validPasswordHash;
    private SessionToken sessionToken;

    @BeforeEach
    void setUp() {
        lenient().when(unitOfWork.executeAtomically(any()))
                .thenAnswer(inv -> ((Supplier<?>) inv.getArgument(0)).get());
        validCommand = new AuthenticateCommand("john.doe", "Password123!");
        validPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");

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

        sessionToken = new SessionToken("jwt-token", "Bearer", 3600L, OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(3600), "refresh-token");
    }

    @Test
    @DisplayName("should_AuthenticateUser_When_ValidCredentialsProvided")
    void should_AuthenticateUser_When_ValidCredentialsProvided() {
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
        when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue()).isEqualTo(sessionToken);
        verify(userRepository).save(any());
        verify(auditLogger).log(eq(AuditEvent.LOGIN_SUCCESS), anyString(), any());
    }

    @Test
    @DisplayName("should_FailWithInvalidCredentials_When_UserNotFound")
    void should_FailWithInvalidCredentials_When_UserNotFound() {
        when(userRepository.findByUsername("nonexistent")).thenReturn(Result.success(Optional.empty()));

        Result<UserError, SessionToken> result = authenticateUser.execute(new AuthenticateCommand("nonexistent", "Password123!"));

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
        verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString());
    }

    @Test
    @DisplayName("should_FailWithLockedUser_When_UserStatusIsLocked")
    void should_FailWithLockedUser_When_UserStatusIsLocked() {
        User lockedUser = User.reconstitute(
                UserId.of("user-2"), "john.doe", "john@example.com", validPasswordHash,
                new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
        );
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));

        Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserLocked.class);
        verify(auditLogger).log(eq(AuditEvent.LOGIN_BLOCKED), anyString(), any());
    }

    @Test
    @DisplayName("should_FailWithInactiveUser_When_UserStatusIsInactive")
    void should_FailWithInactiveUser_When_UserStatusIsInactive() {
        User inactiveUser = User.reconstitute(
                UserId.of("user-3"), "john.doe", "john@example.com", validPasswordHash,
                new HashSet<>(), "branch-1", UserStatus.INACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
        );
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(inactiveUser)));

        Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);

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

    @Test
    @DisplayName("should_FailWithInvalidCredentials_When_PasswordDoesNotMatch")
    void should_FailWithInvalidCredentials_When_PasswordDoesNotMatch() {
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("WrongPassword", validPasswordHash)).thenReturn(false);

        Result<UserError, SessionToken> result = authenticateUser.execute(new AuthenticateCommand("john.doe", "WrongPassword"));

        assertThat(result.isFailure()).isTrue();
        assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
        verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString(), any());
    }

    @Test
    @DisplayName("should_UpdateLastLoginTimestamp_When_AuthenticationSucceeds")
    void should_UpdateLastLoginTimestamp_When_AuthenticationSucceeds() {
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
        when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
        when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
        when(userRepository.save(any())).thenReturn(Result.success(null));

        Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);

        assertThat(result.isSuccess()).isTrue();
        // Verify save was called with a user (immutable, so it's a new instance)
        verify(userRepository).save(argThat(user -> user.lastLogin() != null));
    }

    @Test
    @DisplayName("should_NotCreateSession_When_UserLocked")
    void should_NotCreateSession_When_UserLocked() {
        User lockedUser = User.reconstitute(
                UserId.of("user-4"), "john.doe", "john@example.com", validPasswordHash,
                new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
        );
        when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));

        authenticateUser.execute(validCommand);

        verify(sessionManager, never()).createSession(any());
    }
}
