using AutoMapper; using Koogle.Application.DTOs; using Koogle.Application.Services; using Koogle.Domain.Entities; using Koogle.Domain.Enums; using Koogle.Domain.Interfaces; using Moq; using FluentAssertions; using Koogle.Application.Interfaces; namespace Koogle.Tests.Unit.Services; /// /// Unit tests for BookingCategoryService. /// public class BookingCategoryServiceTests { private readonly Mock _repositoryMock; private readonly Mock _clubContextMock; private readonly Mock _mapperMock; private readonly BookingCategoryService _sut; private readonly Guid _clubId = Guid.NewGuid(); public BookingCategoryServiceTests() { _repositoryMock = new Mock(); _clubContextMock = new Mock(); _mapperMock = new Mock(); _clubContextMock.Setup(c => c.ClubId).Returns(_clubId); SetupMapper(); _sut = new BookingCategoryService( _repositoryMock.Object, _clubContextMock.Object, _mapperMock.Object); } private void SetupMapper() { _mapperMock.Setup(m => m.Map(It.IsAny())) .Returns((BookingCategory c) => new BookingCategoryDto { Id = c.Id, Name = c.Name, Description = c.Description, CategoryType = c.CategoryType, IsSystemCategory = c.IsSystemCategory, Color = c.Color, Icon = c.Icon, IsActive = c.IsActive }); _mapperMock.Setup(m => m.Map>(It.IsAny>())) .Returns((List cats) => cats.Select(c => new BookingCategoryDto { Id = c.Id, Name = c.Name, Description = c.Description, CategoryType = c.CategoryType, IsSystemCategory = c.IsSystemCategory, Color = c.Color, Icon = c.Icon, IsActive = c.IsActive }).ToList()); } #region GetAllAsync Tests [Fact] public async Task GetAllAsync_ReturnsEmptyList_WhenNoCategoriesExist() { // Arrange _repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, false, It.IsAny())) .ReturnsAsync([]); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().BeEmpty(); } [Fact] public async Task GetAllAsync_ReturnsAllCategories_WhenCategoriesExist() { // Arrange var categories = new List { CreateCategory("Spielstrafe"), CreateCategory("Mitgliedsbeitrag"), CreateCategory("Sonstige Einnahmen") }; _repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, false, It.IsAny())) .ReturnsAsync(categories); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().HaveCount(3); } [Fact] public async Task GetAllAsync_IncludesInactive_WhenRequested() { // Arrange _repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, true, It.IsAny())) .ReturnsAsync([]); // Act await _sut.GetAllAsync(includeInactive: true); // Assert _repositoryMock.Verify(r => r.GetByClubIdAsync(_clubId, true, It.IsAny()), Times.Once); } #endregion #region GetByIdAsync Tests [Fact] public async Task GetByIdAsync_ReturnsNull_WhenCategoryNotFound() { // Arrange var id = Guid.NewGuid(); _repositoryMock.Setup(r => r.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((BookingCategory?)null); // Act var result = await _sut.GetByIdAsync(id); // Assert result.Should().BeNull(); } [Fact] public async Task GetByIdAsync_ReturnsNull_WhenCategoryBelongsToDifferentClub() { // Arrange var category = CreateCategory("Test", clubId: Guid.NewGuid()); _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); // Act var result = await _sut.GetByIdAsync(category.Id); // Assert result.Should().BeNull(); } [Fact] public async Task GetByIdAsync_ReturnsCategory_WhenExists() { // Arrange var category = CreateCategory("Test"); _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); // Act var result = await _sut.GetByIdAsync(category.Id); // Assert result.Should().NotBeNull(); result!.Name.Should().Be("Test"); } #endregion #region CreateAsync Tests [Fact] public async Task CreateAsync_CreatesCategory_WithCorrectData() { // Arrange var dto = new CreateBookingCategoryDto { Name = "Neue Kategorie", Description = "Beschreibung", CategoryType = BookingCategoryType.Income, Color = "#FF0000", Icon = "Star" }; BookingCategory? captured = null; _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => captured = c) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act var result = await _sut.CreateAsync(dto); // Assert captured.Should().NotBeNull(); captured!.Name.Should().Be("Neue Kategorie"); captured.ClubId.Should().Be(_clubId); captured.IsSystemCategory.Should().BeFalse(); captured.IsActive.Should().BeTrue(); } [Fact] public async Task CreateAsync_SetsCreatedAt() { // Arrange var dto = new CreateBookingCategoryDto { Name = "Test", CategoryType = BookingCategoryType.Expense }; var beforeCreate = DateTime.UtcNow; BookingCategory? captured = null; _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => captured = c) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act await _sut.CreateAsync(dto); var afterCreate = DateTime.UtcNow; // Assert captured!.CreatedAt.Should().BeOnOrAfter(beforeCreate); captured.CreatedAt.Should().BeOnOrBefore(afterCreate); } #endregion #region UpdateAsync Tests [Fact] public async Task UpdateAsync_ThrowsException_WhenCategoryNotFound() { // Arrange var dto = new UpdateBookingCategoryDto { Id = Guid.NewGuid(), Name = "Updated", IsActive = true }; _repositoryMock.Setup(r => r.GetByIdAsync(dto.Id, It.IsAny())) .ReturnsAsync((BookingCategory?)null); // Act var act = () => _sut.UpdateAsync(dto); // Assert await act.Should().ThrowAsync() .WithMessage($"Category with id {dto.Id} not found."); } [Fact] public async Task UpdateAsync_ThrowsException_WhenCategoryBelongsToDifferentClub() { // Arrange var category = CreateCategory("Test", clubId: Guid.NewGuid()); var dto = new UpdateBookingCategoryDto { Id = category.Id, Name = "Updated", IsActive = true }; _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); // Act var act = () => _sut.UpdateAsync(dto); // Assert await act.Should().ThrowAsync() .WithMessage("Category does not belong to current club."); } [Fact] public async Task UpdateAsync_UpdatesCategory_Correctly() { // Arrange var category = CreateCategory("Original"); var dto = new UpdateBookingCategoryDto { Id = category.Id, Name = "Updated", Description = "New Desc", Color = "#00FF00", Icon = "Edit", IsActive = false }; _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); _repositoryMock.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act var result = await _sut.UpdateAsync(dto); // Assert result.Name.Should().Be("Updated"); } #endregion #region DeleteAsync Tests [Fact] public async Task DeleteAsync_ReturnsFalse_WhenCategoryNotFound() { // Arrange var id = Guid.NewGuid(); _repositoryMock.Setup(r => r.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((BookingCategory?)null); // Act var result = await _sut.DeleteAsync(id); // Assert result.Should().BeFalse(); } [Fact] public async Task DeleteAsync_ThrowsException_WhenSystemCategory() { // Arrange var category = CreateCategory("Spielstrafe", isSystem: true); _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); // Act var act = () => _sut.DeleteAsync(category.Id); // Assert await act.Should().ThrowAsync() .WithMessage("System categories cannot be deleted."); } [Fact] public async Task DeleteAsync_SoftDeletes_WhenNonSystemCategory() { // Arrange var category = CreateCategory("Custom", isSystem: false); _repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny())) .ReturnsAsync(category); _repositoryMock.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act var result = await _sut.DeleteAsync(category.Id); // Assert result.Should().BeTrue(); category.IsDeleted.Should().BeTrue(); } #endregion #region EnsureSystemCategoriesAsync Tests [Fact] public async Task EnsureSystemCategoriesAsync_CreatesAllSystemCategories_WhenNoneExist() { // Arrange _repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, It.IsAny(), It.IsAny())) .ReturnsAsync((BookingCategory?)null); _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act await _sut.EnsureSystemCategoriesAsync(); // Assert _repositoryMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Exactly(4)); } [Fact] public async Task EnsureSystemCategoriesAsync_SkipsExisting_Categories() { // Arrange var existing = CreateCategory("Spielstrafe", isSystem: true); _repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, "Spielstrafe", It.IsAny())) .ReturnsAsync(existing); _repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, It.Is(s => s != "Spielstrafe"), It.IsAny())) .ReturnsAsync((BookingCategory?)null); _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((BookingCategory c, CancellationToken _) => c); // Act await _sut.EnsureSystemCategoriesAsync(); // Assert _repositoryMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Exactly(3)); } #endregion #region Helper Methods private BookingCategory CreateCategory( string name, Guid? clubId = null, bool isSystem = false, BookingCategoryType type = BookingCategoryType.Income) { return new BookingCategory { Id = Guid.NewGuid(), ClubId = clubId ?? _clubId, Name = name, CategoryType = type, IsSystemCategory = isSystem, IsActive = true, CreatedAt = DateTime.UtcNow }; } #endregion }