package de.effigenix.domain.usermanagement;

import de.effigenix.shared.common.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static org.assertj.core.api.Assertions.*;

/**
 * Unit tests for User Entity.
 * Tests validation, immutable business methods, status transitions, role assignment, and permissions.
 */
@DisplayName("User Entity")
class UserTest {

    private UserId userId;
    private String username;
    private String email;
    private PasswordHash passwordHash;
    private Set<Role> roles;
    private String branchId;
    private OffsetDateTime createdAt;

    @BeforeEach
    void setUp() {
        userId = UserId.generate();
        username = "john.doe";
        email = "john@example.com";
        passwordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
        roles = new HashSet<>();
        branchId = "branch-1";
        createdAt = OffsetDateTime.now(ZoneOffset.UTC);
    }

    @Test
    @DisplayName("should_CreateUser_When_ValidDataProvided")
    void should_CreateUser_When_ValidDataProvided() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, createdAt, null);

        assertThat(user.id()).isEqualTo(userId);
        assertThat(user.username()).isEqualTo(username);
        assertThat(user.email()).isEqualTo(email);
        assertThat(user.passwordHash()).isEqualTo(passwordHash);
        assertThat(user.branchId()).isEqualTo(branchId);
        assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
        assertThat(user.createdAt()).isEqualTo(createdAt);
        assertThat(user.lastLogin()).isNull();
    }

    @Test
    @DisplayName("should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt")
    void should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt() {
        OffsetDateTime before = OffsetDateTime.now(ZoneOffset.UTC);
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, null, null);
        OffsetDateTime after = OffsetDateTime.now(ZoneOffset.UTC);

        assertThat(user.createdAt()).isNotNull();
        assertThat(user.createdAt()).isBetween(before, after);
    }

    @Test
    @DisplayName("should_ReturnFailure_When_NullUsernameProvided")
    void should_ReturnFailure_When_NullUsernameProvided() {
        Result<UserError, User> result = User.create(null, email, passwordHash, roles, branchId);

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

    @Test
    @DisplayName("should_ReturnFailure_When_EmptyUsernameProvided")
    void should_ReturnFailure_When_EmptyUsernameProvided() {
        Result<UserError, User> result = User.create("", email, passwordHash, roles, branchId);

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

    @Test
    @DisplayName("should_ReturnFailure_When_InvalidEmailProvided")
    void should_ReturnFailure_When_InvalidEmailProvided() {
        Result<UserError, User> result = User.create(username, "invalid-email", passwordHash, roles, branchId);

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

    @ParameterizedTest
    @ValueSource(strings = {"", "   ", "notanemail"})
    @DisplayName("should_ReturnFailure_When_InvalidEmailFormatsProvided")
    void should_ReturnFailure_When_InvalidEmailFormatsProvided(String invalidEmail) {
        Result<UserError, User> result = User.create(username, invalidEmail, passwordHash, roles, branchId);

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

    @Test
    @DisplayName("should_ReturnFailure_When_NullPasswordHashProvided")
    void should_ReturnFailure_When_NullPasswordHashProvided() {
        Result<UserError, User> result = User.create(username, email, null, roles, branchId);

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

    @Test
    @DisplayName("should_CreateUser_When_FactoryMethodCalled")
    void should_CreateUser_When_FactoryMethodCalled() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        assertThat(user.username()).isEqualTo(username);
        assertThat(user.email()).isEqualTo(email);
        assertThat(user.passwordHash()).isEqualTo(passwordHash);
        assertThat(user.branchId()).isEqualTo(branchId);
        assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
        assertThat(user.createdAt()).isNotNull();
        assertThat(user.lastLogin()).isNull();
        assertThat(user.id()).isNotNull();
    }

    @Test
    @DisplayName("should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled")
    void should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);

        User updated = user.withLastLogin(now).unsafeGetValue();

        assertThat(updated.lastLogin()).isEqualTo(now);
        assertThat(user.lastLogin()).isNull(); // original unchanged
        assertThat(updated.id()).isEqualTo(user.id());
    }

    @Test
    @DisplayName("should_ReturnNewUserWithPassword_When_ChangePasswordCalled")
    void should_ReturnNewUserWithPassword_When_ChangePasswordCalled() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        PasswordHash newHash = new PasswordHash("$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");

        Result<UserError, User> result = user.changePassword(newHash);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().passwordHash()).isEqualTo(newHash);
        assertThat(user.passwordHash()).isEqualTo(passwordHash); // original unchanged
    }

    @Test
    @DisplayName("should_ReturnFailure_When_NullPasswordHashProvidedToChangePassword")
    void should_ReturnFailure_When_NullPasswordHashProvidedToChangePassword() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.changePassword(null);

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

    // ==================== Status Transition Tests ====================

    @Test
    @DisplayName("should_LockUser_When_StatusIsActive")
    void should_LockUser_When_StatusIsActive() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.lock();

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.LOCKED);
        assertThat(result.unsafeGetValue().isLocked()).isTrue();
        assertThat(user.status()).isEqualTo(UserStatus.ACTIVE); // original unchanged
    }

    @Test
    @DisplayName("should_FailLock_When_StatusIsLocked")
    void should_FailLock_When_StatusIsLocked() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.LOCKED, createdAt, null);

        Result<UserError, User> result = user.lock();

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

    @Test
    @DisplayName("should_FailLock_When_StatusIsInactive")
    void should_FailLock_When_StatusIsInactive() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);

        Result<UserError, User> result = user.lock();

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

    @Test
    @DisplayName("should_UnlockUser_When_StatusIsLocked")
    void should_UnlockUser_When_StatusIsLocked() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        User locked = user.lock().unsafeGetValue();

        Result<UserError, User> result = locked.unlock();

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

    @Test
    @DisplayName("should_FailUnlock_When_StatusIsActive")
    void should_FailUnlock_When_StatusIsActive() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.unlock();

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

    @Test
    @DisplayName("should_DeactivateUser_When_StatusIsActive")
    void should_DeactivateUser_When_StatusIsActive() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.deactivate();

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

    @Test
    @DisplayName("should_DeactivateUser_When_StatusIsLocked")
    void should_DeactivateUser_When_StatusIsLocked() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.LOCKED, createdAt, null);

        Result<UserError, User> result = user.deactivate();

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

    @Test
    @DisplayName("should_FailDeactivate_When_StatusIsInactive")
    void should_FailDeactivate_When_StatusIsInactive() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);

        Result<UserError, User> result = user.deactivate();

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

    @Test
    @DisplayName("should_ActivateUser_When_StatusIsInactive")
    void should_ActivateUser_When_StatusIsInactive() {
        User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);

        Result<UserError, User> result = user.activate();

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

    @Test
    @DisplayName("should_FailActivate_When_StatusIsActive")
    void should_FailActivate_When_StatusIsActive() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.activate();

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

    // ==================== Role Tests ====================

    @Test
    @DisplayName("should_AssignRole_When_RoleProvided")
    void should_AssignRole_When_RoleProvided() {
        User user = User.create(username, email, passwordHash, new HashSet<>(), branchId).unsafeGetValue();
        Role role = createTestRole("ADMIN");

        Result<UserError, User> result = user.assignRole(role);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().roles()).contains(role);
        assertThat(user.roles()).doesNotContain(role); // original unchanged
    }

    @Test
    @DisplayName("should_ReturnFailure_When_NullRoleProvidedToAssignRole")
    void should_ReturnFailure_When_NullRoleProvidedToAssignRole() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.assignRole(null);

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

    @Test
    @DisplayName("should_RemoveRole_When_RoleProvided")
    void should_RemoveRole_When_RoleProvided() {
        Role role = createTestRole("ADMIN");
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();

        Result<UserError, User> result = user.removeRole(role);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().roles()).doesNotContain(role);
        assertThat(user.roles()).contains(role); // original unchanged
    }

    @Test
    @DisplayName("should_ReturnFailure_When_NullRoleProvidedToRemoveRole")
    void should_ReturnFailure_When_NullRoleProvidedToRemoveRole() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.removeRole(null);

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

    // ==================== Email & Branch Tests ====================

    @Test
    @DisplayName("should_UpdateEmail_When_ValidEmailProvided")
    void should_UpdateEmail_When_ValidEmailProvided() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        String newEmail = "newemail@example.com";

        Result<UserError, User> result = user.updateEmail(newEmail);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().email()).isEqualTo(newEmail);
        assertThat(user.email()).isEqualTo(email); // original unchanged
    }

    @Test
    @DisplayName("should_ReturnFailure_When_InvalidEmailProvidedToUpdateEmail")
    void should_ReturnFailure_When_InvalidEmailProvidedToUpdateEmail() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        Result<UserError, User> result = user.updateEmail("invalid-email");

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

    @Test
    @DisplayName("should_UpdateBranch_When_BranchIdProvided")
    void should_UpdateBranch_When_BranchIdProvided() {
        User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        String newBranchId = "branch-2";

        Result<UserError, User> result = user.updateBranch(newBranchId);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.unsafeGetValue().branchId()).isEqualTo(newBranchId);
        assertThat(user.branchId()).isEqualTo(branchId); // original unchanged
    }

    // ==================== Permission Tests ====================

    @Test
    @DisplayName("should_ReturnAllPermissions_When_GetAllPermissionsMethodCalled")
    void should_ReturnAllPermissions_When_GetAllPermissionsMethodCalled() {
        Set<Permission> role1Perms = Set.of(Permission.USER_READ, Permission.USER_WRITE);
        Set<Permission> role2Perms = Set.of(Permission.ROLE_READ, Permission.ROLE_WRITE);
        Role role1 = createRoleWithPermissions("ADMIN", role1Perms);
        Role role2 = createRoleWithPermissions("PRODUCTION_MANAGER", role2Perms);
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role1, role2)), branchId).unsafeGetValue();

        Set<Permission> allPermissions = user.getAllPermissions();

        assertThat(allPermissions).contains(Permission.USER_READ, Permission.USER_WRITE, Permission.ROLE_READ, Permission.ROLE_WRITE);
    }

    @Test
    @DisplayName("should_ReturnEmptyPermissions_When_UserHasNoRoles")
    void should_ReturnEmptyPermissions_When_UserHasNoRoles() {
        User user = User.create(username, email, passwordHash, new HashSet<>(), branchId).unsafeGetValue();

        assertThat(user.getAllPermissions()).isEmpty();
    }

    @Test
    @DisplayName("should_CheckPermission_When_UserHasPermission")
    void should_CheckPermission_When_UserHasPermission() {
        Role role = createRoleWithPermissions("ADMIN", Set.of(Permission.USER_READ, Permission.USER_WRITE));
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();

        assertThat(user.hasPermission(Permission.USER_READ)).isTrue();
        assertThat(user.hasPermission(Permission.USER_WRITE)).isTrue();
    }

    @Test
    @DisplayName("should_CheckPermission_When_UserLacksPermission")
    void should_CheckPermission_When_UserLacksPermission() {
        Role role = createRoleWithPermissions("PRODUCTION_WORKER", Set.of(Permission.USER_READ));
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();

        assertThat(user.hasPermission(Permission.USER_DELETE)).isFalse();
    }

    // ==================== Equality Tests ====================

    @Test
    @DisplayName("should_BeEqualToAnother_When_BothHaveSameId")
    void should_BeEqualToAnother_When_BothHaveSameId() {
        User user1 = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, createdAt, null);
        User user2 = User.reconstitute(userId, "different_username", "different@example.com", passwordHash, roles, "different-branch", UserStatus.INACTIVE, createdAt, null);

        assertThat(user1).isEqualTo(user2);
        assertThat(user1.hashCode()).isEqualTo(user2.hashCode());
    }

    @Test
    @DisplayName("should_NotBeEqual_When_DifferentIds")
    void should_NotBeEqual_When_DifferentIds() {
        User user1 = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
        User user2 = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();

        assertThat(user1).isNotEqualTo(user2);
    }

    @Test
    @DisplayName("should_ReturnUnmodifiableRoleSet_When_RolesRetrieved")
    void should_ReturnUnmodifiableRoleSet_When_RolesRetrieved() {
        Role role = createTestRole("ADMIN");
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();

        Set<Role> retrievedRoles = user.roles();

        assertThatThrownBy(() -> retrievedRoles.add(createTestRole("PRODUCTION_MANAGER")))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    @DisplayName("should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved")
    void should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved() {
        Role role = createRoleWithPermissions("ADMIN", Set.of(Permission.USER_READ));
        User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();

        Set<Permission> permissions = user.getAllPermissions();

        assertThatThrownBy(() -> permissions.add(Permission.USER_WRITE))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    @DisplayName("should_PreserveNullRolesAsEmptySet_When_NullRolesProvided")
    void should_PreserveNullRolesAsEmptySet_When_NullRolesProvided() {
        User user = User.reconstitute(userId, username, email, passwordHash, null, branchId, UserStatus.ACTIVE, createdAt, null);

        assertThat(user.roles()).isEmpty();
    }

    // ==================== Helper Methods ====================

    private Role createTestRole(String roleName) {
        return Role.reconstitute(RoleId.generate(), RoleName.valueOf(roleName), new HashSet<>(), "Test role: " + roleName);
    }

    private Role createRoleWithPermissions(String roleName, Set<Permission> permissions) {
        return Role.reconstitute(RoleId.generate(), RoleName.valueOf(roleName), new HashSet<>(permissions), "Test role: " + roleName);
    }
}
