using Api.SeaHavenIndustries.DTOs; using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class CalendarServiceTests { private static ApplicationDbContext NewContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static CalendarService NewService(ApplicationDbContext ctx) => new(new CalendarDataService(ctx)); private static EditEventInput ValidInput(int? id = null, string title = "Standup") => new() { Id = id, Title = title, ReminderMinutes = 15, EventColor = "primary", StartDate = new DateTime(2026, 6, 1), StartTime = "08:00 AM", EndDate = new DateTime(2026, 6, 1), EndTime = "08:30 AM" }; private static Events SeedEvent(ApplicationDbContext ctx, string title, DateTime startDate, bool? isDeleted = false) { var ev = new Events { Title = title, EventColor = "primary", ReminderMinutes = 15, StartDate = startDate, EndDate = startDate, IsDeleted = isDeleted, CreatedDate = DateTime.UtcNow }; ctx.Events.Add(ev); ctx.SaveChanges(); return ev; } private static object Prop(object source, string name) => source.GetType().GetProperty(name)!.GetValue(source)!; [Fact] public async Task CreateEventAsync_PersistsEventWithMappedFieldsAndCreatedDate() { using var ctx = NewContext(); await NewService(ctx).CreateEventAsync(ValidInput(), CancellationToken.None); var saved = ctx.Events.Single(); saved.Title.Should().Be("Standup"); saved.IsDeleted.Should().Be(false); saved.CreatedDate.Should().NotBeNull(); saved.CreatedDate!.Value.Kind.Should().Be(DateTimeKind.Utc); } [Fact] public async Task UpdateEventAsync_WhenNotFound_ReturnsFalse() { using var ctx = NewContext(); var result = await NewService(ctx).UpdateEventAsync(ValidInput(id: 999), CancellationToken.None); result.Should().BeFalse(); } [Fact] public async Task UpdateEventAsync_WhenSoftDeleted_ReturnsFalse() { using var ctx = NewContext(); var existing = SeedEvent(ctx, "Old", new DateTime(2026, 6, 1), isDeleted: true); var result = await NewService(ctx).UpdateEventAsync(ValidInput(id: existing.Id, title: "New"), CancellationToken.None); result.Should().BeFalse(); } [Fact] public async Task UpdateEventAsync_WhenEventExists_UpdatesFieldsAndStampsModified() { using var ctx = NewContext(); var existing = SeedEvent(ctx, "Old", new DateTime(2026, 6, 1)); var result = await NewService(ctx).UpdateEventAsync(ValidInput(id: existing.Id, title: "New"), CancellationToken.None); result.Should().BeTrue(); var updated = ctx.Events.Single(); updated.Title.Should().Be("New"); updated.LastModificationTime.Should().NotBeNull(); updated.LastModificationTime!.Value.Kind.Should().Be(DateTimeKind.Utc); } [Fact] public async Task GetEventByIdAsync_WhenExists_ReturnsMappedDto() { using var ctx = NewContext(); var existing = SeedEvent(ctx, "Review", new DateTime(2026, 6, 1)); var dto = await NewService(ctx).GetEventByIdAsync(existing.Id, CancellationToken.None); dto.Should().NotBeNull(); dto!.Title.Should().Be("Review"); } [Fact] public async Task GetEventByIdAsync_WhenSoftDeleted_ReturnsNull() { using var ctx = NewContext(); var deleted = SeedEvent(ctx, "Gone", new DateTime(2026, 6, 1), isDeleted: true); var dto = await NewService(ctx).GetEventByIdAsync(deleted.Id, CancellationToken.None); dto.Should().BeNull(); } [Fact] public async Task DeleteEventAsync_WhenExists_SoftDeletesAndKeepsRow() { using var ctx = NewContext(); var existing = SeedEvent(ctx, "Cancel", new DateTime(2026, 6, 1)); var result = await NewService(ctx).DeleteEventAsync(existing.Id, CancellationToken.None); result.Should().BeTrue(); var row = ctx.Events.Single(); row.IsDeleted.Should().Be(true); row.DeletionTime.Should().NotBeNull(); row.DeletionTime!.Value.Kind.Should().Be(DateTimeKind.Utc); } [Fact] public async Task DeleteEventAsync_WhenNotFound_ReturnsFalse() { using var ctx = NewContext(); var result = await NewService(ctx).DeleteEventAsync(12345, CancellationToken.None); result.Should().BeFalse(); } [Fact] public async Task GetEventListAsync_ExcludesDeletedAndCountsAfterFilter() { using var ctx = NewContext(); SeedEvent(ctx, "Alpha meeting", new DateTime(2026, 6, 1)); SeedEvent(ctx, "Beta meeting", new DateTime(2026, 6, 2)); SeedEvent(ctx, "Alpha review", new DateTime(2026, 6, 3)); SeedEvent(ctx, "Alpha archived", new DateTime(2026, 6, 4), isDeleted: true); var result = await NewService(ctx).GetEventListAsync("alpha", 1, 10, CancellationToken.None); result.TotalCount.Should().Be(2); result.Rows.Should().HaveCount(2); } [Fact] public async Task GetEventListAsync_OrdersByStartDateDescending() { using var ctx = NewContext(); SeedEvent(ctx, "Earliest", new DateTime(2026, 6, 1)); SeedEvent(ctx, "Latest", new DateTime(2026, 6, 10)); SeedEvent(ctx, "Middle", new DateTime(2026, 6, 5)); var result = await NewService(ctx).GetEventListAsync(null, 1, 10, CancellationToken.None); result.Rows.Select(r => r.Title).Should().ContainInOrder("Latest", "Middle", "Earliest"); } [Fact] public async Task GetEventsForCalendarAsync_ReturnsOnlyEventsOverlappingRange() { using var ctx = NewContext(); SeedEvent(ctx, "InRange", new DateTime(2026, 6, 15)); SeedEvent(ctx, "BeforeRange", new DateTime(2026, 5, 1)); SeedEvent(ctx, "AfterRange", new DateTime(2026, 7, 1)); var events = await NewService(ctx).GetEventsForCalendarAsync( new DateTime(2026, 6, 1), new DateTime(2026, 6, 30), CancellationToken.None); events.Select(e => e.Title).Should().BeEquivalentTo(new[] { "InRange" }); } }