using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; using TaskTracker.Api.Pages; using TaskTracker.Core.Entities; using TaskTracker.Core.Enums; using TaskTracker.Core.Interfaces; using Xunit; namespace TaskTracker.Tests; public class TaskDeadlineConversionTests { [Fact] public void FromLocalInput_uses_the_supplied_time_zone_offset_for_an_unspecified_value() { var timeZone = TimeZoneInfo.CreateCustomTimeZone("TestZone", TimeSpan.FromHours(-4), "Test", "Test"); var localValue = new DateTime(2026, 8, 14, 11, 30, 0, DateTimeKind.Utc); var dueAt = TaskDeadline.FromLocalInput(localValue, timeZone); Assert.Equal(new DateTimeOffset(2026, 8, 14, 11, 30, 0, TimeSpan.FromHours(-4)), dueAt); } [Fact] public void FromLocalInput_returns_null_for_a_blank_local_deadline() { var timeZone = TimeZoneInfo.CreateCustomTimeZone("TestZone", TimeSpan.FromHours(2), "Test", "Test"); var dueAt = TaskDeadline.FromLocalInput(null, timeZone); Assert.Null(dueAt); } [Fact] public async Task Board_create_assigns_due_at_from_local_deadline() { var repository = new InMemoryTaskRepository(); var board = CreateBoard(repository); var localDeadline = new DateTime(2026, 8, 14, 11, 30, 0); await board.OnPostCreateTaskAsync("deadline", null, localDeadline); var created = Assert.Single(repository.Created); var expected = TaskDeadline.FromLocalInput(localDeadline, TimeZoneInfo.Local); Assert.Equal(expected, created.DueAt); } [Fact] public async Task Board_update_explicit_clear_takes_precedence_over_a_supplied_deadline() { var existing = new WorkTask { Id = 1, Title = "existing", Status = WorkTaskStatus.Pending, DueAt = new DateTimeOffset(2026, 8, 14, 11, 30, 0, TimeSpan.FromHours(-4)), }; var repository = new InMemoryTaskRepository(existing); var board = CreateBoard(repository); var result = await board.OnPutUpdateTaskAsync(1, null, null, null, null, new DateTime(2026, 8, 15, 9, 0, 0), true); Assert.IsType(result); Assert.Null(existing.DueAt); } private static BoardModel CreateBoard(ITaskRepository repository) { var board = new BoardModel(repository); var services = new ServiceCollection() .AddLogging() .AddMvcCore() .AddViews() .Services .BuildServiceProvider(); var httpContext = new DefaultHttpContext { RequestServices = services }; var provider = services.GetRequiredService(); board.PageContext = new PageContext { HttpContext = httpContext, ViewData = new ViewDataDictionary(provider, new ModelStateDictionary()), }; return board; } private sealed class InMemoryTaskRepository(params WorkTask[] tasks) : ITaskRepository { private readonly List _tasks = [.. tasks]; public List Created { get; } = []; public Task> GetAllAsync(WorkTaskStatus? status = null, int? parentId = null, bool includeSubTasks = false) => Task.FromResult(_tasks); public Task GetByIdAsync(int id) => Task.FromResult(_tasks.SingleOrDefault(task => task.Id == id)); public Task GetActiveTaskAsync() => Task.FromResult(_tasks.SingleOrDefault(task => task.Status == WorkTaskStatus.Active)); public Task> GetSubTasksAsync(int parentId) => Task.FromResult(_tasks.Where(task => task.ParentTaskId == parentId).ToList()); public Task CreateAsync(WorkTask task) { Created.Add(task); _tasks.Add(task); return Task.FromResult(task); } public Task UpdateAsync(WorkTask task) => Task.CompletedTask; public Task DeleteAsync(int id) => Task.CompletedTask; } }