using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class JobApplicationsWorkflowSignalsTests { [Fact] public async Task Readiness_keeps_package_gap_when_notes_do_not_contain_saved_answer_block() { await using var db = CreateDb(); var company = new Company { Name = "Acme", OwnerUserId = "user-1" }; db.Companies.Add(company); await db.SaveChangesAsync(); var job = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1", Status = "Applied", DateApplied = DateTime.UtcNow, TailoredCvText = "Saved tailored CV", Notes = "Interview talking points and recruiter notes without any saved application-answer markers.", HasPortfolio = true, NextAction = "Wait for recruiter reply" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); var controller = CreateController(db, "user-1"); var result = await controller.GetReadiness(job.Id, CancellationToken.None); var ok = Assert.IsType(result.Result); var payload = Assert.IsType(ok.Value); Assert.Equal("package-work", payload.WorkflowSignal.ActionKey); Assert.True(payload.WorkflowSignal.HasPackageGap); Assert.True(payload.WorkflowSignal.HasTailoredCv); Assert.False(payload.WorkflowSignal.HasSavedApplicationAnswerDraft); Assert.Contains(payload.Missing, item => item.Contains("Save application answers", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(payload.Reminders, item => item.Contains("tailored cv and saved application answers", StringComparison.OrdinalIgnoreCase) && item.Contains("notes", StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task Reminders_return_normalized_workflow_routes_for_package_and_follow_up_actions() { await using var db = CreateDb(); var company = new Company { Name = "Acme", OwnerUserId = "user-1" }; db.Companies.Add(company); await db.SaveChangesAsync(); var packageGapJob = new JobApplication { JobTitle = "Platform Engineer", CompanyId = company.Id, OwnerUserId = "user-1", Status = "Waiting", DateApplied = DateTime.UtcNow.AddDays(-20), Notes = "General notes only", TailoredCvText = null, ResponseReceived = false }; var followUpJob = new JobApplication { JobTitle = "API Engineer", CompanyId = company.Id, OwnerUserId = "user-1", Status = "Waiting", DateApplied = DateTime.UtcNow.AddDays(-20), TailoredCvText = "Saved tailored CV", Notes = "Prep notes\n\n<<>>\nSaved answer\n<<>>", ResponseReceived = false }; db.JobApplications.AddRange(packageGapJob, followUpJob); await db.SaveChangesAsync(); var controller = CreateController(db, "user-1"); var result = await controller.GetReminders(14, CancellationToken.None); var ok = Assert.IsType(result.Result); var payload = Assert.IsType>(ok.Value); var packageReminder = Assert.Single(payload, item => item.Id == packageGapJob.Id); Assert.Equal("package-work", packageReminder.WorkflowSignal.ActionKey); Assert.Equal("tailored-cv", packageReminder.WorkflowSignal.WorkspaceTab); Assert.Equal(packageReminder.WorkflowSignal.Reason, packageReminder.FollowUpReason); Assert.True(packageReminder.WorkflowSignal.HasPackageGap); var followUpReminder = Assert.Single(payload, item => item.Id == followUpJob.Id); Assert.Equal("follow-up", followUpReminder.WorkflowSignal.ActionKey); Assert.Equal("follow-up", followUpReminder.WorkflowSignal.WorkspaceTab); Assert.Equal("waiting-update", followUpReminder.WorkflowSignal.FollowMode); Assert.Equal(followUpReminder.WorkflowSignal.Reason, followUpReminder.FollowUpReason); Assert.True(followUpReminder.WorkflowSignal.NeedsFollowUpAction); } private static JobApplicationsController CreateController(JobTrackerContext db, string userId) { var controller = new JobApplicationsController(db, Mock.Of(), Mock.Of(), CreateUserManager().Object); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")) } }; return controller; } private static JobTrackerContext CreateDb() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var currentUser = new Mock(); currentUser.SetupGet(service => service.UserId).Returns("user-1"); return new JobTrackerContext(options, currentUser.Object); } private static Mock> CreateUserManager() { var store = new Mock>(); return new Mock>( store.Object, Options.Create(new IdentityOptions()), new PasswordHasher(), Array.Empty>(), Array.Empty>(), new UpperInvariantLookupNormalizer(), new IdentityErrorDescriber(), null!, new NullLogger>() ); } }