using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Features.WorkflowTasks.Commands;
using Workflow.Application.Features.WorkflowTasks.Queries;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Xunit;

namespace Workflow.Tests.Handlers;

public class WorkflowTaskHandlerTests
{
    private static WorkflowDbContext CreateDbContext()
    {
        var options = new DbContextOptionsBuilder<WorkflowDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        return new WorkflowDbContext(options);
    }

    #region ApproveTask

    [Fact]
    public async Task ApproveTask_UpdatesTaskStatus()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var assigneeId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = assigneeId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new ApproveTaskCommandHandler(db);
        var command = new ApproveTaskCommand(
            TaskId: taskId,
            UserId: assigneeId,
            Comment: "Looks good"
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert
        var updated = await db.WorkflowTasks.FindAsync(taskId);
        updated.Should().NotBeNull();
        updated!.Status.Should().Be(TaskStatus.Approved);
    }

    [Fact]
    public async Task ApproveTask_SetsCompletedAt()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var assigneeId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = assigneeId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var beforeApprove = DateTime.UtcNow;

        var handler = new ApproveTaskCommandHandler(db);
        var command = new ApproveTaskCommand(
            TaskId: taskId,
            UserId: assigneeId,
            Comment: "Approved"
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert
        var updated = await db.WorkflowTasks.FindAsync(taskId);
        updated.Should().NotBeNull();
        updated!.CompletedAt.Should().NotBeNull();
        updated.CompletedAt.Should().BeOnOrAfter(beforeApprove);
    }

    [Fact]
    public async Task ApproveTask_ByNonAssignee_ThrowsUnauthorizedException()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var assigneeId = Guid.NewGuid();
        var otherUserId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = assigneeId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new ApproveTaskCommandHandler(db);
        var command = new ApproveTaskCommand(
            TaskId: taskId,
            UserId: otherUserId,
            Comment: "Impersonation attempt"
        );

        // Act
        var act = () => handler.Handle(command, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<UnauthorizedException>()
            .WithMessage("*assignee*");
    }

    #endregion

    #region RejectTask

    [Fact]
    public async Task RejectTask_UpdatesTaskStatusWithComment()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var assigneeId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = assigneeId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new RejectTaskCommandHandler(db);
        var command = new RejectTaskCommand(
            TaskId: taskId,
            UserId: assigneeId,
            Comment: "Missing required documents"
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert
        var updated = await db.WorkflowTasks.FindAsync(taskId);
        updated.Should().NotBeNull();
        updated!.Status.Should().Be(TaskStatus.Rejected);
        updated.Comment.Should().Be("Missing required documents");
        updated.CompletedAt.Should().NotBeNull();
    }

    #endregion

    #region TransferTask

    [Fact]
    public async Task TransferTask_CreatesNewTaskForTargetUser()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var fromUserId = Guid.NewGuid();
        var toUserId = Guid.NewGuid();
        var instanceId = Guid.NewGuid();
        var tokenId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = instanceId,
            TokenId = tokenId,
            AssigneeId = fromUserId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new TransferTaskCommandHandler(db);
        var command = new TransferTaskCommand(
            TaskId: taskId,
            FromUserId: fromUserId,
            ToUserId: toUserId
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert - a new task for the target user should exist
        var newTask = await db.WorkflowTasks
            .FirstOrDefaultAsync(t => t.AssigneeId == toUserId && t.Id != taskId);
        newTask.Should().NotBeNull();
        newTask!.Status.Should().Be(TaskStatus.Pending);
        newTask.InstanceId.Should().Be(instanceId);
    }

    [Fact]
    public async Task TransferTask_ClosesOriginalTask()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var fromUserId = Guid.NewGuid();
        var toUserId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = fromUserId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new TransferTaskCommandHandler(db);
        var command = new TransferTaskCommand(
            TaskId: taskId,
            FromUserId: fromUserId,
            ToUserId: toUserId
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert
        var original = await db.WorkflowTasks.FindAsync(taskId);
        original.Should().NotBeNull();
        original!.Status.Should().Be(TaskStatus.Transferred);
        original.CompletedAt.Should().NotBeNull();
    }

    #endregion

    #region DelegateTask

    [Fact]
    public async Task DelegateTask_SetsTaskToDelegated()
    {
        // Arrange
        await using var db = CreateDbContext();
        var taskId = Guid.NewGuid();
        var fromUserId = Guid.NewGuid();
        var toUserId = Guid.NewGuid();
        var instanceId = Guid.NewGuid();
        var tokenId = Guid.NewGuid();

        var task = new WorkflowTask
        {
            Id = taskId,
            InstanceId = instanceId,
            TokenId = tokenId,
            AssigneeId = fromUserId,
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(task);
        await db.SaveChangesAsync();

        var handler = new DelegateTaskCommandHandler(db);
        var command = new DelegateTaskCommand(
            TaskId: taskId,
            FromUserId: fromUserId,
            ToUserId: toUserId
        );

        // Act
        await handler.Handle(command, CancellationToken.None);

        // Assert - original task should be marked as delegated
        var original = await db.WorkflowTasks.FindAsync(taskId);
        original.Should().NotBeNull();
        original!.Status.Should().Be(TaskStatus.Delegated);

        // A new delegated task should exist for the target user
        var delegatedTask = await db.WorkflowTasks
            .FirstOrDefaultAsync(t => t.AssigneeId == toUserId && t.DelegatedFromId == fromUserId);
        delegatedTask.Should().NotBeNull();
        delegatedTask!.Status.Should().Be(TaskStatus.Pending);
    }

    #endregion

    #region GetPendingTasks

    [Fact]
    public async Task GetPendingTasks_ReturnsOnlyUserTasks()
    {
        // Arrange
        await using var db = CreateDbContext();
        var userId = Guid.NewGuid();
        var otherUserId = Guid.NewGuid();

        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Pending
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Pending
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = otherUserId,
            Status = TaskStatus.Pending
        });
        await db.SaveChangesAsync();

        var handler = new GetPendingTasksQueryHandler(db);
        var query = new GetPendingTasksQuery(
            UserId: userId,
            PageIndex: 1,
            PageSize: 10
        );

        // Act
        var result = await handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Items.Should().HaveCount(2);
        result.Items.Should().OnlyContain(t => t.AssigneeId == userId);
    }

    [Fact]
    public async Task GetPendingTasks_ExcludesCompletedTasks()
    {
        // Arrange
        await using var db = CreateDbContext();
        var userId = Guid.NewGuid();

        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Pending
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Approved,
            CompletedAt = DateTime.UtcNow
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Rejected,
            CompletedAt = DateTime.UtcNow
        });
        await db.SaveChangesAsync();

        var handler = new GetPendingTasksQueryHandler(db);
        var query = new GetPendingTasksQuery(
            UserId: userId,
            PageIndex: 1,
            PageSize: 10
        );

        // Act
        var result = await handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Items.Should().HaveCount(1);
        result.Items[0].Status.Should().Be(TaskStatus.Pending);
    }

    #endregion

    #region GetHistoryTasks

    [Fact]
    public async Task GetHistoryTasks_ReturnsOnlyProcessedTasks()
    {
        // Arrange
        await using var db = CreateDbContext();
        var userId = Guid.NewGuid();

        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Approved,
            CompletedAt = DateTime.UtcNow
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Rejected,
            CompletedAt = DateTime.UtcNow
        });
        db.WorkflowTasks.Add(new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = Guid.NewGuid(),
            TokenId = Guid.NewGuid(),
            AssigneeId = userId,
            Status = TaskStatus.Pending
        });
        await db.SaveChangesAsync();

        var handler = new GetHistoryTasksQueryHandler(db);
        var query = new GetHistoryTasksQuery(
            UserId: userId,
            PageIndex: 1,
            PageSize: 10
        );

        // Act
        var result = await handler.Handle(query, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Items.Should().HaveCount(2);
        result.Items.Should().OnlyContain(t =>
            t.Status == TaskStatus.Approved || t.Status == TaskStatus.Rejected);
    }

    #endregion
}
