namespace Workflow.Tests.Engine;

using FluentAssertions;
using Moq;
using Workflow.Application.Engine;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

public class ProcessEngineTests
{
    private readonly Mock<IServiceProvider> _serviceProvider;
    private readonly ProcessEngine _engine;
    private readonly WorkflowDbContext _dbContext;

    public ProcessEngineTests()
    {
        var options = new DbContextOptionsBuilder<WorkflowDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        _dbContext = new WorkflowDbContext(options);
        _serviceProvider = new Mock<IServiceProvider>();
        _engine = new ProcessEngine(_dbContext, _serviceProvider.Object);
    }

    // ============================================================
    // Start Node
    // ============================================================

    [Fact]
    public async Task ProcessStartNode_CreatesSingleToken_AndPropagatesToNextNode()
    {
        var startNode = CreateNode(NodeType.Start, "start-1");
        var nextNode = CreateNode(NodeType.Approval, "approval-1");
        var edge = CreateEdge(startNode, nextNode);

        var definition = CreateDefinition(startNode, nextNode, edge);
        var instance = CreateInstance(definition);

        await _engine.StartAsync(instance);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(nextNode.Id);
        tokens[0].Status.Should().Be(TokenStatus.Active);
        instance.Status.Should().Be(InstanceStatus.Running);
    }

    [Fact]
    public async Task ProcessStartNode_ThrowsWhenNoOutgoingEdge()
    {
        var startNode = CreateNode(NodeType.Start, "start-1");
        var definition = CreateDefinition(startNode);
        var instance = CreateInstance(definition);

        var act = () => _engine.StartAsync(instance);

        await act.Should().ThrowAsync<BusinessException>()
            .WithMessage("*start*edge*");
    }

    // ============================================================
    // Approval Node
    // ============================================================

    [Fact]
    public async Task ProcessApprovalNode_CreatesTaskForAssignee()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        approvalNode.Config = """{ "assigneeRule": "user:user-001", "formId": "form-001" }""";

        var definition = CreateDefinition(approvalNode);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        await _engine.ProcessNodeAsync(instance, token, approvalNode);

        var tasks = await _dbContext.WorkflowTasks
            .Where(t => t.InstanceId == instance.Id)
            .ToListAsync();

        tasks.Should().HaveCount(1);
        tasks[0].AssigneeId.Should().Be("user-001");
        tasks[0].Type.Should().Be(TaskType.Approval);
        tasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
    }

    [Fact]
    public async Task ProcessApprovalNode_CreatesTaskForRole()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-2");
        approvalNode.Config = """{ "assigneeRule": "role:manager", "formId": "form-001" }""";

        var definition = CreateDefinition(approvalNode);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        await _engine.ProcessNodeAsync(instance, token, approvalNode);

        var tasks = await _dbContext.WorkflowTasks
            .Where(t => t.InstanceId == instance.Id)
            .ToListAsync();

        tasks.Should().HaveCount(1);
        tasks[0].AssigneeRole.Should().Be("manager");
        tasks[0].Type.Should().Be(TaskType.Approval);
        tasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
    }

    [Fact]
    public async Task CompleteTask_Approved_PropagatesTokenAlongApprovedEdge()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);

        approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";

        var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        await _engine.CompleteTaskAsync(task, TaskResult.Approved);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(nextNode.Id);
    }

    [Fact]
    public async Task CompleteTask_Rejected_PropagatesTokenAlongRejectedEdge()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var rejectNode = CreateNode(NodeType.End, "reject-end-1");
        var rejectedEdge = CreateEdge(approvalNode, rejectNode, EdgeType.Rejected);

        approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";

        var definition = CreateDefinition(approvalNode, rejectNode, rejectedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        await _engine.CompleteTaskAsync(task, TaskResult.Rejected);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(rejectNode.Id);
    }

    [Fact]
    public async Task CompleteTask_Rejected_WithNoRejectEdge_ThrowsBusinessException()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);

        approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";

        var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        var act = () => _engine.CompleteTaskAsync(task, TaskResult.Rejected);

        await act.Should().ThrowAsync<BusinessException>()
            .WithMessage("*reject*edge*");
    }

    // ============================================================
    // Cc Node
    // ============================================================

    [Fact]
    public async Task ProcessCcNode_CreatesCcTasks_ForAllRecipients()
    {
        var ccNode = CreateNode(NodeType.Cc, "cc-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var edge = CreateEdge(ccNode, nextNode);

        ccNode.Config = """{ "recipients": ["user-001", "user-002", "user-003"] }""";

        var definition = CreateDefinition(ccNode, nextNode, edge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, ccNode);

        await _engine.ProcessNodeAsync(instance, token, ccNode);

        var tasks = await _dbContext.WorkflowTasks
            .Where(t => t.InstanceId == instance.Id)
            .ToListAsync();

        tasks.Should().HaveCount(3);
        tasks.Should().OnlyContain(t => t.Type == TaskType.Cc);
        tasks.Should().OnlyContain(t => t.Status == Enums.TaskStatus.Pending);
        tasks.Select(t => t.AssigneeId).Should().BeEquivalentTo("user-001", "user-002", "user-003");
    }

    [Fact]
    public async Task ProcessCcNode_PropagatesImmediatelyWithoutWaiting()
    {
        var ccNode = CreateNode(NodeType.Cc, "cc-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var edge = CreateEdge(ccNode, nextNode);

        ccNode.Config = """{ "recipients": ["user-001"] }""";

        var definition = CreateDefinition(ccNode, nextNode, edge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, ccNode);

        await _engine.ProcessNodeAsync(instance, token, ccNode);

        var propagatedTokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id
                     && t.NodeId == nextNode.Id
                     && t.Status == TokenStatus.Active)
            .ToListAsync();

        propagatedTokens.Should().HaveCount(1);

        var ccTasks = await _dbContext.WorkflowTasks
            .Where(t => t.InstanceId == instance.Id && t.Type == TaskType.Cc)
            .ToListAsync();

        ccTasks.Should().HaveCount(1);
        ccTasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
    }

    // ============================================================
    // Condition Gateway (Exclusive Gateway)
    // ============================================================

    [Fact]
    public async Task ProcessConditionNode_TakesMatchingBranch()
    {
        var conditionNode = CreateNode(NodeType.Condition, "gw-1");
        var branchA = CreateNode(NodeType.End, "branch-a");
        var branchB = CreateNode(NodeType.End, "branch-b");

        var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 1000");
        var edgeB = CreateEdge(conditionNode, branchB, condition: "amount <= 1000");

        var definition = CreateDefinition(conditionNode, branchA, branchB, edgeA, edgeB);
        var instance = CreateInstance(definition);
        instance.Variables = """{ "amount": 1500 }""";
        var token = CreateToken(instance, conditionNode);

        await _engine.ProcessNodeAsync(instance, token, conditionNode);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(branchA.Id);
    }

    [Fact]
    public async Task ProcessConditionNode_NoMatchingBranch_ThrowsBusinessException()
    {
        var conditionNode = CreateNode(NodeType.Condition, "gw-1");
        var branchA = CreateNode(NodeType.End, "branch-a");

        var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 5000");

        var definition = CreateDefinition(conditionNode, branchA, edgeA);
        var instance = CreateInstance(definition);
        instance.Variables = """{ "amount": 100 }""";
        var token = CreateToken(instance, conditionNode);

        var act = () => _engine.ProcessNodeAsync(instance, token, conditionNode);

        await act.Should().ThrowAsync<BusinessException>()
            .WithMessage("*condition*match*");
    }

    [Fact]
    public async Task ProcessConditionNode_FirstMatchingBranchWins_WhenMultipleMatch()
    {
        var conditionNode = CreateNode(NodeType.Condition, "gw-1");
        var branchA = CreateNode(NodeType.End, "branch-a");
        var branchB = CreateNode(NodeType.End, "branch-b");
        var branchC = CreateNode(NodeType.End, "branch-c");

        var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 1000", order: 1);
        var edgeB = CreateEdge(conditionNode, branchB, condition: "amount > 500", order: 2);
        var edgeC = CreateEdge(conditionNode, branchC, condition: "amount > 2000", order: 3);

        var definition = CreateDefinition(conditionNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
        var instance = CreateInstance(definition);
        instance.Variables = """{ "amount": 1500 }""";
        var token = CreateToken(instance, conditionNode);

        await _engine.ProcessNodeAsync(instance, token, conditionNode);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(branchA.Id);
    }

    // ============================================================
    // Parallel Gateway (Fork)
    // ============================================================

    [Fact]
    public async Task ProcessParallelFork_CreatesOneTokenPerOutgoingEdge()
    {
        var forkNode = CreateNode(NodeType.Parallel, "fork-1");
        var branchA = CreateNode(NodeType.Approval, "branch-a");
        var branchB = CreateNode(NodeType.Approval, "branch-b");

        var edgeA = CreateEdge(forkNode, branchA);
        var edgeB = CreateEdge(forkNode, branchB);

        var definition = CreateDefinition(forkNode, branchA, branchB, edgeA, edgeB);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, forkNode);

        await _engine.ProcessNodeAsync(instance, token, forkNode);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(2);
        tokens.Select(t => t.NodeId).Should().BeEquivalentTo(branchA.Id, branchB.Id);
    }

    [Fact]
    public async Task ProcessParallelFork_With3Edges_Creates3Tokens()
    {
        var forkNode = CreateNode(NodeType.Parallel, "fork-1");
        var branchA = CreateNode(NodeType.Approval, "branch-a");
        var branchB = CreateNode(NodeType.Approval, "branch-b");
        var branchC = CreateNode(NodeType.Approval, "branch-c");

        var edgeA = CreateEdge(forkNode, branchA);
        var edgeB = CreateEdge(forkNode, branchB);
        var edgeC = CreateEdge(forkNode, branchC);

        var definition = CreateDefinition(forkNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, forkNode);

        await _engine.ProcessNodeAsync(instance, token, forkNode);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(3);
        tokens.Select(t => t.NodeId)
            .Should().BeEquivalentTo(branchA.Id, branchB.Id, branchC.Id);
    }

    // ============================================================
    // Parallel Gateway (Join)
    // ============================================================

    [Fact]
    public async Task ProcessParallelJoin_WaitsForAllTokens_WhenNotAllArrived()
    {
        var joinNode = CreateNode(NodeType.Parallel, "join-1");
        var branchA = CreateNode(NodeType.Approval, "branch-a");
        var branchB = CreateNode(NodeType.Approval, "branch-b");
        var branchC = CreateNode(NodeType.Approval, "branch-c");

        var edgeA = CreateEdge(branchA, joinNode);
        var edgeB = CreateEdge(branchB, joinNode);
        var edgeC = CreateEdge(branchC, joinNode);

        var definition = CreateDefinition(joinNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
        var instance = CreateInstance(definition);

        var token1 = CreateToken(instance, joinNode, TokenStatus.Active);
        var token2 = CreateToken(instance, joinNode, TokenStatus.Active);

        await _engine.ProcessNodeAsync(instance, token2, joinNode);

        var activeTokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        activeTokens.Should().HaveCount(2);
        activeTokens.Should().OnlyContain(t => t.NodeId == joinNode.Id);
    }

    [Fact]
    public async Task ProcessParallelJoin_MergesAllTokens_WhenAllArrived()
    {
        var joinNode = CreateNode(NodeType.Parallel, "join-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var branchA = CreateNode(NodeType.Approval, "branch-a");
        var branchB = CreateNode(NodeType.Approval, "branch-b");

        var edgeA = CreateEdge(branchA, joinNode);
        var edgeB = CreateEdge(branchB, joinNode);
        var edgeOut = CreateEdge(joinNode, nextNode);

        var definition = CreateDefinition(
            joinNode, nextNode, branchA, branchB,
            edgeA, edgeB, edgeOut);
        var instance = CreateInstance(definition);

        var token1 = CreateToken(instance, joinNode, TokenStatus.Active);
        var token2 = CreateToken(instance, joinNode, TokenStatus.Active);

        await _engine.ProcessNodeAsync(instance, token2, joinNode);

        var consumedTokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Consumed)
            .ToListAsync();

        consumedTokens.Should().HaveCount(2);

        var activeTokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        activeTokens.Should().HaveCount(1);
        activeTokens[0].NodeId.Should().Be(nextNode.Id);
    }

    // ============================================================
    // End Node
    // ============================================================

    [Fact]
    public async Task ProcessEndNode_ConsumesToken()
    {
        var endNode = CreateNode(NodeType.End, "end-1");
        var definition = CreateDefinition(endNode);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, endNode);

        await _engine.ProcessNodeAsync(instance, token, endNode);

        token.Status.Should().Be(TokenStatus.Consumed);
    }

    [Fact]
    public async Task ProcessEndNode_CompletesInstance_WhenAllTokensConsumed()
    {
        var endNode = CreateNode(NodeType.End, "end-1");
        var definition = CreateDefinition(endNode);
        var instance = CreateInstance(definition, InstanceStatus.Running);
        var token = CreateToken(instance, endNode);

        await _engine.ProcessNodeAsync(instance, token, endNode);

        instance.Status.Should().Be(InstanceStatus.Completed);
    }

    [Fact]
    public async Task ProcessEndNode_DoesNotCompleteInstance_WhenOtherTokensActive()
    {
        var endNode = CreateNode(NodeType.End, "end-1");
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");

        var definition = CreateDefinition(endNode, approvalNode);
        var instance = CreateInstance(definition, InstanceStatus.Running);

        var endToken = CreateToken(instance, endNode, TokenStatus.Active);
        var activeToken = CreateToken(instance, approvalNode, TokenStatus.Active);

        await _engine.ProcessNodeAsync(instance, endToken, endNode);

        endToken.Status.Should().Be(TokenStatus.Consumed);
        activeToken.Status.Should().Be(TokenStatus.Active);
        instance.Status.Should().Be(InstanceStatus.Running);
    }

    // ============================================================
    // SubProcess
    // ============================================================

    [Fact]
    public async Task ProcessSubProcessNode_CreatesChildInstance()
    {
        var subDefId = Guid.NewGuid();
        var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1");
        subProcessNode.Config = $"{{ "definitionId": "{subDefId}" }}";

        var definition = CreateDefinition(subProcessNode);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, subProcessNode);

        await _engine.ProcessNodeAsync(instance, token, subProcessNode);

        var childInstance = await _dbContext.WorkflowInstances
            .FirstOrDefaultAsync(i => i.ParentInstanceId == instance.Id);

        childInstance.Should().NotBeNull();
        childInstance!.DefinitionId.Should().Be(subDefId);
        childInstance.Status.Should().Be(InstanceStatus.Running);
    }

    [Fact]
    public async Task SubProcessComplete_PropagatesTokenInParent()
    {
        var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var edge = CreateEdge(subProcessNode, nextNode);

        subProcessNode.Config = """{ "definitionId": "00000000-0000-0000-0000-000000000001" }""";

        var definition = CreateDefinition(subProcessNode, nextNode, edge);
        var parentInstance = CreateInstance(definition);

        var childInstance = new WorkflowInstance
        {
            Id = Guid.NewGuid(),
            DefinitionId = Guid.Parse("00000000-0000-0000-0000-000000000001"),
            ParentInstanceId = parentInstance.Id,
            ParentTokenId = Guid.Empty,
            Status = InstanceStatus.Running,
        };
        _dbContext.WorkflowInstances.Add(childInstance);
        await _dbContext.SaveChangesAsync();

        childInstance.Status = InstanceStatus.Completed;
        await _dbContext.SaveChangesAsync();

        await _engine.HandleSubProcessCompletionAsync(childInstance);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == parentInstance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(nextNode.Id);
    }

    // ============================================================
    // Node Actions (Hooks)
    // ============================================================

    [Fact]
    public async Task ProcessApprovalNode_ExecutesOnEnterAction()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        approvalNode.Config = """{ "assigneeRule": "user:user-001", "onEnter": "send-notification" }""";

        var definition = CreateDefinition(approvalNode);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var actionExecuted = false;
        _serviceProvider
            .Setup(sp => sp.GetService(It.IsAny<Type>()))
            .Returns(new TestAction(() => actionExecuted = true));

        await _engine.ProcessNodeAsync(instance, token, approvalNode);

        actionExecuted.Should().BeTrue("the onEnter action should be executed when entering an approval node");
    }

    [Fact]
    public async Task CompleteTask_Approved_ExecutesOnApprovedAction()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);

        approvalNode.Config = """{ "assigneeRule": "user:user-001", "onApproved": "log-approval" }""";

        var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        var actionExecuted = false;
        _serviceProvider
            .Setup(sp => sp.GetService(It.IsAny<Type>()))
            .Returns(new TestAction(() => actionExecuted = true));

        await _engine.CompleteTaskAsync(task, TaskResult.Approved);

        actionExecuted.Should().BeTrue("the onApproved action should be executed when task is approved");
    }

    [Fact]
    public async Task CompleteTask_Rejected_ExecutesOnRejectedAction()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var rejectNode = CreateNode(NodeType.End, "reject-end-1");
        var rejectedEdge = CreateEdge(approvalNode, rejectNode, EdgeType.Rejected);

        approvalNode.Config = """{ "assigneeRule": "user:user-001", "onRejected": "notify-rejection" }""";

        var definition = CreateDefinition(approvalNode, rejectNode, rejectedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        var actionExecuted = false;
        _serviceProvider
            .Setup(sp => sp.GetService(It.IsAny<Type>()))
            .Returns(new TestAction(() => actionExecuted = true));

        await _engine.CompleteTaskAsync(task, TaskResult.Rejected);

        actionExecuted.Should().BeTrue("the onRejected action should be executed when task is rejected");
    }

    [Fact]
    public async Task ActionFailure_DoesNotBlockTokenPropagation()
    {
        var approvalNode = CreateNode(NodeType.Approval, "approval-1");
        var nextNode = CreateNode(NodeType.End, "end-1");
        var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);

        approvalNode.Config = """{ "assigneeRule": "user:user-001", "onApproved": "failing-action" }""";

        var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
        var instance = CreateInstance(definition);
        var token = CreateToken(instance, approvalNode);

        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = approvalNode.Id.ToString(),
            AssigneeId = "user-001",
            Type = TaskType.Approval,
            Status = Enums.TaskStatus.Pending,
        };
        _dbContext.WorkflowTasks.Add(task);
        await _dbContext.SaveChangesAsync();

        _serviceProvider
            .Setup(sp => sp.GetService(It.IsAny<Type>()))
            .Returns(new ThrowingAction());

        await _engine.CompleteTaskAsync(task, TaskResult.Approved);

        var tokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
            .ToListAsync();

        tokens.Should().HaveCount(1);
        tokens[0].NodeId.Should().Be(nextNode.Id);
    }

    // ============================================================
    // Test Helpers
    // ============================================================

    private static WorkflowNode CreateNode(NodeType type, string nodeId)
    {
        return new WorkflowNode
        {
            Id = Guid.NewGuid(),
            NodeType = type,
            Config = "{}",
        };
    }

    private static WorkflowEdge CreateEdge(
        WorkflowNode source,
        WorkflowNode target,
        EdgeType? edgeType = null,
        string? condition = null,
        int order = 0)
    {
        return new WorkflowEdge
        {
            Id = Guid.NewGuid(),
            SourceNodeId = source.Id.ToString(),
            TargetNodeId = target.Id.ToString(),
            EdgeType = edgeType ?? EdgeType.Normal,
            Condition = condition,
            Order = order,
        };
    }

    private static WorkflowDefinition CreateDefinition(
        params object[] elements)
    {
        var nodes = elements.OfType<WorkflowNode>().ToList();
        var edges = elements.OfType<WorkflowEdge>().ToList();

        return new WorkflowDefinition
        {
            Id = Guid.NewGuid(),
            Nodes = nodes,
            Edges = edges,
        };
    }

    private static WorkflowInstance CreateInstance(
        WorkflowDefinition definition,
        InstanceStatus status = InstanceStatus.Pending)
    {
        return new WorkflowInstance
        {
            Id = Guid.NewGuid(),
            DefinitionId = definition.Id,
            Status = status,
            Variables = "{}",
        };
    }

    private WorkflowToken CreateToken(
        WorkflowInstance instance,
        WorkflowNode node,
        TokenStatus status = TokenStatus.Active)
    {
        var token = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            NodeId = node.Id,
            Status = status,
        };

        _dbContext.WorkflowTokens.Add(token);
        _dbContext.SaveChanges();

        return token;
    }

    private class TestAction : INodeAction
    {
        private readonly Action _callback;

        public TestAction(Action callback)
        {
            _callback = callback;
        }

        public Task ExecuteAsync(NodeActionContext context)
        {
            _callback();
            return Task.CompletedTask;
        }
    }

    private class ThrowingAction : INodeAction
    {
        public Task ExecuteAsync(NodeActionContext context)
        {
            throw new InvalidOperationException("Simulated action failure");
        }
    }
}
