using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
///
/// Moving a job backwards into a pre-application stage clears DateApplied so analytics stay
/// accurate. These tests guard the other half of that bargain: the application activity must be
/// preserved as history rather than destroyed.
///
public sealed class JobApplicationsAppliedDateHistoryTests
{
private static readonly DateTime AppliedOn = new(2026, 3, 1, 9, 30, 0, DateTimeKind.Utc);
[Fact]
public async Task Moving_back_to_a_prospect_stage_clears_the_date_but_records_it_as_history()
{
await using var db = CreateDb();
var job = await SeedAppliedJob(db);
var controller = CreateController(db, "user-1");
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Saved"), CancellationToken.None);
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
Assert.Equal("Saved", saved.Status);
Assert.Null(saved.DateApplied);
var cleared = await db.JobEvents.SingleAsync(e => e.Type == JobPipeline.AppliedDateClearedEvent);
// Round-tripped, so the original date is recoverable exactly — not just described in prose.
Assert.Equal(AppliedOn, DateTime.Parse(cleared.OldValue!, null, System.Globalization.DateTimeStyles.RoundtripKind));
// The status transition itself is still recorded separately.
Assert.Contains(await db.JobEvents.ToListAsync(), e => e.Type == "StatusChanged" && e.NewValue == "Saved");
}
[Fact]
public async Task Re_applying_after_a_backwards_move_stamps_a_fresh_date_and_leaves_history_intact()
{
await using var db = CreateDb();
var job = await SeedAppliedJob(db);
var controller = CreateController(db, "user-1");
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Saved"), CancellationToken.None);
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Applied"), CancellationToken.None);
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
Assert.Equal("Applied", saved.Status);
Assert.NotNull(saved.DateApplied);
Assert.NotEqual(AppliedOn, saved.DateApplied);
// The first application's date survives the round trip in history.
var cleared = await db.JobEvents.SingleAsync(e => e.Type == JobPipeline.AppliedDateClearedEvent);
Assert.Equal(AppliedOn, DateTime.Parse(cleared.OldValue!, null, System.Globalization.DateTimeStyles.RoundtripKind));
}
[Fact]
public async Task Moving_between_post_application_stages_records_no_cleared_date_event()
{
await using var db = CreateDb();
var job = await SeedAppliedJob(db);
var controller = CreateController(db, "user-1");
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Interview"), CancellationToken.None);
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
Assert.Equal(AppliedOn, saved.DateApplied);
Assert.DoesNotContain(await db.JobEvents.ToListAsync(), e => e.Type == JobPipeline.AppliedDateClearedEvent);
}
[Fact]
public async Task Withdrawing_keeps_the_applied_date()
{
await using var db = CreateDb();
var job = await SeedAppliedJob(db);
var controller = CreateController(db, "user-1");
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Withdrawn"), CancellationToken.None);
// Withdrawn is a closed stage, not a pre-application one: you did apply, then pulled out.
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
Assert.Equal("Withdrawn", saved.Status);
Assert.Equal(AppliedOn, saved.DateApplied);
}
private static async Task SeedAppliedJob(JobTrackerContext db)
{
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 = AppliedOn,
SavedAt = AppliedOn.AddDays(-2),
};
db.JobApplications.Add(job);
await db.SaveChangesAsync();
return job;
}
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
{
var controller = new JobApplicationsController(db, Mock.Of(), Mock.Of(), TestHostFactory.CreateUserManager().Object, NullLogger.Instance);
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() => TestHostFactory.CreateInMemoryDb();
}