using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
using FluentAssertions;
using Xunit;

namespace ColaFlow.Modules.Identity.Domain.Tests.Aggregates;

public sealed class UserTests
{
    private readonly TenantId _tenantId = TenantId.CreateUnique();

    [Fact]
    public void CreateLocal_ShouldSucceed()
    {
        // Arrange
        var email = Email.Create("test@example.com");
        var fullName = FullName.Create("John Doe");
        var passwordHash = "hashed_password";

        // Act
        var user = User.CreateLocal(_tenantId, email, passwordHash, fullName);

        // Assert
        user.Should().NotBeNull();
        user.Id.Should().NotBe(Guid.Empty);
        user.TenantId.Should().Be(_tenantId);
        user.Email.Value.Should().Be("test@example.com");
        user.FullName.Value.Should().Be("John Doe");
        user.PasswordHash.Should().Be(passwordHash);
        user.Status.Should().Be(UserStatus.Active);
        user.AuthProvider.Should().Be(AuthenticationProvider.Local);
        user.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void CreateLocal_ShouldRaiseUserCreatedEvent()
    {
        // Arrange
        var email = Email.Create("test@example.com");
        var fullName = FullName.Create("John Doe");

        // Act
        var user = User.CreateLocal(_tenantId, email, "password", fullName);

        // Assert
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<UserCreatedEvent>();
        var domainEvent = user.DomainEvents.First() as UserCreatedEvent;
        domainEvent.Should().NotBeNull();
        domainEvent!.UserId.Should().Be(user.Id);
        domainEvent.Email.Should().Be("test@example.com");
        domainEvent.TenantId.Should().Be(_tenantId);
    }

    [Fact]
    public void CreateFromSso_ShouldSucceed()
    {
        // Arrange
        var email = Email.Create("user@company.com");
        var fullName = FullName.Create("Jane Smith");
        var externalUserId = "google-12345";
        var avatarUrl = "https://example.com/avatar.jpg";

        // Act
        var user = User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.Google,
            externalUserId,
            email,
            fullName,
            avatarUrl);

        // Assert
        user.Should().NotBeNull();
        user.TenantId.Should().Be(_tenantId);
        user.AuthProvider.Should().Be(AuthenticationProvider.Google);
        user.ExternalUserId.Should().Be(externalUserId);
        user.ExternalEmail.Should().Be("user@company.com");
        user.AvatarUrl.Should().Be(avatarUrl);
        user.PasswordHash.Should().BeEmpty();
        user.EmailVerifiedAt.Should().NotBeNull(); // SSO users are auto-verified
    }

    [Fact]
    public void CreateFromSso_ShouldRaiseUserCreatedFromSsoEvent()
    {
        // Arrange
        var email = Email.Create("user@company.com");
        var fullName = FullName.Create("Jane Smith");

        // Act
        var user = User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.AzureAD,
            "azure-123",
            email,
            fullName);

        // Assert
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<UserCreatedFromSsoEvent>();
        var domainEvent = user.DomainEvents.First() as UserCreatedFromSsoEvent;
        domainEvent.Should().NotBeNull();
        domainEvent!.Provider.Should().Be(AuthenticationProvider.AzureAD);
    }

    [Fact]
    public void CreateFromSso_ShouldThrow_ForLocalProvider()
    {
        // Arrange
        var email = Email.Create("test@example.com");
        var fullName = FullName.Create("John Doe");

        // Act & Assert
        var act = () => User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.Local,
            "external-id",
            email,
            fullName);
        act.Should().Throw<ArgumentException>()
            .WithMessage("Use CreateLocal for local authentication");
    }

    [Fact]
    public void UpdatePassword_ShouldSucceed_ForLocalUser()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "old_hash",
            FullName.Create("John Doe"));
        user.ClearDomainEvents();

        // Act
        user.UpdatePassword("new_hash");

        // Assert
        user.PasswordHash.Should().Be("new_hash");
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<UserPasswordChangedEvent>();
    }

    [Fact]
    public void UpdatePassword_ShouldThrow_ForSsoUser()
    {
        // Arrange
        var user = User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.Google,
            "google-123",
            Email.Create("test@example.com"),
            FullName.Create("John Doe"));

        // Act & Assert
        var act = () => user.UpdatePassword("new_hash");
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("Cannot change password for SSO users");
    }

    [Fact]
    public void UpdateProfile_ShouldUpdateAllFields()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));

        // Act
        user.UpdateProfile(
            fullName: FullName.Create("Jane Smith"),
            avatarUrl: "https://example.com/avatar.jpg",
            jobTitle: "Software Engineer",
            phoneNumber: "+1234567890");

        // Assert
        user.FullName.Value.Should().Be("Jane Smith");
        user.AvatarUrl.Should().Be("https://example.com/avatar.jpg");
        user.JobTitle.Should().Be("Software Engineer");
        user.PhoneNumber.Should().Be("+1234567890");
    }

    [Fact]
    public void RecordLogin_ShouldUpdateLastLoginAt()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));

        // Act
        user.RecordLogin();

        // Assert
        user.LastLoginAt.Should().NotBeNull();
        user.LastLoginAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void VerifyEmail_ShouldSetEmailVerifiedAt()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));

        // Act
        user.VerifyEmail();

        // Assert
        user.EmailVerifiedAt.Should().NotBeNull();
        user.EmailVerifiedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
        user.EmailVerificationToken.Should().BeNull();
    }

    [Fact]
    public void Suspend_ShouldChangeStatus()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.ClearDomainEvents();

        // Act
        user.Suspend("Violation of terms");

        // Assert
        user.Status.Should().Be(UserStatus.Suspended);
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<UserSuspendedEvent>();
    }

    [Fact]
    public void Reactivate_ShouldChangeStatusToActive()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.Suspend("Test");

        // Act
        user.Reactivate();

        // Assert
        user.Status.Should().Be(UserStatus.Active);
    }

    [Fact]
    public void UpdateSsoProfile_ShouldSucceed_ForSsoUser()
    {
        // Arrange
        var user = User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.Google,
            "google-123",
            Email.Create("old@example.com"),
            FullName.Create("Old Name"));

        // Act
        user.UpdateSsoProfile(
            "google-456",
            Email.Create("new@example.com"),
            FullName.Create("New Name"),
            "https://new-avatar.jpg");

        // Assert
        user.ExternalUserId.Should().Be("google-456");
        user.ExternalEmail.Should().Be("new@example.com");
        user.FullName.Value.Should().Be("New Name");
        user.AvatarUrl.Should().Be("https://new-avatar.jpg");
    }

    [Fact]
    public void UpdateSsoProfile_ShouldThrow_ForLocalUser()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));

        // Act & Assert
        var act = () => user.UpdateSsoProfile(
            "external-id",
            Email.Create("new@example.com"),
            FullName.Create("New Name"));
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("Cannot update SSO profile for local users");
    }

    [Fact]
    public void VerifyEmail_WhenUnverified_ShouldSetVerifiedAt()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.ClearDomainEvents();

        // Act
        user.VerifyEmail();

        // Assert
        user.IsEmailVerified.Should().BeTrue();
        user.EmailVerifiedAt.Should().NotBeNull();
        user.EmailVerificationToken.Should().BeNull();
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<EmailVerifiedEvent>();
    }

    [Fact]
    public void VerifyEmail_WhenAlreadyVerified_ShouldBeIdempotent()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.VerifyEmail();
        var firstVerifiedAt = user.EmailVerifiedAt;
        user.ClearDomainEvents();

        // Act
        user.VerifyEmail();

        // Assert
        user.EmailVerifiedAt.Should().Be(firstVerifiedAt);
        user.DomainEvents.Should().BeEmpty(); // No new event for idempotent operation
    }

    [Fact]
    public void SetEmailVerificationToken_ShouldHashToken()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        const string token = "verification_token";

        // Act
        user.SetEmailVerificationToken(token);

        // Assert
        user.EmailVerificationToken.Should().Be(token);
    }

    [Fact]
    public void SetPasswordResetToken_ShouldSetTokenAndExpiration()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        const string token = "reset_token";
        var expiresAt = DateTime.UtcNow.AddHours(1);

        // Act
        user.SetPasswordResetToken(token, expiresAt);

        // Assert
        user.PasswordResetToken.Should().Be(token);
        user.PasswordResetTokenExpiresAt.Should().Be(expiresAt);
    }

    [Fact]
    public void SetPasswordResetToken_ForSsoUser_ShouldThrowException()
    {
        // Arrange
        var user = User.CreateFromSso(
            _tenantId,
            AuthenticationProvider.Google,
            "google-123",
            Email.Create("test@example.com"),
            FullName.Create("John Doe"));

        // Act
        var act = () => user.SetPasswordResetToken("token", DateTime.UtcNow.AddHours(1));

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot set password reset token for SSO users*");
    }

    [Fact]
    public void ClearPasswordResetToken_ShouldClearTokenAndExpiration()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.SetPasswordResetToken("token", DateTime.UtcNow.AddHours(1));

        // Act
        user.ClearPasswordResetToken();

        // Assert
        user.PasswordResetToken.Should().BeNull();
        user.PasswordResetTokenExpiresAt.Should().BeNull();
    }

    [Fact]
    public void RecordLoginWithEvent_ShouldUpdateLastLogin()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.ClearDomainEvents();

        // Act
        user.RecordLoginWithEvent(_tenantId, "192.168.1.1", "Mozilla/5.0");

        // Assert
        user.LastLoginAt.Should().NotBeNull();
        user.LastLoginAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void RecordLoginWithEvent_ShouldRaiseDomainEvent()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.ClearDomainEvents();
        const string ipAddress = "192.168.1.1";
        const string userAgent = "Mozilla/5.0";

        // Act
        user.RecordLoginWithEvent(_tenantId, ipAddress, userAgent);

        // Assert
        user.DomainEvents.Should().ContainSingle();
        user.DomainEvents.Should().ContainItemsAssignableTo<UserLoggedInEvent>();
        var domainEvent = user.DomainEvents.First() as UserLoggedInEvent;
        domainEvent.Should().NotBeNull();
        domainEvent!.UserId.Should().Be(user.Id);
        domainEvent.TenantId.Should().Be(_tenantId);
        domainEvent.IpAddress.Should().Be(ipAddress);
        domainEvent.UserAgent.Should().Be(userAgent);
    }

    [Fact]
    public void Delete_ShouldChangeStatusToDeleted()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));

        // Act
        user.Delete();

        // Assert
        user.Status.Should().Be(UserStatus.Deleted);
        user.UpdatedAt.Should().NotBeNull();
    }

    [Fact]
    public void Suspend_WithDeletedUser_ShouldThrowException()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.Delete();

        // Act
        var act = () => user.Suspend("Test reason");

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot suspend deleted user*");
    }

    [Fact]
    public void Reactivate_WithDeletedUser_ShouldThrowException()
    {
        // Arrange
        var user = User.CreateLocal(
            _tenantId,
            Email.Create("test@example.com"),
            "hash",
            FullName.Create("John Doe"));
        user.Delete();

        // Act
        var act = () => user.Reactivate();

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot reactivate deleted user*");
    }
}
