using Duempelkas.Domain.Entities; using Duempelkas.Domain.Enums; using Duempelkas.Infrastructure.Persistence; using Duempelkas.Infrastructure.Services; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace Duempelkas.Tests; public class BalanceCalculationTests : IDisposable { private readonly FinanceDbContext _db; private readonly BalanceQueryService _balanceQueryService; private readonly EntryService _entryService; private readonly string _connectionString = $"Data Source=duempelkas-balance-tests-{Guid.NewGuid():N};Mode=Memory;Cache=Shared"; public BalanceCalculationTests() { var options = new DbContextOptionsBuilder() .UseSqlite(_connectionString) .Options; _db = new FinanceDbContext(options); _db.Database.OpenConnection(); _db.Database.EnsureCreated(); var dbFactory = new TestDbContextFactory(options); _balanceQueryService = new BalanceQueryService(dbFactory); _entryService = new EntryService(dbFactory); } [Fact] public async Task AccountBalance_CalculatesCorrectTotals() { var account = new Account { Name = "Test Account", CarryoverBalance = 500.00m }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); _db.Entries.AddRange( new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 1, 15), Title = "Gehalt", Amount = 800.00m }, new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Income, Date = new DateTime(2026, 2, 15), Title = "Bonus", Amount = 400.00m }, new Entry { AccountId = account.Id, DisplayId = "2026-003", Type = EntryType.Expense, Date = new DateTime(2026, 1, 20), Title = "Miete", Amount = 200.00m }, new Entry { AccountId = account.Id, DisplayId = "2026-004", Type = EntryType.Expense, Date = new DateTime(2026, 2, 20), Title = "Lebensmittel", Amount = 250.00m } ); await _db.SaveChangesAsync(); var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id); balance.CarryoverBalance.Should().Be(500.00m); balance.TotalIncome.Should().Be(1200.00m); balance.TotalExpense.Should().Be(450.00m); balance.TotalBalance.Should().Be(1250.00m); } [Fact] public async Task AccountBalance_IncludesTransfersCorrectly() { var accountA = new Account { Name = "Account A", CarryoverBalance = 500.00m }; var accountB = new Account { Name = "Account B" }; _db.Accounts.AddRange(accountA, accountB); await _db.SaveChangesAsync(); _db.Entries.Add(new Entry { AccountId = accountA.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 1, 10), Title = "Gehalt", Amount = 1000.00m }); await _db.SaveChangesAsync(); await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 2, 1), "Umbuchung nach B", 300.00m); var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); balanceA.CarryoverBalance.Should().Be(500.00m); balanceA.TotalIncome.Should().Be(1000.00m); balanceA.TotalExpense.Should().Be(300.00m); balanceA.TotalBalance.Should().Be(1200.00m); } [Fact] public async Task AccountBalance_WithCarryover() { var account = new Account { Name = "Konto mit Übertrag", CarryoverBalance = 1000.00m }; _db.Accounts.Add(account); await _db.SaveChangesAsync(); _db.Entries.AddRange( new Entry { AccountId = account.Id, DisplayId = "2025-001", Type = EntryType.Income, Date = new DateTime(2025, 6, 1), Title = "Einnahme 2025", Amount = 500.00m }, new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 6, 1), Title = "Einnahme 2026", Amount = 300.00m }, new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Expense, Date = new DateTime(2026, 7, 1), Title = "Ausgabe 2026", Amount = 150.00m } ); await _db.SaveChangesAsync(); var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id); // 1000 + 500 + 300 - 150 = 1650 balance.TotalBalance.Should().Be(1650.00m); balance.CarryoverBalance.Should().Be(1000.00m); } public void Dispose() { _db.Database.CloseConnection(); _db.Dispose(); } private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions _options; public TestDbContextFactory(DbContextOptions options) { _options = options; } public FinanceDbContext CreateDbContext() => new(_options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(new FinanceDbContext(_options)); } }