using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Features.WorkflowInstances.Commands;
using Workflow.Application.Features.WorkflowInstances.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 WorkflowInstanceHandlerTests
{
    private static WorkflowDbContext CreateDbContext()
    {
        var options = new DbContextOptionsBuilder<WorkflowDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;
        return new WorkflowDbContext(options);
    }

    private static async Task<WorkflowDbContext> SeedPublishedDefinitionAsync(
        string code = "leave-approval",
        string name = "Leave Approval",
        Guid? startNodeId = null)
    {
        var db = CreateDbContext();
        var definitionId = Guid.NewGuid();
        var nodeId = startNodeId ?? Guid.NewGuid();

        var definition = new WorkflowDefinition
        {
            Id = definitionId,
            Name = name,
            Code = code,
            Status = DefinitionStatus.Published,
            Version = 2,
            IsEnabled = true
        };
        db.WorkflowDefinitions.Add(definition);

        var startNode = new WorkflowNode
        {
            Id = nodeId,
            DefinitionId = definitionId,
            NodeType = NodeType.Start,
            Name = "Start",
            PositionX = 100,
            PositionY = 100
        };
        db.WorkflowNodes.Add(startNode);

        await db.SaveChangesAsync();
        return db;
    }

    #region StartWorkflowInstance

    [Fact]
    public async Task StartInstance_CreatesInstanceWithRunningStatus()
    {
        // Arrange
        await using var db = await SeedPublishedDefinitionAsync("running-test");
        var handler = new StartWorkflowInstanceCommandHandler(db);
        var command = new StartWorkflowInstanceCommand(
            DefinitionCode: "running-test",
            Title: "Test Instance",
            Variables: null
        );

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

        // Assert
        instanceId.Should().NotBe(Guid.Empty);

        var instance = await db.WorkflowInstances.FindAsync(instanceId);
        instance.Should().NotBeNull();
        instance!.Status.Should().Be(InstanceStatus.Running);
        instance.Title.Should().Be("Test Instance");
    }

    [Fact]
    public async Task StartInstance_CreatesInitialTokenAtStartNode()
    {
        // Arrange
        var startNodeId = Guid.NewGuid();
        await using var db = await SeedPublishedDefinitionAsync("token-test", startNodeId: startNodeId);
        var handler = new StartWorkflowInstanceCommandHandler(db);
        var command = new StartWorkflowInstanceCommand(
            DefinitionCode: "token-test",
            Title: "Token Test",
            Variables: null
        );

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

        // Assert
        var tokens = await db.WorkflowTokens
            .Where(t => t.InstanceId == instanceId)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(startNodeId);
        tokens[0].Status.Should().Be(TokenStatus.Active);
    }

    [Fact]
    public async Task StartInstance_WithInvalidDefinitionCode_ThrowsNotFoundException()
    {
        // Arrange
        await using var db = CreateDbContext();
        var handler = new StartWorkflowInstanceCommandHandler(db);
        var command = new StartWorkflowInstanceCommand(
            DefinitionCode: "nonexistent-code",
            Title: "Should Fail",
            Variables: null
        );

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

        // Assert
        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage("*nonexistent-code*");
    }

    [Fact]
    public async Task StartInstance_WithDisabledDefinition_ThrowsBusinessException()
    {
        // Arrange
        await using var db = CreateDbContext();
        var definition = new WorkflowDefinition
        {
            Id = Guid.NewGuid(),
            Name = "Disabled Workflow",
            Code = "disabled-wf",
            Status = DefinitionStatus.Published,
            Version = 2,
            IsEnabled = false
        };
        db.WorkflowDefinitions.Add(definition);
        await db.SaveChangesAsync();

        var handler = new StartWorkflowInstanceCommandHandler(db);
        var command = new StartWorkflowInstanceCommand(
            DefinitionCode: "disabled-wf",
            Title: "Should Fail",
            Variables: null
        );

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

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

    #endregion

    #region WithdrawWorkflowInstance

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

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "Test",
            Status = InstanceStatus.Running,
            InitiatorId = initiatorId
        };
        db.WorkflowInstances.Add(instance);

        var token1 = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instanceId,
            NodeId = Guid.NewGuid(),
            Status = TokenStatus.Active
        };
        var token2 = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instanceId,
            NodeId = Guid.NewGuid(),
            Status = TokenStatus.Active
        };
        db.WorkflowTokens.AddRange(token1, token2);

        // No tasks have been processed (task exists but is pending, not completed)
        var pendingTask = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instanceId,
            TokenId = token1.Id,
            AssigneeId = Guid.NewGuid(),
            Status = TaskStatus.Pending
        };
        db.WorkflowTasks.Add(pendingTask);
        await db.SaveChangesAsync();

        var handler = new WithdrawWorkflowInstanceCommandHandler(db);
        var command = new WithdrawWorkflowInstanceCommand(
            InstanceId: instanceId,
            UserId: initiatorId
        );

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

        // Assert
        var updatedInstance = await db.WorkflowInstances.FindAsync(instanceId);
        updatedInstance.Should().NotBeNull();
        updatedInstance!.Status.Should().Be(InstanceStatus.Terminated);

        var tokens = await db.WorkflowTokens
            .Where(t => t.InstanceId == instanceId)
            .ToListAsync();
        tokens.Should().OnlyContain(t => t.Status == TokenStatus.Terminated);
    }

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

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "Test",
            Status = InstanceStatus.Running,
            InitiatorId = initiatorId
        };
        db.WorkflowInstances.Add(instance);
        await db.SaveChangesAsync();

        var handler = new WithdrawWorkflowInstanceCommandHandler(db);
        var command = new WithdrawWorkflowInstanceCommand(
            InstanceId: instanceId,
            UserId: otherUserId
        );

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

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

    [Fact]
    public async Task WithdrawInstance_Throws_WhenTaskAlreadyProcessed()
    {
        // Arrange
        await using var db = CreateDbContext();
        var instanceId = Guid.NewGuid();
        var initiatorId = Guid.NewGuid();
        var tokenId = Guid.NewGuid();

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "Test",
            Status = InstanceStatus.Running,
            InitiatorId = initiatorId
        };
        db.WorkflowInstances.Add(instance);

        var token = new WorkflowToken
        {
            Id = tokenId,
            InstanceId = instanceId,
            NodeId = Guid.NewGuid(),
            Status = TokenStatus.Active
        };
        db.WorkflowTokens.Add(token);

        // A task that has already been completed (processed)
        var completedTask = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instanceId,
            TokenId = tokenId,
            AssigneeId = Guid.NewGuid(),
            Status = TaskStatus.Approved
        };
        db.WorkflowTasks.Add(completedTask);
        await db.SaveChangesAsync();

        var handler = new WithdrawWorkflowInstanceCommandHandler(db);
        var command = new WithdrawWorkflowInstanceCommand(
            InstanceId: instanceId,
            UserId: initiatorId
        );

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

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

    #endregion

    #region SuspendWorkflowInstance

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

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "To Suspend",
            Status = InstanceStatus.Running,
            InitiatorId = Guid.NewGuid()
        };
        db.WorkflowInstances.Add(instance);
        await db.SaveChangesAsync();

        var handler = new SuspendWorkflowInstanceCommandHandler(db);
        var command = new SuspendWorkflowInstanceCommand(InstanceId: instanceId);

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

        // Assert
        var updated = await db.WorkflowInstances.FindAsync(instanceId);
        updated.Should().NotBeNull();
        updated!.Status.Should().Be(InstanceStatus.Suspended);
    }

    #endregion

    #region ResumeWorkflowInstance

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

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "To Resume",
            Status = InstanceStatus.Suspended,
            InitiatorId = Guid.NewGuid()
        };
        db.WorkflowInstances.Add(instance);
        await db.SaveChangesAsync();

        var handler = new ResumeWorkflowInstanceCommandHandler(db);
        var command = new ResumeWorkflowInstanceCommand(InstanceId: instanceId);

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

        // Assert
        var updated = await db.WorkflowInstances.FindAsync(instanceId);
        updated.Should().NotBeNull();
        updated!.Status.Should().Be(InstanceStatus.Running);
    }

    #endregion

    #region GetWorkflowInstanceList

    [Fact]
    public async Task GetInstanceList_ReturnsPaged()
    {
        // Arrange
        await using var db = CreateDbContext();
        var definitionId = Guid.NewGuid();
        for (int i = 1; i <= 12; i++)
        {
            db.WorkflowInstances.Add(new WorkflowInstance
            {
                Id = Guid.NewGuid(),
                DefinitionId = definitionId,
                Title = $"Instance {i}",
                Status = InstanceStatus.Running,
                InitiatorId = Guid.NewGuid()
            });
        }
        await db.SaveChangesAsync();

        var handler = new GetWorkflowInstanceListQueryHandler(db);
        var query = new GetWorkflowInstanceListQuery(
            PageIndex: 1,
            PageSize: 5,
            Status: null,
            InitiatorId: null
        );

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

        // Assert
        result.Should().NotBeNull();
        result.Items.Should().HaveCount(5);
        result.Total.Should().Be(12);
        result.PageIndex.Should().Be(1);
        result.PageSize.Should().Be(5);
    }

    #endregion

    #region GetWorkflowInstanceById

    [Fact]
    public async Task GetInstanceById_IncludesTokensAndTasks()
    {
        // Arrange
        await using var db = CreateDbContext();
        var instanceId = Guid.NewGuid();
        var tokenId = Guid.NewGuid();
        var nodeId = Guid.NewGuid();

        var instance = new WorkflowInstance
        {
            Id = instanceId,
            DefinitionId = Guid.NewGuid(),
            Title = "Detail Test",
            Status = InstanceStatus.Running,
            InitiatorId = Guid.NewGuid()
        };
        db.WorkflowInstances.Add(instance);

        var token = new WorkflowToken
        {
            Id = tokenId,
            InstanceId = instanceId,
            NodeId = nodeId,
            Status = TokenStatus.Active
        };
        db.WorkflowTokens.Add(token);

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

        var handler = new GetWorkflowInstanceByIdQueryHandler(db);
        var query = new GetWorkflowInstanceByIdQuery(Id: instanceId);

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

        // Assert
        result.Should().NotBeNull();
        result.Id.Should().Be(instanceId);
        result.Tokens.Should().NotBeNull().And.HaveCount(1);
        result.Tasks.Should().NotBeNull().And.HaveCount(1);

        result.Tokens[0].NodeId.Should().Be(nodeId);
        result.Tasks[0].Status.Should().Be(TaskStatus.Pending);
    }

    #endregion
}
