// EN: Unit tests for KitchenTicketServedDomainEventHandler - inventory deduction on served tickets.
// VI: Unit tests cho KitchenTicketServedDomainEventHandler - tru kho khi phieu da phuc vu.

using FluentAssertions;
using FnbEngine.API.Application.IntegrationEvents.EventHandlers;
using FnbEngine.Domain.AggregatesModel.KitchenAggregate;
using FnbEngine.Domain.AggregatesModel.RecipeAggregate;
using FnbEngine.Domain.AggregatesModel.SessionAggregate;
using FnbEngine.Domain.Events;
using FnbEngine.Infrastructure.ExternalServices;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace FnbEngine.UnitTests.Application.IntegrationEvents;

/// <summary>
/// EN: Unit tests for KitchenTicketServedDomainEventHandler.
/// VI: Unit tests cho KitchenTicketServedDomainEventHandler.
/// </summary>
public class KitchenTicketServedDomainEventHandlerTests
{
    private readonly Mock<IRecipeRepository> _recipeRepositoryMock;
    private readonly Mock<ISessionRepository> _sessionRepositoryMock;
    private readonly Mock<IInventoryServiceClient> _inventoryClientMock;
    private readonly Mock<ILogger<KitchenTicketServedDomainEventHandler>> _loggerMock;
    private readonly KitchenTicketServedDomainEventHandler _handler;

    private readonly Guid _shopId = Guid.NewGuid();
    private readonly Guid _sessionId = Guid.NewGuid();
    private readonly Guid _productId = Guid.NewGuid();

    public KitchenTicketServedDomainEventHandlerTests()
    {
        _recipeRepositoryMock = new Mock<IRecipeRepository>();
        _sessionRepositoryMock = new Mock<ISessionRepository>();
        _inventoryClientMock = new Mock<IInventoryServiceClient>();
        _loggerMock = new Mock<ILogger<KitchenTicketServedDomainEventHandler>>();

        _handler = new KitchenTicketServedDomainEventHandler(
            _recipeRepositoryMock.Object,
            _sessionRepositoryMock.Object,
            _inventoryClientMock.Object,
            _loggerMock.Object);
    }

    /// <summary>
    /// EN: Helper to create a KitchenTicket for testing.
    /// VI: Helper de tao KitchenTicket de test.
    /// </summary>
    private KitchenTicket CreateTicket(Guid? productId = null, int quantity = 1)
    {
        return new KitchenTicket(
            _sessionId,
            Guid.NewGuid(),
            productId ?? _productId,
            "Pho Bo",
            quantity,
            "Kitchen",
            0);
    }

    /// <summary>
    /// EN: Helper to create a Session for testing.
    /// VI: Helper de tao Session de test.
    /// </summary>
    private Session CreateSession()
    {
        return new Session(Guid.NewGuid(), _shopId, 2);
    }

    /// <summary>
    /// EN: Helper to create a Recipe with linked ingredients.
    /// VI: Helper de tao Recipe voi nguyen lieu co lien ket.
    /// </summary>
    private Recipe CreateRecipeWithLinkedIngredients(int ingredientCount = 2)
    {
        var recipe = new Recipe(_shopId, _productId, "Pho Bo Recipe", "Boil broth", 30);
        for (int i = 0; i < ingredientCount; i++)
        {
            recipe.AddIngredient(
                $"Ingredient {i + 1}",
                quantity: 100,
                unit: "g",
                costPerUnit: 10_000m,
                inventoryItemId: Guid.NewGuid(),
                quantityPerServing: 50m);
        }
        return recipe;
    }

    /// <summary>
    /// EN: Helper to create a Recipe with no linked inventory items.
    /// VI: Helper de tao Recipe khong co lien ket den inventory items.
    /// </summary>
    private Recipe CreateRecipeWithoutLinkedIngredients()
    {
        var recipe = new Recipe(_shopId, _productId, "Simple Drink", "Pour and serve", 2);
        recipe.AddIngredient("Water", 200, "ml", 0, inventoryItemId: null, quantityPerServing: 0);
        return recipe;
    }

    [Fact]
    public async Task Handle_WithValidEvent_ShouldCallInventoryClient()
    {
        // Arrange
        var ticket = CreateTicket();
        var session = CreateSession();
        var recipe = CreateRecipeWithLinkedIngredients();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(session);
        _recipeRepositoryMock.Setup(r => r.GetByProductIdAndShopAsync(_productId, _shopId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(recipe);
        _inventoryClientMock.Setup(c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);

        // Act
        await _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Should call inventory client exactly once with the correct deduction request.
        // VI: Nen goi inventory client dung 1 lan voi request tru kho chinh xac.
        _inventoryClientMock.Verify(
            c => c.DeductInventoryAsync(
                It.Is<DeductInventoryRequest>(req =>
                    req.ShopId == _shopId &&
                    req.ReferenceId == ticket.Id &&
                    req.ReferenceType == "KitchenTicket" &&
                    req.Items.Count == 2),
                It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task Handle_WithNoRecipeFound_ShouldSkipDeduction()
    {
        // Arrange
        var ticket = CreateTicket();
        var session = CreateSession();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(session);
        _recipeRepositoryMock.Setup(r => r.GetByProductIdAndShopAsync(_productId, _shopId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Recipe?)null);

        // Act
        await _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: No recipe means no inventory deduction should be attempted.
        // VI: Khong co cong thuc nghia la khong nen co gang tru kho.
        _inventoryClientMock.Verify(
            c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()),
            Times.Never);
    }

    [Fact]
    public async Task Handle_WhenInventoryClientFails_ShouldLogErrorNotThrow()
    {
        // Arrange
        var ticket = CreateTicket();
        var session = CreateSession();
        var recipe = CreateRecipeWithLinkedIngredients();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(session);
        _recipeRepositoryMock.Setup(r => r.GetByProductIdAndShopAsync(_productId, _shopId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(recipe);
        _inventoryClientMock.Setup(c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()))
            .ThrowsAsync(new HttpRequestException("Inventory service unavailable"));

        // Act
        var action = () => _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Should NOT throw - inventory failure should not block kitchen workflow.
        // VI: KHONG nen throw - loi tru kho khong nen chan luong bep.
        await action.Should().NotThrowAsync();

        _inventoryClientMock.Verify(
            c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task Handle_WithMultipleIngredients_ShouldDeductAll()
    {
        // Arrange
        var ticket = CreateTicket(quantity: 3);
        var session = CreateSession();
        var recipe = CreateRecipeWithLinkedIngredients(ingredientCount: 4);
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(session);
        _recipeRepositoryMock.Setup(r => r.GetByProductIdAndShopAsync(_productId, _shopId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(recipe);

        DeductInventoryRequest? capturedRequest = null;
        _inventoryClientMock.Setup(c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()))
            .Callback<DeductInventoryRequest, CancellationToken>((req, _) => capturedRequest = req)
            .ReturnsAsync(true);

        // Act
        await _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Should send all 4 ingredients in one deduction request.
        // VI: Nen gui tat ca 4 nguyen lieu trong mot request tru kho.
        capturedRequest.Should().NotBeNull();
        capturedRequest!.Items.Should().HaveCount(4);

        // EN: Each ingredient amount should be multiplied by ticket quantity (3).
        // VI: So luong moi nguyen lieu nen duoc nhan voi so luong phieu (3).
        capturedRequest.Items.Should().AllSatisfy(item =>
        {
            // EN: quantityPerServing=50, ticket.Quantity=3, so Math.Ceiling(50*3)=150
            // VI: quantityPerServing=50, ticket.Quantity=3, nen Math.Ceiling(50*3)=150
            item.Amount.Should().Be(150);
        });
    }

    [Fact]
    public async Task Handle_WithNoLinkedInventoryItems_ShouldSkipDeduction()
    {
        // Arrange
        var ticket = CreateTicket();
        var session = CreateSession();
        var recipe = CreateRecipeWithoutLinkedIngredients();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(session);
        _recipeRepositoryMock.Setup(r => r.GetByProductIdAndShopAsync(_productId, _shopId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(recipe);

        // Act
        await _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Recipe with no linked inventory items should not trigger deduction.
        // VI: Cong thuc khong co nguyen lieu lien ket kho khong nen kich hoat tru kho.
        _inventoryClientMock.Verify(
            c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()),
            Times.Never);
    }

    [Fact]
    public async Task Handle_WithSessionNotFound_ShouldSkipDeduction()
    {
        // Arrange
        var ticket = CreateTicket();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Session?)null);

        // Act
        await _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Missing session means we cannot determine the shop, so skip deduction.
        // VI: Thieu session nghia la khong the xac dinh shop, nen bo qua tru kho.
        _recipeRepositoryMock.Verify(
            r => r.GetByProductIdAndShopAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<CancellationToken>()),
            Times.Never);
        _inventoryClientMock.Verify(
            c => c.DeductInventoryAsync(It.IsAny<DeductInventoryRequest>(), It.IsAny<CancellationToken>()),
            Times.Never);
    }

    [Fact]
    public async Task Handle_WhenRepositoryThrows_ShouldNotThrow()
    {
        // Arrange
        var ticket = CreateTicket();
        var notification = new KitchenTicketServedDomainEvent(ticket);

        _sessionRepositoryMock.Setup(r => r.GetByIdAsync(ticket.SessionId, It.IsAny<CancellationToken>()))
            .ThrowsAsync(new InvalidOperationException("Database connection failed"));

        // Act
        var action = () => _handler.Handle(notification, CancellationToken.None);

        // Assert
        // EN: Repository failures should be caught and logged, not propagated.
        // VI: Loi repository nen duoc bat va log, khong nen lan truyen.
        await action.Should().NotThrowAsync();
    }

    [Fact]
    public void Constructor_WithNullRecipeRepository_ShouldThrowArgumentNullException()
    {
        // Act
        var action = () => new KitchenTicketServedDomainEventHandler(
            null!,
            _sessionRepositoryMock.Object,
            _inventoryClientMock.Object,
            _loggerMock.Object);

        // Assert
        action.Should().Throw<ArgumentNullException>()
            .And.ParamName.Should().Be("recipeRepository");
    }

    [Fact]
    public void Constructor_WithNullInventoryClient_ShouldThrowArgumentNullException()
    {
        // Act
        var action = () => new KitchenTicketServedDomainEventHandler(
            _recipeRepositoryMock.Object,
            _sessionRepositoryMock.Object,
            null!,
            _loggerMock.Object);

        // Assert
        action.Should().Throw<ArgumentNullException>()
            .And.ParamName.Should().Be("inventoryClient");
    }
}
