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

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

public sealed class InvitationTests
{
    private readonly TenantId _tenantId = TenantId.CreateUnique();
    private readonly UserId _invitedBy = UserId.CreateUnique();
    private const string TestEmail = "invite@example.com";
    private const string TestTokenHash = "hashed_token_value";

    [Fact]
    public void Create_WithValidData_ShouldSucceed()
    {
        // Arrange & Act
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);

        // Assert
        invitation.Should().NotBeNull();
        invitation.Id.Should().NotBeNull();
        invitation.TenantId.Should().Be(_tenantId);
        invitation.Email.Should().Be(TestEmail.ToLowerInvariant());
        invitation.Role.Should().Be(TenantRole.TenantMember);
        invitation.TokenHash.Should().Be(TestTokenHash);
        invitation.InvitedBy.Should().Be(_invitedBy);
        invitation.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow.AddDays(7), TimeSpan.FromSeconds(1));
        invitation.AcceptedAt.Should().BeNull();
        invitation.IsPending.Should().BeTrue();
        invitation.IsExpired.Should().BeFalse();
        invitation.IsAccepted.Should().BeFalse();
    }

    [Fact]
    public void Create_WithTenantOwnerRole_ShouldThrowException()
    {
        // Arrange & Act
        var act = () => Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantOwner,
            TestTokenHash,
            _invitedBy);

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot invite users with role TenantOwner*");
    }

    [Fact]
    public void Create_WithAIAgentRole_ShouldThrowException()
    {
        // Arrange & Act
        var act = () => Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.AIAgent,
            TestTokenHash,
            _invitedBy);

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot invite users with role AIAgent*");
    }

    [Fact]
    public void Create_WithEmptyEmail_ShouldThrowException()
    {
        // Arrange & Act
        var act = () => Invitation.Create(
            _tenantId,
            string.Empty,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);

        // Assert
        act.Should().Throw<ArgumentException>()
            .WithMessage("*Email cannot be empty*");
    }

    [Fact]
    public void Create_WithEmptyTokenHash_ShouldThrowException()
    {
        // Arrange & Act
        var act = () => Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            string.Empty,
            _invitedBy);

        // Assert
        act.Should().Throw<ArgumentException>()
            .WithMessage("*Token hash cannot be empty*");
    }

    [Fact]
    public void Create_ShouldRaiseUserInvitedEvent()
    {
        // Arrange & Act
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantAdmin,
            TestTokenHash,
            _invitedBy);

        // Assert
        invitation.DomainEvents.Should().ContainSingle();
        invitation.DomainEvents.Should().ContainItemsAssignableTo<UserInvitedEvent>();
        var domainEvent = invitation.DomainEvents.First() as UserInvitedEvent;
        domainEvent.Should().NotBeNull();
        domainEvent!.InvitationId.Should().Be(invitation.Id);
        domainEvent.TenantId.Should().Be(_tenantId);
        domainEvent.Email.Should().Be(TestEmail.ToLowerInvariant());
        domainEvent.Role.Should().Be(TenantRole.TenantAdmin);
        domainEvent.InvitedBy.Should().Be(_invitedBy);
    }

    [Fact]
    public void Accept_WhenPending_ShouldMarkAccepted()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.ClearDomainEvents();

        // Act
        invitation.Accept();

        // Assert
        invitation.IsAccepted.Should().BeTrue();
        invitation.IsPending.Should().BeFalse();
        invitation.AcceptedAt.Should().NotBeNull();
        invitation.AcceptedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
        invitation.DomainEvents.Should().ContainSingle();
        invitation.DomainEvents.Should().ContainItemsAssignableTo<InvitationAcceptedEvent>();
    }

    [Fact]
    public void Accept_WhenExpired_ShouldThrowException()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        // Force expiration by canceling
        invitation.Cancel();

        // Act
        var act = () => invitation.Accept();

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Invitation has expired*");
    }

    [Fact]
    public void Accept_WhenAlreadyAccepted_ShouldThrowException()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.Accept();

        // Act
        var act = () => invitation.Accept();

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Invitation has already been accepted*");
    }

    [Fact]
    public void Cancel_WhenPending_ShouldMarkCancelled()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.ClearDomainEvents();

        // Act
        invitation.Cancel();

        // Assert
        invitation.IsExpired.Should().BeTrue();
        invitation.IsPending.Should().BeFalse();
        invitation.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
        invitation.DomainEvents.Should().ContainSingle();
        invitation.DomainEvents.Should().ContainItemsAssignableTo<InvitationCancelledEvent>();
    }

    [Fact]
    public void Cancel_WhenAccepted_ShouldThrowException()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.Accept();

        // Act
        var act = () => invitation.Cancel();

        // Assert
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Cannot cancel non-pending invitation*");
    }

    [Fact]
    public void IsPending_WithPendingInvitation_ShouldReturnTrue()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);

        // Act & Assert
        invitation.IsPending.Should().BeTrue();
        invitation.IsExpired.Should().BeFalse();
        invitation.IsAccepted.Should().BeFalse();
    }

    [Fact]
    public void IsPending_WithAcceptedInvitation_ShouldReturnFalse()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.Accept();

        // Act & Assert
        invitation.IsPending.Should().BeFalse();
        invitation.IsAccepted.Should().BeTrue();
    }

    [Fact]
    public void ValidateForAcceptance_WithValidInvitation_ShouldNotThrow()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);

        // Act & Assert
        var act = () => invitation.ValidateForAcceptance();
        act.Should().NotThrow();
    }

    [Fact]
    public void ValidateForAcceptance_WithExpiredInvitation_ShouldThrow()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.Cancel(); // Force expiration

        // Act & Assert
        var act = () => invitation.ValidateForAcceptance();
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Invitation has expired*");
    }

    [Fact]
    public void ValidateForAcceptance_WithAcceptedInvitation_ShouldThrow()
    {
        // Arrange
        var invitation = Invitation.Create(
            _tenantId,
            TestEmail,
            TenantRole.TenantMember,
            TestTokenHash,
            _invitedBy);
        invitation.Accept();

        // Act & Assert
        var act = () => invitation.ValidateForAcceptance();
        act.Should().Throw<InvalidOperationException>()
            .WithMessage("*Invitation has already been accepted*");
    }
}
