using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Events;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
using FluentAssertions;

namespace ColaFlow.Domain.Tests.Aggregates;

/// <summary>
/// Unit tests for Project aggregate root
/// </summary>
public class ProjectTests
{
    #region Create Tests

    [Fact]
    public void Create_WithValidData_ShouldCreateProject()
    {
        // Arrange
        var name = "Test Project";
        var description = "Test Description";
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), name, description, key, ownerId);

        // Assert
        project.Should().NotBeNull();
        project.Name.Should().Be(name);
        project.Description.Should().Be(description);
        project.Key.Value.Should().Be(key);
        project.OwnerId.Should().Be(ownerId);
        project.Status.Should().Be(ProjectStatus.Active);
        project.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
        project.UpdatedAt.Should().BeNull();
        project.Epics.Should().BeEmpty();
    }

    [Fact]
    public void Create_WithValidData_ShouldRaiseProjectCreatedEvent()
    {
        // Arrange
        var name = "Test Project";
        var description = "Test Description";
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), name, description, key, ownerId);

        // Assert
        project.DomainEvents.Should().ContainSingle();
        var domainEvent = project.DomainEvents.First();
        domainEvent.Should().BeOfType<ProjectCreatedEvent>();

        var createdEvent = (ProjectCreatedEvent)domainEvent;
        createdEvent.ProjectId.Should().Be(project.Id);
        createdEvent.ProjectName.Should().Be(name);
        createdEvent.CreatedBy.Should().Be(ownerId);
    }

    [Fact]
    public void Create_WithNullDescription_ShouldCreateProjectWithEmptyDescription()
    {
        // Arrange
        var name = "Test Project";
        string? description = null;
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), name, description!, key, ownerId);

        // Assert
        project.Should().NotBeNull();
        project.Description.Should().Be(string.Empty);
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public void Create_WithEmptyName_ShouldThrowDomainException(string invalidName)
    {
        // Arrange
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        Action act = () => Project.Create(TenantId.Create(Guid.NewGuid()), invalidName, "Description", key, ownerId);

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project name cannot be empty");
    }

    [Fact]
    public void Create_WithNameExceeding200Characters_ShouldThrowDomainException()
    {
        // Arrange
        var name = new string('A', 201);
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        Action act = () => Project.Create(TenantId.Create(Guid.NewGuid()), name, "Description", key, ownerId);

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project name cannot exceed 200 characters");
    }

    [Fact]
    public void Create_WithNameExactly200Characters_ShouldSucceed()
    {
        // Arrange
        var name = new string('A', 200);
        var key = "TEST";
        var ownerId = UserId.Create();

        // Act
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), name, "Description", key, ownerId);

        // Assert
        project.Should().NotBeNull();
        project.Name.Should().Be(name);
    }

    #endregion

    #region UpdateDetails Tests

    [Fact]
    public void UpdateDetails_WithValidData_ShouldUpdateProject()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Original Name", "Original Description", "TEST", UserId.Create());
        var originalCreatedAt = project.CreatedAt;
        var newName = "Updated Name";
        var newDescription = "Updated Description";

        // Act
        project.UpdateDetails(newName, newDescription);

        // Assert
        project.Name.Should().Be(newName);
        project.Description.Should().Be(newDescription);
        project.CreatedAt.Should().Be(originalCreatedAt); // CreatedAt should not change
        project.UpdatedAt.Should().NotBeNull();
        project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void UpdateDetails_WhenCalled_ShouldRaiseProjectUpdatedEvent()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Original Name", "Original Description", "TEST", UserId.Create());
        project.ClearDomainEvents(); // Clear creation event
        var newName = "Updated Name";
        var newDescription = "Updated Description";

        // Act
        project.UpdateDetails(newName, newDescription);

        // Assert
        project.DomainEvents.Should().ContainSingle();
        var domainEvent = project.DomainEvents.First();
        domainEvent.Should().BeOfType<ProjectUpdatedEvent>();

        var updatedEvent = (ProjectUpdatedEvent)domainEvent;
        updatedEvent.ProjectId.Should().Be(project.Id);
        updatedEvent.Name.Should().Be(newName);
        updatedEvent.Description.Should().Be(newDescription);
    }

    [Fact]
    public void UpdateDetails_WithNullDescription_ShouldSetEmptyDescription()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Original Name", "Original Description", "TEST", UserId.Create());

        // Act
        project.UpdateDetails("Updated Name", null!);

        // Assert
        project.Description.Should().Be(string.Empty);
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public void UpdateDetails_WithEmptyName_ShouldThrowDomainException(string invalidName)
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Original Name", "Original Description", "TEST", UserId.Create());

        // Act
        Action act = () => project.UpdateDetails(invalidName, "Updated Description");

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project name cannot be empty");
    }

    [Fact]
    public void UpdateDetails_WithNameExceeding200Characters_ShouldThrowDomainException()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Original Name", "Original Description", "TEST", UserId.Create());
        var name = new string('A', 201);

        // Act
        Action act = () => project.UpdateDetails(name, "Updated Description");

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project name cannot exceed 200 characters");
    }

    #endregion

    #region CreateEpic Tests

    [Fact]
    public void CreateEpic_WithValidData_ShouldCreateEpic()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.ClearDomainEvents();
        var epicName = "Epic 1";
        var epicDescription = "Epic Description";
        var createdBy = UserId.Create();

        // Act
        var epic = project.CreateEpic(epicName, epicDescription, createdBy);

        // Assert
        epic.Should().NotBeNull();
        epic.Name.Should().Be(epicName);
        epic.Description.Should().Be(epicDescription);
        epic.ProjectId.Should().Be(project.Id);
        epic.CreatedBy.Should().Be(createdBy);
        project.Epics.Should().ContainSingle();
        project.Epics.Should().Contain(epic);
    }

    [Fact]
    public void CreateEpic_WhenCalled_ShouldRaiseEpicCreatedEvent()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.ClearDomainEvents();
        var epicName = "Epic 1";
        var createdBy = UserId.Create();

        // Act
        var epic = project.CreateEpic(epicName, "Epic Description", createdBy);

        // Assert
        project.DomainEvents.Should().ContainSingle();
        var domainEvent = project.DomainEvents.First();
        domainEvent.Should().BeOfType<EpicCreatedEvent>();

        var epicCreatedEvent = (EpicCreatedEvent)domainEvent;
        epicCreatedEvent.EpicId.Should().Be(epic.Id);
        epicCreatedEvent.EpicName.Should().Be(epicName);
        epicCreatedEvent.ProjectId.Should().Be(project.Id);
    }

    [Fact]
    public void CreateEpic_InArchivedProject_ShouldThrowDomainException()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.Archive();
        var createdBy = UserId.Create();

        // Act
        Action act = () => project.CreateEpic("Epic 1", "Description", createdBy);

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Cannot create epic in an archived project");
    }

    [Fact]
    public void CreateEpic_MultipleEpics_ShouldAddToCollection()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        var createdBy = UserId.Create();

        // Act
        var epic1 = project.CreateEpic("Epic 1", "Description 1", createdBy);
        var epic2 = project.CreateEpic("Epic 2", "Description 2", createdBy);
        var epic3 = project.CreateEpic("Epic 3", "Description 3", createdBy);

        // Assert
        project.Epics.Should().HaveCount(3);
        project.Epics.Should().Contain(new[] { epic1, epic2, epic3 });
    }

    #endregion

    #region Archive Tests

    [Fact]
    public void Archive_ActiveProject_ShouldArchiveProject()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());

        // Act
        project.Archive();

        // Assert
        project.Status.Should().Be(ProjectStatus.Archived);
        project.UpdatedAt.Should().NotBeNull();
        project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void Archive_WhenCalled_ShouldRaiseProjectArchivedEvent()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.ClearDomainEvents();

        // Act
        project.Archive();

        // Assert
        project.DomainEvents.Should().ContainSingle();
        var domainEvent = project.DomainEvents.First();
        domainEvent.Should().BeOfType<ProjectArchivedEvent>();

        var archivedEvent = (ProjectArchivedEvent)domainEvent;
        archivedEvent.ProjectId.Should().Be(project.Id);
    }

    [Fact]
    public void Archive_AlreadyArchivedProject_ShouldThrowDomainException()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.Archive();

        // Act
        Action act = () => project.Archive();

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project is already archived");
    }

    #endregion

    #region Activate Tests

    [Fact]
    public void Activate_ArchivedProject_ShouldActivateProject()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.Archive();

        // Act
        project.Activate();

        // Assert
        project.Status.Should().Be(ProjectStatus.Active);
        project.UpdatedAt.Should().NotBeNull();
        project.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void Activate_AlreadyActiveProject_ShouldThrowDomainException()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());

        // Act
        Action act = () => project.Activate();

        // Assert
        act.Should().Throw<DomainException>()
            .WithMessage("Project is already active");
    }

    [Fact]
    public void Activate_ArchivedProjectWithEpics_ShouldActivateSuccessfully()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());
        project.CreateEpic("Epic 1", "Description", UserId.Create());
        project.Archive();

        // Act
        project.Activate();

        // Assert
        project.Status.Should().Be(ProjectStatus.Active);
        project.Epics.Should().NotBeEmpty();
    }

    #endregion

    #region Aggregate Boundary Tests

    [Fact]
    public void Epics_Collection_ShouldBeReadOnly()
    {
        // Arrange
        var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TEST", UserId.Create());

        // Act & Assert
        project.Epics.Should().BeAssignableTo<IReadOnlyCollection<Epic>>();
    }

    [Fact]
    public void Project_ShouldHaveUniqueId()
    {
        // Arrange & Act
        var project1 = Project.Create(TenantId.Create(Guid.NewGuid()), "Project 1", "Description", "PRJ1", UserId.Create());
        var project2 = Project.Create(TenantId.Create(Guid.NewGuid()), "Project 2", "Description", "PRJ2", UserId.Create());

        // Assert
        project1.Id.Should().NotBe(project2.Id);
    }

    #endregion
}
