// EN: Unit tests for DeductInventoryCommandHandler - bulk inventory deduction with idempotency.
// VI: Unit tests cho DeductInventoryCommandHandler - tru kho hang loat voi idempotency.

using FluentAssertions;
using InventoryService.API.Application.Commands.DeductInventory;
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
using InventoryService.Domain.SeedWork;
using InventoryService.Infrastructure.Idempotency;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace InventoryService.UnitTests.Application.Commands;

/// <summary>
/// EN: Unit tests for DeductInventoryCommandHandler.
/// VI: Unit tests cho DeductInventoryCommandHandler.
/// </summary>
public class DeductInventoryCommandHandlerTests
{
    private readonly Mock<IInventoryRepository> _repositoryMock;
    private readonly Mock<IRequestManager> _requestManagerMock;
    private readonly Mock<ILogger<DeductInventoryCommandHandler>> _loggerMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly DeductInventoryCommandHandler _handler;

    public DeductInventoryCommandHandlerTests()
    {
        _repositoryMock = new Mock<IInventoryRepository>();
        _requestManagerMock = new Mock<IRequestManager>();
        _loggerMock = new Mock<ILogger<DeductInventoryCommandHandler>>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();

        _unitOfWorkMock.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);
        _repositoryMock.Setup(r => r.UnitOfWork).Returns(_unitOfWorkMock.Object);

        _requestManagerMock.Setup(r => r.ExistAsync(It.IsAny<Guid>()))
            .ReturnsAsync(false);
        _requestManagerMock.Setup(r => r.CreateRequestForCommandAsync<DeductInventoryCommand>(It.IsAny<Guid>()))
            .Returns(Task.CompletedTask);

        _handler = new DeductInventoryCommandHandler(
            _repositoryMock.Object,
            _requestManagerMock.Object,
            _loggerMock.Object);
    }

    /// <summary>
    /// EN: Helper to create an InventoryItem with a specified available quantity.
    /// VI: Helper de tao InventoryItem voi so luong kha dung chi dinh.
    /// </summary>
    private static InventoryItem CreateInventoryItemWithStock(Guid id, int quantity)
    {
        var item = new InventoryItem(Guid.NewGuid(), Guid.NewGuid(), reorderLevel: 5);
        // EN: Use reflection to set Id and quantity since they use private fields.
        // VI: Dung reflection de set Id va quantity vi chung dung private fields.
        typeof(InventoryItem).BaseType!.GetProperty("Id")!.SetValue(item, id);
        if (quantity > 0)
        {
            item.StockIn(quantity, "Initial stock for test");
        }
        return item;
    }

    [Fact]
    public async Task Handle_WithValidCommand_ShouldDeductInventory()
    {
        // Arrange
        var itemId = Guid.NewGuid();
        var inventoryItem = CreateInventoryItemWithStock(itemId, 100);

        _repositoryMock.Setup(r => r.GetByIdAsync(itemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(inventoryItem);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Test deduction",
            Items: new List<DeductionItem>
            {
                new(itemId, 10, "pcs", "Sugar")
            });

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

        // Assert
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(1);
        result.ItemsSkipped.Should().Be(0);
        result.Items.Should().HaveCount(1);
        result.Items[0].Deducted.Should().BeTrue();
        result.Items[0].Error.Should().BeNull();

        _unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_WithInsufficientStock_ShouldReturnPartialDeduction()
    {
        // Arrange
        var itemId = Guid.NewGuid();
        var inventoryItem = CreateInventoryItemWithStock(itemId, 5);

        _repositoryMock.Setup(r => r.GetByIdAsync(itemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(inventoryItem);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Test partial deduction",
            Items: new List<DeductionItem>
            {
                new(itemId, 20, "pcs", "Flour")
            });

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

        // Assert
        // EN: Should partially deduct what is available (5 of 20 requested).
        // VI: Nen tru mot phan nhung gi co san (5 cua 20 yeu cau).
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(1);
        result.Items[0].Deducted.Should().BeTrue();
        result.Items[0].Error.Should().Contain("Partial deduction");
    }

    [Fact]
    public async Task Handle_WithNonExistentItem_ShouldReturnNotFound()
    {
        // Arrange
        var missingItemId = Guid.NewGuid();

        _repositoryMock.Setup(r => r.GetByIdAsync(missingItemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((InventoryItem?)null);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Test not found",
            Items: new List<DeductionItem>
            {
                new(missingItemId, 5, "pcs", "Missing Item")
            });

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

        // Assert
        result.Should().NotBeNull();
        result.Success.Should().BeFalse();
        result.ItemsDeducted.Should().Be(0);
        result.ItemsSkipped.Should().Be(1);
        result.Items[0].Deducted.Should().BeFalse();
        result.Items[0].Error.Should().Be("Item not found");
    }

    [Fact]
    public async Task Handle_WithZeroStockAvailable_ShouldSkipDeduction()
    {
        // Arrange
        var itemId = Guid.NewGuid();
        var inventoryItem = CreateInventoryItemWithStock(itemId, 0);

        _repositoryMock.Setup(r => r.GetByIdAsync(itemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(inventoryItem);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Test zero stock",
            Items: new List<DeductionItem>
            {
                new(itemId, 10, "pcs", "Empty Item")
            });

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

        // Assert
        // EN: Item with 0 stock and insufficient stock should be skipped.
        // VI: Item voi 0 stock va khong du stock nen duoc bo qua.
        result.Should().NotBeNull();
        result.Success.Should().BeFalse();
        result.ItemsSkipped.Should().Be(1);
        result.Items[0].Deducted.Should().BeFalse();
        result.Items[0].Error.Should().Contain("Insufficient stock (0 available)");
    }

    [Fact]
    public async Task Handle_WithDuplicateRequest_ShouldReturnIdempotentResult()
    {
        // Arrange
        var referenceId = Guid.NewGuid();

        _requestManagerMock.Setup(r => r.ExistAsync(referenceId))
            .ReturnsAsync(true);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: referenceId,
            ReferenceType: "KitchenTicket",
            Reason: "Duplicate test",
            Items: new List<DeductionItem>
            {
                new(Guid.NewGuid(), 5, "pcs", "Item A")
            });

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

        // Assert
        // EN: Duplicate request should be detected and skipped.
        // VI: Request trung lap nen duoc phat hien va bo qua.
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(0);
        result.ItemsSkipped.Should().Be(1);
        result.Items[0].Error.Should().Be("Duplicate request");

        // EN: Should not attempt to get inventory items.
        // VI: Khong nen co gang lay inventory items.
        _repositoryMock.Verify(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Fact]
    public async Task Handle_WithMultipleItems_ShouldDeductAll()
    {
        // Arrange
        var itemId1 = Guid.NewGuid();
        var itemId2 = Guid.NewGuid();
        var itemId3 = Guid.NewGuid();

        var item1 = CreateInventoryItemWithStock(itemId1, 50);
        var item2 = CreateInventoryItemWithStock(itemId2, 30);
        var item3 = CreateInventoryItemWithStock(itemId3, 100);

        _repositoryMock.Setup(r => r.GetByIdAsync(itemId1, It.IsAny<CancellationToken>())).ReturnsAsync(item1);
        _repositoryMock.Setup(r => r.GetByIdAsync(itemId2, It.IsAny<CancellationToken>())).ReturnsAsync(item2);
        _repositoryMock.Setup(r => r.GetByIdAsync(itemId3, It.IsAny<CancellationToken>())).ReturnsAsync(item3);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Multi-item deduction",
            Items: new List<DeductionItem>
            {
                new(itemId1, 10, "g", "Sugar"),
                new(itemId2, 5, "ml", "Milk"),
                new(itemId3, 2, "pcs", "Cup")
            });

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

        // Assert
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(3);
        result.ItemsSkipped.Should().Be(0);
        result.Items.Should().HaveCount(3);
        result.Items.Should().AllSatisfy(i => i.Deducted.Should().BeTrue());

        _unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_WithMixedResults_ShouldDeductFoundItemsAndSkipMissing()
    {
        // Arrange
        var existingItemId = Guid.NewGuid();
        var missingItemId = Guid.NewGuid();

        var existingItem = CreateInventoryItemWithStock(existingItemId, 50);

        _repositoryMock.Setup(r => r.GetByIdAsync(existingItemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(existingItem);
        _repositoryMock.Setup(r => r.GetByIdAsync(missingItemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((InventoryItem?)null);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Mixed test",
            Items: new List<DeductionItem>
            {
                new(existingItemId, 5, "pcs", "Sugar"),
                new(missingItemId, 3, "ml", "Missing Ingredient")
            });

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

        // Assert
        // EN: Should deduct existing item and skip missing one.
        // VI: Nen tru item ton tai va bo qua item khong tim thay.
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(1);
        result.ItemsSkipped.Should().Be(1);
    }

    [Fact]
    public async Task Handle_WithItemExceptionDuringDeduction_ShouldContinueWithRemainingItems()
    {
        // Arrange
        var faultyItemId = Guid.NewGuid();
        var goodItemId = Guid.NewGuid();

        var goodItem = CreateInventoryItemWithStock(goodItemId, 50);

        _repositoryMock.Setup(r => r.GetByIdAsync(faultyItemId, It.IsAny<CancellationToken>()))
            .ThrowsAsync(new InvalidOperationException("Database timeout"));
        _repositoryMock.Setup(r => r.GetByIdAsync(goodItemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(goodItem);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: Guid.NewGuid(),
            ReferenceType: "KitchenTicket",
            Reason: "Error resilience test",
            Items: new List<DeductionItem>
            {
                new(faultyItemId, 5, "pcs", "Faulty Item"),
                new(goodItemId, 3, "pcs", "Good Item")
            });

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

        // Assert
        // EN: Should handle error for faulty item but still deduct good item.
        // VI: Nen xu ly loi cho item loi nhung van tru item tot.
        result.Should().NotBeNull();
        result.Success.Should().BeTrue();
        result.ItemsDeducted.Should().Be(1);
        result.ItemsSkipped.Should().Be(1);
        result.Items[0].Deducted.Should().BeFalse();
        result.Items[0].Error.Should().Be("Database timeout");
        result.Items[1].Deducted.Should().BeTrue();
    }

    [Fact]
    public void Constructor_WithNullRepository_ShouldThrowArgumentNullException()
    {
        // Act
        var action = () => new DeductInventoryCommandHandler(
            null!,
            _requestManagerMock.Object,
            _loggerMock.Object);

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

    [Fact]
    public void Constructor_WithNullRequestManager_ShouldThrowArgumentNullException()
    {
        // Act
        var action = () => new DeductInventoryCommandHandler(
            _repositoryMock.Object,
            null!,
            _loggerMock.Object);

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

    [Fact]
    public void Constructor_WithNullLogger_ShouldThrowArgumentNullException()
    {
        // Act
        var action = () => new DeductInventoryCommandHandler(
            _repositoryMock.Object,
            _requestManagerMock.Object,
            null!);

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

    [Fact]
    public async Task Handle_ShouldRecordIdempotencyRequest()
    {
        // Arrange
        var referenceId = Guid.NewGuid();
        var itemId = Guid.NewGuid();
        var inventoryItem = CreateInventoryItemWithStock(itemId, 50);

        _repositoryMock.Setup(r => r.GetByIdAsync(itemId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(inventoryItem);

        var command = new DeductInventoryCommand(
            ShopId: Guid.NewGuid(),
            ReferenceId: referenceId,
            ReferenceType: "KitchenTicket",
            Reason: "Idempotency test",
            Items: new List<DeductionItem>
            {
                new(itemId, 5, "pcs", "Item A")
            });

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

        // Assert
        // EN: Should check and record the idempotency request.
        // VI: Nen kiem tra va ghi nhan idempotency request.
        _requestManagerMock.Verify(r => r.ExistAsync(referenceId), Times.Once);
        _requestManagerMock.Verify(
            r => r.CreateRequestForCommandAsync<DeductInventoryCommand>(referenceId), Times.Once);
    }
}
