using Health.Application.Medications; using Health.Domain.Entities; using Health.Domain.Enums; using Health.Infrastructure.AI; using Health.Infrastructure.Data; using Health.Infrastructure.Data.Records; using Health.Infrastructure.Medications; using Health.Infrastructure.Notifications; using Health.Infrastructure.Reports; using Microsoft.EntityFrameworkCore; namespace Health.Tests; public sealed class PersistencePipelineTests { [Fact] public async Task AiConfirmation_CanOnlyBeTakenOnceByOwner() { await using var db = CreateDbContext(); var store = new EfAiWriteConfirmationStore(db); var ownerId = Guid.NewGuid(); var command = await store.CreateAsync(ownerId, "record_health_data", "{}", TimeSpan.FromMinutes(10), CancellationToken.None); var wrongUser = await store.TakeAsync(command.Id, Guid.NewGuid(), CancellationToken.None); var firstTake = await store.TakeAsync(command.Id, ownerId, CancellationToken.None); var secondTake = await store.TakeAsync(command.Id, ownerId, CancellationToken.None); Assert.Null(wrongUser); Assert.NotNull(firstTake); Assert.Null(secondTake); } [Fact] public async Task ReportTask_RetryUsesBackoffBeforeMaxAttempts() { await using var db = CreateDbContext(); var task = NewReportTask(attempts: 1); db.ReportAnalysisTasks.Add(task); await db.SaveChangesAsync(); var queue = new EfReportAnalysisQueue(db); var before = DateTime.UtcNow; await queue.RetryAsync(task.Id, "temporary failure", CancellationToken.None); Assert.Equal("Pending", task.Status); Assert.True(task.AvailableAt > before); Assert.Equal("temporary failure", task.LastError); } [Fact] public async Task ReportTask_RetryMarksFailedAtMaxAttempts() { await using var db = CreateDbContext(); var task = NewReportTask(attempts: 3); db.ReportAnalysisTasks.Add(task); await db.SaveChangesAsync(); var queue = new EfReportAnalysisQueue(db); await queue.RetryAsync(task.Id, new string('x', 2500), CancellationToken.None); Assert.Equal("Failed", task.Status); Assert.Equal(2000, task.LastError!.Length); } [Fact] public async Task ReportTask_OnlyEnqueuesFailureNotificationAtFinalAttempt() { await using var db = CreateDbContext(); var userId = Guid.NewGuid(); var report = new Report { Id = Guid.NewGuid(), UserId = userId, FileUrl = "report.jpg", Status = ReportStatus.Analyzing, }; var task = NewReportTask(attempts: 2); task.ReportId = report.Id; db.Reports.Add(report); db.ReportAnalysisTasks.Add(task); await db.SaveChangesAsync(); var queue = new EfReportAnalysisQueue(db, new EfUserNotificationProducer(db)); await queue.RetryAsync(task.Id, "temporary failure", CancellationToken.None); Assert.Empty(db.NotificationOutbox); task.Status = "Processing"; task.Attempts = 3; await db.SaveChangesAsync(); await queue.RetryAsync(task.Id, "final failure", CancellationToken.None); var notification = Assert.Single(db.NotificationOutbox); Assert.Equal(task.Id, notification.SourceTaskId); Assert.Equal("ReportAnalysisFailed", notification.Type); Assert.Equal(userId, notification.UserId); } [Fact] public async Task MedicationReminder_MorningDoseDoesNotSuppressEveningDose() { await using var db = CreateDbContext(); var userId = Guid.NewGuid(); var medicationId = Guid.NewGuid(); var scanTimeUtc = new DateTime(2026, 6, 20, 12, 2, 0, DateTimeKind.Utc); db.Medications.Add(new Medication { Id = medicationId, UserId = userId, Name = "测试药物", Frequency = MedicationFrequency.TwiceDaily, TimeOfDay = [new TimeOnly(8, 0), new TimeOnly(20, 0)], StartDate = new DateOnly(2026, 6, 20), IsActive = true, }); db.MedicationLogs.Add(new MedicationLog { Id = Guid.NewGuid(), UserId = userId, MedicationId = medicationId, Status = MedicationLogStatus.Taken, ScheduledTime = new TimeOnly(8, 0), ConfirmedAt = new DateTime(2026, 6, 20, 0, 1, 0, DateTimeKind.Utc), CreatedAt = new DateTime(2026, 6, 20, 0, 1, 0, DateTimeKind.Utc), }); await db.SaveChangesAsync(); var scanner = new MedicationReminderScanner(new EfMedicationRepository(db)); var tasks = await scanner.ScanAsync(scanTimeUtc, CancellationToken.None); var reminder = Assert.Single(tasks); Assert.Equal(new TimeOnly(20, 0), reminder.ScheduledTime); } [Fact] public async Task MedicationReminder_ConfirmedDoseIsNotRemindedAgain() { await using var db = CreateDbContext(); var userId = Guid.NewGuid(); var medicationId = Guid.NewGuid(); var confirmedAt = new DateTime(2026, 6, 20, 12, 0, 0, DateTimeKind.Utc); db.Medications.Add(new Medication { Id = medicationId, UserId = userId, Name = "测试药物", Frequency = MedicationFrequency.Daily, TimeOfDay = [new TimeOnly(20, 0)], StartDate = new DateOnly(2026, 6, 20), IsActive = true, }); db.MedicationLogs.Add(new MedicationLog { Id = Guid.NewGuid(), UserId = userId, MedicationId = medicationId, Status = MedicationLogStatus.Taken, ScheduledTime = new TimeOnly(20, 0), ConfirmedAt = confirmedAt, CreatedAt = confirmedAt, }); await db.SaveChangesAsync(); var scanner = new MedicationReminderScanner(new EfMedicationRepository(db)); var tasks = await scanner.ScanAsync( new DateTime(2026, 6, 20, 12, 2, 0, DateTimeKind.Utc), CancellationToken.None); Assert.Empty(tasks); } private static ReportAnalysisTaskRecord NewReportTask(int attempts) => new() { Id = Guid.NewGuid(), ReportId = Guid.NewGuid(), FilePath = "report.jpg", Status = "Processing", Attempts = attempts, AvailableAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; private static AppDbContext CreateDbContext() => new( new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); }