namespace BudgetApp.Tests.Application;

using BudgetApp.Application.Services;
using BudgetApp.Domain.Interfaces;
using BudgetApp.Domain.Models;
using Moq;
using Xunit;

public class LedgerServiceTests
{
    private readonly Mock<ILedgerRepository> _mockLedgerRepository;
    private readonly Mock<IBudgetRepository> _mockBudgetRepository;
    private readonly LedgerService _service;

    public LedgerServiceTests()
    {
        _mockLedgerRepository = new Mock<ILedgerRepository>();
        _mockBudgetRepository = new Mock<IBudgetRepository>();
        _service = new LedgerService(_mockLedgerRepository.Object, _mockBudgetRepository.Object);
    }

    [Fact]
    public async Task RecordExpenseAsync_WithValidParameters_CreatesAndSavesEntry()
    {
        // Arrange
        var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
        var budgetId = budget.Id;
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(100m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget });
        _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
            .ReturnsAsync(budget);
        _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny<Budget>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
            .ReturnsAsync(new Ledger());
        _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
            .Returns(Task.CompletedTask);

        // Act
        var entry = await _service.RecordExpenseAsync(
            DateTime.Now,
            "Grocery Shopping",
            new Money(100m, "USD"),
            budgetAllocations);

        // Assert
        Assert.NotNull(entry);
        Assert.Equal(EntryType.Expense, entry.Type);
        Assert.Equal(100m, entry.Amount.Amount);
        _mockLedgerRepository.Verify(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()), Times.Once);
        _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny<Budget>()), Times.Once);
    }

    [Fact]
    public async Task RecordExpenseAsync_WithAllocationsNotMatchingAmount_ThrowsException()
    {
        // Arrange
        var budgetId = Guid.NewGuid();
        var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(50m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget });

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => 
            _service.RecordExpenseAsync(
                DateTime.Now,
                "Grocery Shopping",
                new Money(100m, "USD"),
                budgetAllocations));
    }

    [Fact]
    public async Task RecordExpenseAsync_WithNonExistentBudget_ThrowsException()
    {
        // Arrange
        var budgetId = Guid.NewGuid();
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(100m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget>());

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => 
            _service.RecordExpenseAsync(
                DateTime.Now,
                "Grocery Shopping",
                new Money(100m, "USD"),
                budgetAllocations));
    }

    [Fact]
    public async Task RecordExpenseAsync_WithMultipleBudgets_UpdatesAllBudgets()
    {
        // Arrange
        var budget1 = new BaseBudget("Grocery", new Money(100m, "USD"));
        var budget2 = new BaseBudget("Medical", new Money(50m, "USD"));
        var budgetId1 = budget1.Id;
        var budgetId2 = budget2.Id;
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId1, new Money(90m, "USD") },
            { budgetId2, new Money(10m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget1, budget2 });
        _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId1))
            .ReturnsAsync(budget1);
        _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId2))
            .ReturnsAsync(budget2);
        _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny<Budget>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
            .ReturnsAsync(new Ledger());
        _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
            .Returns(Task.CompletedTask);

        // Act
        var entry = await _service.RecordExpenseAsync(
            DateTime.Now,
            "Grocery Shopping",
            new Money(100m, "USD"),
            budgetAllocations);

        // Assert
        Assert.NotNull(entry);
        Assert.Equal(2, entry.BudgetAllocations.Count);
        _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny<Budget>()), Times.Exactly(2));
    }

    [Fact]
    public async Task RecordIncomeAsync_WithValidParameters_CreatesAndSavesEntry()
    {
        // Arrange
        var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
        var budgetId = budget.Id;
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(1000m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget });
        _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
            .ReturnsAsync(budget);
        _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny<Budget>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
            .ReturnsAsync(new Ledger());
        _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
            .Returns(Task.CompletedTask);
        _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
            .Returns(Task.CompletedTask);

        // Act
        var entry = await _service.RecordIncomeAsync(
            DateTime.Now,
            "Salary",
            new Money(1000m, "USD"),
            budgetAllocations);

        // Assert
        Assert.NotNull(entry);
        Assert.Equal(EntryType.Income, entry.Type);
        Assert.Equal(1000m, entry.Amount.Amount);
        _mockLedgerRepository.Verify(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()), Times.Once);
        _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny<Budget>()), Times.Once);
    }

    [Fact]
    public async Task RecordIncomeAsync_WithAllocationsNotMatchingAmount_ThrowsException()
    {
        // Arrange
        var budgetId = Guid.NewGuid();
        var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(500m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget });

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => 
            _service.RecordIncomeAsync(
                DateTime.Now,
                "Salary",
                new Money(1000m, "USD"),
                budgetAllocations));
    }

    [Fact]
    public async Task GetTotalBudgetBalanceAsync_ReturnsSumOfAllBudgets()
    {
        // Arrange
        var budgets = new List<Budget>
        {
            new BaseBudget("Grocery", new Money(100m, "USD")),
            new BaseBudget("Medical", new Money(200m, "USD"))
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(budgets);

        // Act
        var total = await _service.GetTotalBudgetBalanceAsync("USD");

        // Assert
        Assert.Equal(300m, total.Amount);
    }

    [Fact]
    public async Task RecordExpenseAsync_WithInsufficientBudgetBalance_ThrowsException()
    {
        // Arrange
        var budget = new BaseBudget("Grocery", new Money(50m, "USD"));
        var budgetId = budget.Id;
        var budgetAllocations = new Dictionary<Guid, Money>
        {
            { budgetId, new Money(100m, "USD") }
        };

        _mockBudgetRepository.Setup(r => r.GetAllAsync())
            .ReturnsAsync(new List<Budget> { budget });
        _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
            .ReturnsAsync(budget);

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => 
            _service.RecordExpenseAsync(
                DateTime.Now,
                "Grocery Shopping",
                new Money(100m, "USD"),
                budgetAllocations));
    }
}

