using ColaFlow.Modules.Mcp.Domain.Entities;
using ColaFlow.Modules.Mcp.Domain.Events;
using ColaFlow.Modules.Mcp.Domain.ValueObjects;
using Xunit;

namespace ColaFlow.Modules.Mcp.Tests.Domain;

public class PendingChangeTests
{
    private static DiffPreview CreateValidDiff()
    {
        return DiffPreview.ForCreate(
            entityType: "Epic",
            afterData: "{\"title\": \"New Epic\"}"
        );
    }

    [Fact]
    public void Create_ValidInput_Success()
    {
        // Arrange
        var diff = CreateValidDiff();
        var tenantId = Guid.NewGuid();
        var apiKeyId = Guid.NewGuid();

        // Act
        var pendingChange = PendingChange.Create(
            toolName: "create_epic",
            diff: diff,
            tenantId: tenantId,
            apiKeyId: apiKeyId
        );

        // Assert
        Assert.NotEqual(Guid.Empty, pendingChange.Id);
        Assert.Equal("create_epic", pendingChange.ToolName);
        Assert.Equal(diff, pendingChange.Diff);
        Assert.Equal(tenantId, pendingChange.TenantId);
        Assert.Equal(apiKeyId, pendingChange.ApiKeyId);
        Assert.Equal(PendingChangeStatus.PendingApproval, pendingChange.Status);
        Assert.True(pendingChange.ExpiresAt > DateTime.UtcNow);
        Assert.Null(pendingChange.ApprovedBy);
        Assert.Null(pendingChange.ApprovedAt);
        Assert.Null(pendingChange.RejectedBy);
        Assert.Null(pendingChange.RejectedAt);
    }

    [Fact]
    public void Create_RaisesPendingChangeCreatedEvent()
    {
        // Arrange
        var diff = CreateValidDiff();
        var tenantId = Guid.NewGuid();
        var apiKeyId = Guid.NewGuid();

        // Act
        var pendingChange = PendingChange.Create("create_epic", diff, tenantId, apiKeyId);

        // Assert
        Assert.Single(pendingChange.DomainEvents);
        var domainEvent = pendingChange.DomainEvents.First();
        Assert.IsType<PendingChangeCreatedEvent>(domainEvent);
        var createdEvent = (PendingChangeCreatedEvent)domainEvent;
        Assert.Equal(pendingChange.Id, createdEvent.PendingChangeId);
        Assert.Equal("create_epic", createdEvent.ToolName);
        Assert.Equal("Epic", createdEvent.EntityType);
        Assert.Equal("CREATE", createdEvent.Operation);
    }

    [Fact]
    public void Create_EmptyToolName_ThrowsException()
    {
        // Arrange
        var diff = CreateValidDiff();

        // Act & Assert
        Assert.Throws<ArgumentException>(() => PendingChange.Create(
            toolName: "",
            diff: diff,
            tenantId: Guid.NewGuid(),
            apiKeyId: Guid.NewGuid()
        ));
    }

    [Fact]
    public void Create_NullDiff_ThrowsException()
    {
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => PendingChange.Create(
            toolName: "create_epic",
            diff: null!,
            tenantId: Guid.NewGuid(),
            apiKeyId: Guid.NewGuid()
        ));
    }

    [Fact]
    public void Create_EmptyTenantId_ThrowsException()
    {
        // Arrange
        var diff = CreateValidDiff();

        // Act & Assert
        Assert.Throws<ArgumentException>(() => PendingChange.Create(
            toolName: "create_epic",
            diff: diff,
            tenantId: Guid.Empty,
            apiKeyId: Guid.NewGuid()
        ));
    }

    [Fact]
    public void Create_CustomExpirationHours_SetsCorrectExpiration()
    {
        // Arrange
        var diff = CreateValidDiff();
        var before = DateTime.UtcNow;

        // Act
        var pendingChange = PendingChange.Create(
            toolName: "create_epic",
            diff: diff,
            tenantId: Guid.NewGuid(),
            apiKeyId: Guid.NewGuid(),
            expirationHours: 48
        );

        // Assert
        var after = DateTime.UtcNow;
        Assert.True(pendingChange.ExpiresAt >= before.AddHours(48));
        Assert.True(pendingChange.ExpiresAt <= after.AddHours(48));
    }

    [Fact]
    public void Approve_PendingChange_Success()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        var approverId = Guid.NewGuid();
        pendingChange.ClearDomainEvents(); // Clear creation event

        // Act
        pendingChange.Approve(approverId);

        // Assert
        Assert.Equal(PendingChangeStatus.Approved, pendingChange.Status);
        Assert.Equal(approverId, pendingChange.ApprovedBy);
        Assert.NotNull(pendingChange.ApprovedAt);
        Assert.True(pendingChange.ApprovedAt <= DateTime.UtcNow);
    }

    [Fact]
    public void Approve_RaisesPendingChangeApprovedEvent()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        var approverId = Guid.NewGuid();
        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Approve(approverId);

        // Assert
        Assert.Single(pendingChange.DomainEvents);
        var domainEvent = pendingChange.DomainEvents.First();
        Assert.IsType<PendingChangeApprovedEvent>(domainEvent);
        var approvedEvent = (PendingChangeApprovedEvent)domainEvent;
        Assert.Equal(pendingChange.Id, approvedEvent.PendingChangeId);
        Assert.Equal(approverId, approvedEvent.ApprovedBy);
    }

    [Fact]
    public void Approve_AlreadyApproved_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Approve(Guid.NewGuid());

        // Act & Assert
        Assert.Throws<InvalidOperationException>(
            () => pendingChange.Approve(Guid.NewGuid()));
    }

    [Fact]
    public void Approve_Rejected_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Reject(Guid.NewGuid(), "Not valid");

        // Act & Assert
        Assert.Throws<InvalidOperationException>(
            () => pendingChange.Approve(Guid.NewGuid()));
    }

    [Fact]
    public void Reject_PendingChange_Success()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        var rejectorId = Guid.NewGuid();
        var reason = "Invalid data";
        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Reject(rejectorId, reason);

        // Assert
        Assert.Equal(PendingChangeStatus.Rejected, pendingChange.Status);
        Assert.Equal(rejectorId, pendingChange.RejectedBy);
        Assert.Equal(reason, pendingChange.RejectionReason);
        Assert.NotNull(pendingChange.RejectedAt);
        Assert.True(pendingChange.RejectedAt <= DateTime.UtcNow);
    }

    [Fact]
    public void Reject_RaisesPendingChangeRejectedEvent()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        var rejectorId = Guid.NewGuid();
        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Reject(rejectorId, "Invalid");

        // Assert
        Assert.Single(pendingChange.DomainEvents);
        var domainEvent = pendingChange.DomainEvents.First();
        Assert.IsType<PendingChangeRejectedEvent>(domainEvent);
        var rejectedEvent = (PendingChangeRejectedEvent)domainEvent;
        Assert.Equal(pendingChange.Id, rejectedEvent.PendingChangeId);
        Assert.Equal(rejectorId, rejectedEvent.RejectedBy);
        Assert.Equal("Invalid", rejectedEvent.Reason);
    }

    [Fact]
    public void Reject_EmptyReason_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );

        // Act & Assert
        Assert.Throws<ArgumentException>(
            () => pendingChange.Reject(Guid.NewGuid(), ""));
    }

    [Fact]
    public void Reject_AlreadyApproved_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Approve(Guid.NewGuid());

        // Act & Assert
        Assert.Throws<InvalidOperationException>(
            () => pendingChange.Reject(Guid.NewGuid(), "Too late"));
    }

    [Fact]
    public void Expire_PendingChange_Success()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 1
        );

        // Use reflection to set ExpiresAt to the past (simulating expiration)
        var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt");
        expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1));

        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Expire();

        // Assert
        Assert.Equal(PendingChangeStatus.Expired, pendingChange.Status);
    }

    [Fact]
    public void Expire_RaisesPendingChangeExpiredEvent()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 1
        );

        // Use reflection to set ExpiresAt to the past
        var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt");
        expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1));

        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Expire();

        // Assert
        Assert.Single(pendingChange.DomainEvents);
        Assert.IsType<PendingChangeExpiredEvent>(pendingChange.DomainEvents.First());
    }

    [Fact]
    public void Expire_NotYetExpired_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 24
        );

        // Act & Assert
        Assert.Throws<InvalidOperationException>(() => pendingChange.Expire());
    }

    [Fact]
    public void Expire_AlreadyApproved_DoesNothing()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 1
        );
        pendingChange.Approve(Guid.NewGuid());
        var statusBefore = pendingChange.Status;

        // Use reflection to set ExpiresAt to the past
        var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt");
        expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1));

        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.Expire();

        // Assert
        Assert.Equal(statusBefore, pendingChange.Status); // Still Approved
        Assert.Empty(pendingChange.DomainEvents); // No event raised
    }

    [Fact]
    public void MarkAsApplied_ApprovedChange_Success()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Approve(Guid.NewGuid());
        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.MarkAsApplied("Successfully created Epic COLA-1");

        // Assert
        Assert.Equal(PendingChangeStatus.Applied, pendingChange.Status);
        Assert.NotNull(pendingChange.AppliedAt);
        Assert.Equal("Successfully created Epic COLA-1", pendingChange.ApplicationResult);
    }

    [Fact]
    public void MarkAsApplied_RaisesPendingChangeAppliedEvent()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Approve(Guid.NewGuid());
        pendingChange.ClearDomainEvents();

        // Act
        pendingChange.MarkAsApplied("Success");

        // Assert
        Assert.Single(pendingChange.DomainEvents);
        Assert.IsType<PendingChangeAppliedEvent>(pendingChange.DomainEvents.First());
    }

    [Fact]
    public void MarkAsApplied_NotApproved_ThrowsException()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );

        // Act & Assert
        Assert.Throws<InvalidOperationException>(
            () => pendingChange.MarkAsApplied("Success"));
    }

    [Fact]
    public void IsExpired_ExpiredChange_ReturnsTrue()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 1
        );

        // Use reflection to set ExpiresAt to the past
        var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt");
        expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1));

        // Act & Assert
        Assert.True(pendingChange.IsExpired());
    }

    [Fact]
    public void IsExpired_NotExpiredChange_ReturnsFalse()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 24
        );

        // Act & Assert
        Assert.False(pendingChange.IsExpired());
    }

    [Fact]
    public void CanBeApproved_PendingAndNotExpired_ReturnsTrue()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 24
        );

        // Act & Assert
        Assert.True(pendingChange.CanBeApproved());
    }

    [Fact]
    public void CanBeApproved_Expired_ReturnsFalse()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid(),
            expirationHours: 1
        );

        // Use reflection to set ExpiresAt to the past
        var expiresAtProperty = typeof(PendingChange).GetProperty("ExpiresAt");
        expiresAtProperty!.SetValue(pendingChange, DateTime.UtcNow.AddHours(-1));

        // Act & Assert
        Assert.False(pendingChange.CanBeApproved());
    }

    [Fact]
    public void CanBeApproved_AlreadyApproved_ReturnsFalse()
    {
        // Arrange
        var pendingChange = PendingChange.Create(
            "create_epic",
            CreateValidDiff(),
            Guid.NewGuid(),
            Guid.NewGuid()
        );
        pendingChange.Approve(Guid.NewGuid());

        // Act & Assert
        Assert.False(pendingChange.CanBeApproved());
    }

    [Fact]
    public void GetSummary_ReturnsFormattedString()
    {
        // Arrange
        var diff = DiffPreview.ForCreate("Epic", "{}", "COLA-1");
        var pendingChange = PendingChange.Create(
            "create_epic",
            diff,
            Guid.NewGuid(),
            Guid.NewGuid()
        );

        // Act
        var summary = pendingChange.GetSummary();

        // Assert
        Assert.Contains("create_epic", summary);
        Assert.Contains("CREATE Epic (COLA-1)", summary);
        Assert.Contains("PendingApproval", summary);
    }
}
