using FluentAssertions;
using FnbEngine.API.Application.Commands;
using FnbEngine.Domain.AggregatesModel.RecipeAggregate;
using FnbEngine.Domain.SeedWork;
using Moq;
using Xunit;

namespace FnbEngine.UnitTests.Application.Commands;

/// <summary>
/// EN: Unit tests for Recipe command handlers (Create, Update, Delete).
/// VI: Unit tests cho cac Recipe command handlers (Create, Update, Delete).
/// </summary>
public class RecipeCommandHandlersTests
{
    private readonly Mock<IRecipeRepository> _repoMock;

    public RecipeCommandHandlersTests()
    {
        _repoMock = new Mock<IRecipeRepository>();
        _repoMock.Setup(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);
    }

    // --- CreateRecipeCommandHandler ---

    [Fact]
    public async Task CreateRecipe_WithValidCommand_ShouldCreateAndReturnId()
    {
        // Arrange
        _repoMock.Setup(r => r.Add(It.IsAny<Recipe>())).Returns((Recipe r) => r);
        var handler = new CreateRecipeCommandHandler(_repoMock.Object);

        var command = new CreateRecipeCommand
        {
            ShopId = Guid.NewGuid(),
            ProductId = Guid.NewGuid(),
            Name = "Pho Bo",
            Instructions = "Boil broth",
            PrepTimeMinutes = 480
        };

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

        // Assert
        result.Should().NotBeEmpty();
        _repoMock.Verify(r => r.Add(It.IsAny<Recipe>()), Times.Once);
        _repoMock.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task CreateRecipe_WithIngredients_ShouldAddIngredientsToRecipe()
    {
        // Arrange
        Recipe? capturedRecipe = null;
        _repoMock.Setup(r => r.Add(It.IsAny<Recipe>()))
            .Callback<Recipe>(r => capturedRecipe = r)
            .Returns((Recipe r) => r);
        var handler = new CreateRecipeCommandHandler(_repoMock.Object);

        var command = new CreateRecipeCommand
        {
            ShopId = Guid.NewGuid(),
            ProductId = Guid.NewGuid(),
            Name = "Pho Bo",
            PrepTimeMinutes = 480,
            Ingredients = new List<IngredientItem>
            {
                new("Beef bones", 2.0m, "kg", 150_000m),
                new("Rice noodles", 0.5m, "kg", 30_000m, Guid.NewGuid(), 0.1m)
            }
        };

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

        // Assert
        capturedRecipe.Should().NotBeNull();
        capturedRecipe!.Ingredients.Should().HaveCount(2);
        capturedRecipe.Ingredients[0].IngredientName.Should().Be("Beef bones");
        capturedRecipe.Ingredients[1].IngredientName.Should().Be("Rice noodles");
    }

    [Fact]
    public async Task CreateRecipe_WithNullIngredients_ShouldCreateRecipeWithoutIngredients()
    {
        // Arrange
        Recipe? capturedRecipe = null;
        _repoMock.Setup(r => r.Add(It.IsAny<Recipe>()))
            .Callback<Recipe>(r => capturedRecipe = r)
            .Returns((Recipe r) => r);
        var handler = new CreateRecipeCommandHandler(_repoMock.Object);

        var command = new CreateRecipeCommand
        {
            ShopId = Guid.NewGuid(),
            ProductId = Guid.NewGuid(),
            Name = "Simple Dish",
            PrepTimeMinutes = 10
        };

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

        // Assert
        capturedRecipe.Should().NotBeNull();
        capturedRecipe!.Ingredients.Should().BeEmpty();
    }

    // --- UpdateRecipeCommandHandler ---

    [Fact]
    public async Task UpdateRecipe_WithExistingRecipe_ShouldUpdateAndReturnTrue()
    {
        // Arrange
        var recipeId = Guid.NewGuid();
        var existingRecipe = new Recipe(Guid.NewGuid(), Guid.NewGuid(), "Old Name", "Old", 30);
        _repoMock.Setup(r => r.GetByIdAsync(recipeId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(existingRecipe);
        var handler = new UpdateRecipeCommandHandler(_repoMock.Object);

        var command = new UpdateRecipeCommand
        {
            RecipeId = recipeId,
            ShopId = Guid.NewGuid(),
            ProductId = Guid.NewGuid(),
            Name = "New Name",
            Instructions = "New instructions",
            PrepTimeMinutes = 60,
            Ingredients = new List<IngredientItem>
            {
                new("Salt", 10m, "g", 500m)
            }
        };

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

        // Assert
        result.Should().BeTrue();
        existingRecipe.Name.Should().Be("New Name");
        existingRecipe.Ingredients.Should().HaveCount(1);
        _repoMock.Verify(r => r.Update(existingRecipe), Times.Once);
    }

    [Fact]
    public async Task UpdateRecipe_WithNonExistentRecipe_ShouldReturnFalse()
    {
        // Arrange
        _repoMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync((Recipe?)null);
        var handler = new UpdateRecipeCommandHandler(_repoMock.Object);

        var command = new UpdateRecipeCommand
        {
            RecipeId = Guid.NewGuid(),
            ShopId = Guid.NewGuid(),
            ProductId = Guid.NewGuid(),
            Name = "Anything",
            PrepTimeMinutes = 10
        };

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

        // Assert
        result.Should().BeFalse();
        _repoMock.Verify(r => r.Update(It.IsAny<Recipe>()), Times.Never);
    }

    // --- DeleteRecipeCommandHandler ---

    [Fact]
    public async Task DeleteRecipe_WithExistingRecipe_ShouldDeactivateAndReturnTrue()
    {
        // Arrange
        var recipeId = Guid.NewGuid();
        var existingRecipe = new Recipe(Guid.NewGuid(), Guid.NewGuid(), "Pho Bo", null, 480);
        _repoMock.Setup(r => r.GetByIdAsync(recipeId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(existingRecipe);
        var handler = new DeleteRecipeCommandHandler(_repoMock.Object);

        var command = new DeleteRecipeCommand(recipeId);

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

        // Assert
        result.Should().BeTrue();
        existingRecipe.IsActive.Should().BeFalse();
        _repoMock.Verify(r => r.Update(existingRecipe), Times.Once);
        _repoMock.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task DeleteRecipe_WithNonExistentRecipe_ShouldReturnFalse()
    {
        // Arrange
        _repoMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync((Recipe?)null);
        var handler = new DeleteRecipeCommandHandler(_repoMock.Object);

        var command = new DeleteRecipeCommand(Guid.NewGuid());

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

        // Assert
        result.Should().BeFalse();
    }
}
