using System.Collections.Concurrent;
using System.Text.Json;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class BackgroundWorkerTenantTests
{
    private static readonly DateTimeOffset FixedNow = new(2026, 8, 15, 12, 0, 0, TimeSpan.Zero);

    [Fact]
    public async Task Runner_enters_each_owner_filter_and_isolates_owner_failures()
    {
        await using var fixture = await Fixture.CreateAsync();
        await fixture.SeedJobsAsync();
        var seen = new ConcurrentDictionary<string, string[]>();

        var result = await fixture.Runner.RunForJobOwnersAsync("test", async (services, cancellationToken) =>
        {
            var db = services.GetRequiredService<JobTrackerContext>();
            var owner = Assert.IsType<string>(db.CurrentUserId);
            seen[owner] = await db.JobApplications.Select(job => job.OwnerUserId!).Distinct().ToArrayAsync(cancellationToken);
            if (owner == "user-1") throw new InvalidOperationException("synthetic owner failure");
        }, default);

        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 1, 1), result);
        Assert.Equal(new[] { "user-1" }, seen["user-1"]);
        Assert.Equal(new[] { "user-2" }, seen["user-2"]);
    }

    [Fact]
    public void Background_owner_cannot_replace_an_http_identity_context()
    {
        var accessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() };
        var currentUser = new CurrentUserService(accessor);
        Assert.Throws<InvalidOperationException>(() => currentUser.UseBackgroundUser("user-1"));
    }

    [Fact]
    public async Task Rules_worker_uses_each_owners_settings_and_is_idempotent()
    {
        await using var fixture = await Fixture.CreateAsync(new Dictionary<string, string?> { ["Workers:RulesEnabled"] = "true" });
        await fixture.SeedJobsAsync();
        await using (var scope = fixture.Provider.CreateAsyncScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
            db.UserRuleSettings.AddRange(
                new UserRuleSettings { OwnerUserId = "user-1", AppliedFollowUpDays = 1, AppliedGhostDays = 5 },
                new UserRuleSettings { OwnerUserId = "user-2", AppliedFollowUpDays = 40, AppliedGhostDays = 60 });
            await db.SaveChangesAsync();
        }

        var worker = new RulesHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<RulesHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            TimeProvider.System);

        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));

        await using var verificationScope = fixture.Provider.CreateAsyncScope();
        var jobs = await verificationScope.ServiceProvider.GetRequiredService<JobTrackerContext>()
            .JobApplications.IgnoreQueryFilters().AsNoTracking().OrderBy(job => job.OwnerUserId).ToListAsync();
        Assert.Equal("Ghosted", jobs[0].Status);
        Assert.Equal("Applied", jobs[1].Status);
    }

    [Fact]
    public async Task Affected_workers_are_disabled_by_default()
    {
        var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build();
        var runner = new BackgroundTenantRunner(Mock.Of<IServiceScopeFactory>(), NullLogger<BackgroundTenantRunner>.Instance);
        var readiness = Mock.Of<IStartupReadiness>();
        var root = Path.Combine(Path.GetTempPath(), $"jobtracker-worker-defaults-{Guid.NewGuid():N}");
        var environment = new Mock<IHostEnvironment>();
        environment.SetupGet(x => x.ContentRootPath).Returns(root);
        try
        {
            var rules = new RulesHostedService(runner, configuration, NullLogger<RulesHostedService>.Instance, readiness, TimeProvider.System);
            var reminders = new FollowUpReminderHostedService(runner, configuration, NullLogger<FollowUpReminderHostedService>.Instance, readiness, ExternalOrigin.Parse(null, false), TimeProvider.System);
            var exports = new DailyExportHostedService(runner, NullLogger<DailyExportHostedService>.Instance, configuration, new AppPaths(configuration, environment.Object), readiness, TimeProvider.System);
            var enrichment = new JobEnrichmentHostedService(runner, configuration, NullLogger<JobEnrichmentHostedService>.Instance, readiness, TimeProvider.System);

            Assert.Equal(BackgroundWorkerRunResult.Disabled, await rules.RunOnceAsync(default));
            Assert.Equal(BackgroundWorkerRunResult.Disabled, await reminders.RunOnceAsync(default));
            Assert.Equal(BackgroundWorkerRunResult.Disabled, await exports.RunOnceAsync(default));
            Assert.Equal(BackgroundWorkerRunResult.Disabled, await enrichment.RunOnceAsync(default));
        }
        finally
        {
            if (Directory.Exists(root)) Directory.Delete(root, true);
        }
    }

    [Fact]
    public async Task Daily_export_writes_one_isolated_atomic_file_per_owner()
    {
        var root = Path.Combine(Path.GetTempPath(), $"jobtracker-worker-export-{Guid.NewGuid():N}");
        await using var fixture = await Fixture.CreateAsync(new Dictionary<string, string?>
        {
            ["Workers:DailyExportEnabled"] = "true",
            ["Exports:DailyEnabled"] = "true",
            ["Data:Root"] = root,
        });
        await fixture.SeedJobsAsync();
        var clock = new MutableTimeProvider(FixedNow);
        await using (var seedScope = fixture.Provider.CreateAsyncScope())
        {
            var db = seedScope.ServiceProvider.GetRequiredService<JobTrackerContext>();
            var jobs = await db.JobApplications.IgnoreQueryFilters().OrderBy(job => job.OwnerUserId).ToListAsync();
            db.EmailSendAttempts.AddRange(
                new EmailSendAttempt { Id = Guid.NewGuid(), OwnerUserId = "user-1", JobApplicationId = jobs[0].Id, Provider = "gmail", ClientRequestId = Guid.NewGuid().ToString(), PayloadHash = new string('a', 64), Status = EmailSendStatuses.Sent, CreatedAtUtc = DateTime.UtcNow },
                new EmailSendAttempt { Id = Guid.NewGuid(), OwnerUserId = "user-2", JobApplicationId = jobs[1].Id, Provider = "microsoft", ClientRequestId = Guid.NewGuid().ToString(), PayloadHash = new string('b', 64), Status = EmailSendStatuses.Failed, FailureCategory = "rejected", CreatedAtUtc = DateTime.UtcNow });
            db.EmailDrafts.AddRange(
                Draft("user-1", jobs[0].Id, "one@example.test", "Private draft one"),
                Draft("user-2", jobs[1].Id, "two@example.test", "Private draft two"));
            await db.SaveChangesAsync();
        }
        var environment = new Mock<IHostEnvironment>();
        environment.SetupGet(x => x.ContentRootPath).Returns(root);
        try
        {
            var worker = new DailyExportHostedService(
                fixture.Runner,
                NullLogger<DailyExportHostedService>.Instance,
                fixture.Configuration,
                new AppPaths(fixture.Configuration, environment.Object),
                Mock.Of<IStartupReadiness>(),
                clock);

            Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));
            var restartedWorker = new DailyExportHostedService(
                fixture.Runner,
                NullLogger<DailyExportHostedService>.Instance,
                fixture.Configuration,
                new AppPaths(fixture.Configuration, environment.Object),
                Mock.Of<IStartupReadiness>(),
                clock);
            Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await restartedWorker.RunOnceAsync(default));
            var files = Directory.GetFiles(Path.Combine(root, "exports"), "*.json", SearchOption.AllDirectories);
            Assert.Equal(2, files.Length);
            Assert.DoesNotContain(files, path => path.Contains("user-1", StringComparison.Ordinal) || path.Contains("user-2", StringComparison.Ordinal));
            Assert.Equal(2, files.Select(path => Directory.GetParent(path)!.Name).Distinct(StringComparer.Ordinal).Count());
            Assert.All(files, path => Assert.Matches("^[0-9a-f]{64}$", Directory.GetParent(path)!.Name));
            var owners = new List<string?>();
            foreach (var path in files)
            {
                using var document = JsonDocument.Parse(System.IO.File.ReadAllText(path));
                var rootElement = document.RootElement;
                Assert.Equal(FixedNow.DateTime, rootElement.GetProperty("CreatedAt").GetDateTime());
                var owner = rootElement.GetProperty("OwnerUserId").GetString();
                owners.Add(owner);
                var attempt = Assert.Single(rootElement.GetProperty("EmailSendAttempts").EnumerateArray());
                Assert.Equal(owner == "user-1" ? "gmail" : "microsoft", attempt.GetProperty("Provider").GetString());
                Assert.False(attempt.TryGetProperty("PayloadHash", out _));
                var draft = Assert.Single(rootElement.GetProperty("EmailDrafts").EnumerateArray());
                Assert.Equal(owner == "user-1" ? "one@example.test" : "two@example.test", draft.GetProperty("To").GetString());
                Assert.Equal(owner == "user-1" ? "Private draft one" : "Private draft two", draft.GetProperty("BodyText").GetString());
                Assert.True(Guid.TryParse(draft.GetProperty("ClientRequestId").GetString(), out _));
                Assert.Equal(1, draft.GetProperty("Revision").GetInt64());
            }
            owners.Sort(StringComparer.Ordinal);
            Assert.Equal(new[] { "user-1", "user-2" }, owners);
            Assert.Empty(Directory.GetFiles(Path.Combine(root, "exports"), "*.tmp", SearchOption.AllDirectories));
        }
        finally
        {
            if (Directory.Exists(root)) Directory.Delete(root, true);
        }
    }

    private static EmailDraft Draft(string ownerUserId, int jobApplicationId, string recipient, string body) => new()
    {
        Id = Guid.NewGuid(),
        OwnerUserId = ownerUserId,
        JobApplicationId = jobApplicationId,
        Provider = ownerUserId == "user-1" ? "gmail" : "microsoft",
        To = recipient,
        Subject = "Synthetic export subject",
        BodyText = body,
        ThreadId = "synthetic-thread",
        ClientRequestId = Guid.NewGuid().ToString("D"),
        CreatedAtUtc = DateTime.UtcNow.AddMinutes(-5),
        UpdatedAtUtc = DateTime.UtcNow,
    };

    [Fact]
    public async Task Enrichment_processes_both_owners_only_through_fake_ai()
    {
        var summarizer = new Mock<ISummarizerService>();
        summarizer.Setup(x => x.SummarizeAsync(It.IsAny<string>(), 160, 60))
            .ReturnsAsync((string text, int _, int _) => $"summary:{text}");
        await using var fixture = await Fixture.CreateAsync(
            new Dictionary<string, string?> { ["Workers:JobEnrichmentEnabled"] = "true" },
            services => services.AddSingleton(summarizer.Object));
        await fixture.SeedJobsAsync(includeUsers: true);
        await using (var roleScope = fixture.Provider.CreateAsyncScope())
        {
            var roles = roleScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
            var users = roleScope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            Assert.True((await roles.CreateAsync(new IdentityRole("Premium"))).Succeeded);
            foreach (var id in new[] { "user-1", "user-2" })
                Assert.True((await users.AddToRoleAsync((await users.FindByIdAsync(id))!, "Premium")).Succeeded);
        }
        var worker = new JobEnrichmentHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<JobEnrichmentHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            TimeProvider.System);

        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));

        await using var scope = fixture.Provider.CreateAsyncScope();
        var jobs = await scope.ServiceProvider.GetRequiredService<JobTrackerContext>().JobApplications.IgnoreQueryFilters().AsNoTracking().ToListAsync();
        Assert.Contains(jobs, job => job.OwnerUserId == "user-1" && job.ShortSummary == "summary:description-user-1");
        Assert.Contains(jobs, job => job.OwnerUserId == "user-2" && job.ShortSummary == "summary:description-user-2");
        summarizer.Verify(x => x.SummarizeAsync(It.IsAny<string>(), 160, 60), Times.Exactly(2));
    }

    [Fact]
    public async Task Enrichment_does_not_call_ai_for_free_owners()
    {
        var summarizer = new Mock<ISummarizerService>();
        await using var fixture = await Fixture.CreateAsync(
            new Dictionary<string, string?> { ["Workers:JobEnrichmentEnabled"] = "true" },
            services => services.AddSingleton(summarizer.Object));
        await fixture.SeedJobsAsync(includeUsers: true);
        var worker = new JobEnrichmentHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<JobEnrichmentHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            TimeProvider.System);

        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));
        summarizer.Verify(x => x.SummarizeAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never);
    }

    [Fact]
    public async Task Reminder_worker_sends_only_to_each_confirmed_owner_through_fake_email()
    {
        var email = new Mock<IAppEmailSender>();
        await using var fixture = await Fixture.CreateAsync(
            new Dictionary<string, string?>
            {
                ["Workers:FollowUpRemindersEnabled"] = "true",
                ["Email:FollowUpReminders:Enabled"] = "true",
                ["App:PublicBaseUrl"] = "http://localhost:3000",
            },
            services => services.AddSingleton(email.Object));
        await fixture.SeedJobsAsync(includeUsers: true);
        var clock = new MutableTimeProvider(FixedNow);
        var worker = new FollowUpReminderHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<FollowUpReminderHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            ExternalOrigin.FromConfiguration(fixture.Configuration),
            clock);

        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));
        var restartedWorker = new FollowUpReminderHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<FollowUpReminderHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            ExternalOrigin.FromConfiguration(fixture.Configuration),
            clock);
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await restartedWorker.RunOnceAsync(default));
        email.Verify(x => x.SendAsync("one@example.test", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
        email.Verify(x => x.SendAsync("two@example.test", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);

        await using var scope = fixture.Provider.CreateAsyncScope();
        var jobs = await scope.ServiceProvider.GetRequiredService<JobTrackerContext>().JobApplications.IgnoreQueryFilters().AsNoTracking().ToListAsync();
        Assert.All(jobs, job => Assert.Equal(FixedNow.DateTime, job.LastReminderEmailSentAt));
    }

    [Fact]
    public async Task Reminder_worker_respects_each_owners_persisted_email_opt_out()
    {
        var email = new Mock<IAppEmailSender>();
        await using var fixture = await Fixture.CreateAsync(
            new Dictionary<string, string?>
            {
                ["Workers:FollowUpRemindersEnabled"] = "true",
                ["Email:FollowUpReminders:Enabled"] = "true",
                ["App:PublicBaseUrl"] = "http://localhost:3000",
            },
            services => services.AddSingleton(email.Object));
        await fixture.SeedJobsAsync(includeUsers: true);
        await using (var scope = fixture.Provider.CreateAsyncScope())
        {
            var user = await scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>().FindByIdAsync("user-2");
            Assert.NotNull(user);
            user!.EmailFollowUpRemindersEnabled = false;
            Assert.True((await scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>().UpdateAsync(user)).Succeeded);
        }

        var worker = new FollowUpReminderHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<FollowUpReminderHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            ExternalOrigin.FromConfiguration(fixture.Configuration),
            new MutableTimeProvider(FixedNow));
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await worker.RunOnceAsync(default));

        email.Verify(x => x.SendAsync("one@example.test", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
        email.Verify(x => x.SendAsync("two@example.test", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
        await using var verificationScope = fixture.Provider.CreateAsyncScope();
        var jobs = await verificationScope.ServiceProvider.GetRequiredService<JobTrackerContext>().JobApplications
            .IgnoreQueryFilters().AsNoTracking().OrderBy(job => job.OwnerUserId).ToListAsync();
        Assert.Equal(FixedNow.DateTime, jobs[0].LastReminderEmailSentAt);
        Assert.Null(jobs[1].LastReminderEmailSentAt);
    }

    [Fact]
    public async Task Rules_worker_uses_injected_clock_across_threshold_and_restart()
    {
        var clock = new MutableTimeProvider(FixedNow.AddMinutes(-1));
        await using var fixture = await Fixture.CreateAsync(new Dictionary<string, string?> { ["Workers:RulesEnabled"] = "true" });
        await fixture.SeedJobsAsync(appliedAt: FixedNow.DateTime.AddDays(-5));
        await using (var scope = fixture.Provider.CreateAsyncScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
            db.UserRuleSettings.AddRange(
                new UserRuleSettings { OwnerUserId = "user-1", AppliedFollowUpDays = 1, AppliedGhostDays = 5 },
                new UserRuleSettings { OwnerUserId = "user-2", AppliedFollowUpDays = 1, AppliedGhostDays = 5 });
            await db.SaveChangesAsync();
        }

        var beforeBoundary = new RulesHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<RulesHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            clock);
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await beforeBoundary.RunOnceAsync(default));

        await using (var scope = fixture.Provider.CreateAsyncScope())
        {
            var statuses = await scope.ServiceProvider.GetRequiredService<JobTrackerContext>()
                .JobApplications.IgnoreQueryFilters().Select(job => job.Status).ToListAsync();
            Assert.All(statuses, status => Assert.Equal("Applied", status));
        }

        clock.SetUtcNow(FixedNow);
        var afterRestart = new RulesHostedService(
            fixture.Runner,
            fixture.Configuration,
            NullLogger<RulesHostedService>.Instance,
            Mock.Of<IStartupReadiness>(),
            clock);
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await afterRestart.RunOnceAsync(default));
        Assert.Equal(new BackgroundWorkerRunResult(true, 2, 2, 0), await afterRestart.RunOnceAsync(default));

        await using var verificationScope = fixture.Provider.CreateAsyncScope();
        var finalStatuses = await verificationScope.ServiceProvider.GetRequiredService<JobTrackerContext>()
            .JobApplications.IgnoreQueryFilters().Select(job => job.Status).ToListAsync();
        Assert.All(finalStatuses, status => Assert.Equal("Ghosted", status));
    }

    private sealed class Fixture : IAsyncDisposable
    {
        private readonly SqliteConnection _connection;
        public ServiceProvider Provider { get; }
        public IConfiguration Configuration { get; }
        public BackgroundTenantRunner Runner => Provider.GetRequiredService<BackgroundTenantRunner>();

        private Fixture(SqliteConnection connection, ServiceProvider provider, IConfiguration configuration)
        {
            _connection = connection;
            Provider = provider;
            Configuration = configuration;
        }

        public static async Task<Fixture> CreateAsync(Dictionary<string, string?>? settings = null, Action<IServiceCollection>? configure = null)
        {
            var configuration = new ConfigurationBuilder().AddInMemoryCollection(settings ?? new()).Build();
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var services = new ServiceCollection();
            services.AddSingleton<IConfiguration>(configuration);
            services.AddLogging();
            services.AddHttpContextAccessor();
            services.AddScoped<CurrentUserService>();
            services.AddScoped<ICurrentUserService>(provider => provider.GetRequiredService<CurrentUserService>());
            services.AddDbContext<JobTrackerContext>((_, options) => options.UseSqlite(connection));
            services.AddIdentityCore<ApplicationUser>().AddRoles<IdentityRole>().AddEntityFrameworkStores<JobTrackerContext>();
            services.AddSingleton<BackgroundTenantRunner>();
            configure?.Invoke(services);
            var provider = services.BuildServiceProvider();
            await using var scope = provider.CreateAsyncScope();
            await scope.ServiceProvider.GetRequiredService<JobTrackerContext>().Database.EnsureCreatedAsync();
            return new Fixture(connection, provider, configuration);
        }

        public async Task SeedJobsAsync(bool includeUsers = false, DateTime? appliedAt = null)
        {
            await using var scope = Provider.CreateAsyncScope();
            var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
            if (includeUsers)
            {
                db.Users.AddRange(
                    new ApplicationUser { Id = "user-1", UserName = "one@example.test", NormalizedUserName = "ONE@EXAMPLE.TEST", Email = "one@example.test", NormalizedEmail = "ONE@EXAMPLE.TEST", EmailConfirmed = true },
                    new ApplicationUser { Id = "user-2", UserName = "two@example.test", NormalizedUserName = "TWO@EXAMPLE.TEST", Email = "two@example.test", NormalizedEmail = "TWO@EXAMPLE.TEST", EmailConfirmed = true });
            }
            var companies = new[]
            {
                new Company { OwnerUserId = "user-1", Name = "One" },
                new Company { OwnerUserId = "user-2", Name = "Two" },
            };
            db.Companies.AddRange(companies);
            await db.SaveChangesAsync();
            db.JobApplications.AddRange(
                new JobApplication { OwnerUserId = "user-1", CompanyId = companies[0].Id, JobTitle = "One", Status = "Applied", DateApplied = appliedAt ?? DateTime.Now.AddDays(-30), Description = "description-user-1" },
                new JobApplication { OwnerUserId = "user-2", CompanyId = companies[1].Id, JobTitle = "Two", Status = "Applied", DateApplied = appliedAt ?? DateTime.Now.AddDays(-30), Description = "description-user-2" });
            await db.SaveChangesAsync();
        }

        public async ValueTask DisposeAsync()
        {
            await Provider.DisposeAsync();
            await _connection.DisposeAsync();
        }
    }

    private sealed class MutableTimeProvider(DateTimeOffset utcNow) : TimeProvider
    {
        private DateTimeOffset _utcNow = utcNow;

        public override DateTimeOffset GetUtcNow() => _utcNow;

        public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;

        public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
    }
}
