using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Feedser.Core.Jobs;

namespace StellaOps.Feedser.Core.Tests;

public sealed class JobCoordinatorTests
{
    [Fact]
    public async Task TriggerAsync_RunCompletesSuccessfully()
    {
        var services = new ServiceCollection();
        services.AddTransient<SuccessfulJob>();
        services.AddLogging();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new InMemoryLeaseStore();
        var jobOptions = new JobSchedulerOptions
        {
            DefaultLeaseDuration = TimeSpan.FromSeconds(5),
            DefaultTimeout = TimeSpan.FromSeconds(10),
        };

        var definition = new JobDefinition(
            Kind: "test:run",
            JobType: typeof(SuccessfulJob),
            Timeout: TimeSpan.FromSeconds(5),
            LeaseDuration: TimeSpan.FromSeconds(2),
            CronExpression: null,
            Enabled: true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var result = await coordinator.TriggerAsync(definition.Kind, new Dictionary<string, object?> { ["foo"] = "bar" }, "unit-test", CancellationToken.None);

        Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
        var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Assert.Equal(JobRunStatus.Succeeded, completed.Status);
        await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
        Assert.True(leaseStore.ReleaseCount > 0);
        Assert.Equal("bar", completed.Parameters["foo"]);
    }

    [Fact]
    public async Task TriggerAsync_MarksRunFailed_WhenLeaseReleaseFails()
    {
        var services = new ServiceCollection();
        services.AddTransient<SuccessfulJob>();
        services.AddLogging();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new FailingLeaseStore
        {
            ThrowOnRelease = true,
        };

        var jobOptions = new JobSchedulerOptions
        {
            DefaultLeaseDuration = TimeSpan.FromSeconds(5),
            DefaultTimeout = TimeSpan.FromSeconds(10),
        };

        var definition = new JobDefinition(
            Kind: "test:run",
            JobType: typeof(SuccessfulJob),
            Timeout: TimeSpan.FromSeconds(5),
            LeaseDuration: TimeSpan.FromSeconds(2),
            CronExpression: null,
            Enabled: true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);

        Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
        var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Assert.Equal(JobRunStatus.Failed, completed.Status);
        Assert.NotNull(completed.Error);
        Assert.Contains("Failed to release lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
        Assert.True(leaseStore.ReleaseAttempts > 0);
    }

    [Fact]
    public async Task TriggerAsync_MarksRunFailed_WhenLeaseHeartbeatFails()
    {
        var services = new ServiceCollection();
        services.AddTransient<SlowJob>();
        services.AddLogging();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new FailingLeaseStore
        {
            ThrowOnHeartbeat = true,
        };

        var jobOptions = new JobSchedulerOptions
        {
            DefaultLeaseDuration = TimeSpan.FromSeconds(2),
            DefaultTimeout = TimeSpan.FromSeconds(10),
        };

        var definition = new JobDefinition(
            Kind: "test:heartbeat",
            JobType: typeof(SlowJob),
            Timeout: TimeSpan.FromSeconds(5),
            LeaseDuration: TimeSpan.FromSeconds(2),
            CronExpression: null,
            Enabled: true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);

        Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);
        var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(6));
        Assert.Equal(JobRunStatus.Failed, completed.Status);
        Assert.NotNull(completed.Error);
        Assert.Contains("Failed to heartbeat lease", completed.Error!, StringComparison.OrdinalIgnoreCase);
        Assert.True(leaseStore.HeartbeatCount > 0);
    }

    [Fact]
    public async Task TriggerAsync_ReturnsAlreadyRunning_WhenLeaseUnavailable()
    {
        var services = new ServiceCollection();
        services.AddTransient<SuccessfulJob>();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new InMemoryLeaseStore
        {
            NextLease = null,
        };
        var jobOptions = new JobSchedulerOptions();
        var definition = new JobDefinition(
            "test:run",
            typeof(SuccessfulJob),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(2),
            null,
            true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);

        Assert.Equal(JobTriggerOutcome.AlreadyRunning, result.Outcome);
        Assert.False(jobStore.CreatedRuns.Any());
    }

    [Fact]
    public async Task TriggerAsync_ReturnsInvalidParameters_ForUnsupportedPayload()
    {
        var services = new ServiceCollection();
        services.AddTransient<SuccessfulJob>();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new InMemoryLeaseStore();
        var jobOptions = new JobSchedulerOptions();
        var definition = new JobDefinition(
            "test:run",
            typeof(SuccessfulJob),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(2),
            null,
            true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var parameters = new Dictionary<string, object?>
        {
            ["bad"] = new object(),
        };

        var result = await coordinator.TriggerAsync(definition.Kind, parameters, "unit-test", CancellationToken.None);

        Assert.Equal(JobTriggerOutcome.InvalidParameters, result.Outcome);
        Assert.Contains("unsupported", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
        Assert.False(jobStore.CreatedRuns.Any());
    }

    [Fact]
    public async Task TriggerAsync_CancelsJobOnTimeout()
    {
        var services = new ServiceCollection();
        services.AddTransient<TimeoutJob>();
        using var provider = services.BuildServiceProvider();

        var jobStore = new InMemoryJobStore();
        var leaseStore = new InMemoryLeaseStore();
        var jobOptions = new JobSchedulerOptions
        {
            DefaultLeaseDuration = TimeSpan.FromSeconds(5),
            DefaultTimeout = TimeSpan.FromMilliseconds(100),
        };

        var definition = new JobDefinition(
            Kind: "test:timeout",
            JobType: typeof(TimeoutJob),
            Timeout: TimeSpan.FromMilliseconds(100),
            LeaseDuration: TimeSpan.FromSeconds(2),
            CronExpression: null,
            Enabled: true);
        jobOptions.Definitions.Add(definition.Kind, definition);

        using var diagnostics = new JobDiagnostics();
        var coordinator = new JobCoordinator(
            Options.Create(jobOptions),
            jobStore,
            leaseStore,
            provider.GetRequiredService<IServiceScopeFactory>(),
            NullLogger<JobCoordinator>.Instance,
            NullLoggerFactory.Instance,
            new TestTimeProvider(),
            diagnostics);

        var result = await coordinator.TriggerAsync(definition.Kind, null, "unit-test", CancellationToken.None);
        Assert.Equal(JobTriggerOutcome.Accepted, result.Outcome);

        var completed = await jobStore.Completion.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Assert.Equal(JobRunStatus.Cancelled, completed.Status);
        await leaseStore.WaitForReleaseAsync(TimeSpan.FromSeconds(1));
        Assert.True(leaseStore.ReleaseCount > 0);
    }

    private sealed class SuccessfulJob : IJob
    {
        public Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }

    private sealed class TimeoutJob : IJob
    {
        public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
        {
            await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
        }
    }

    private sealed class SlowJob : IJob
    {
        public async Task ExecuteAsync(JobExecutionContext context, CancellationToken cancellationToken)
        {
            await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
        }
    }

    private sealed class InMemoryJobStore : IJobStore
    {
        private readonly Dictionary<Guid, JobRunSnapshot> _runs = new();
        public TaskCompletionSource<JobRunSnapshot> Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public List<JobRunSnapshot> CreatedRuns { get; } = new();

        public Task<JobRunSnapshot> CreateAsync(JobRunCreateRequest request, CancellationToken cancellationToken)
        {
            var run = new JobRunSnapshot(
                Guid.NewGuid(),
                request.Kind,
                JobRunStatus.Pending,
                request.CreatedAt,
                null,
                null,
                request.Trigger,
                request.ParametersHash,
                null,
                request.Timeout,
                request.LeaseDuration,
                request.Parameters);
            _runs[run.RunId] = run;
            CreatedRuns.Add(run);
            return Task.FromResult(run);
        }

        public Task<JobRunSnapshot?> TryStartAsync(Guid runId, DateTimeOffset startedAt, CancellationToken cancellationToken)
        {
            if (_runs.TryGetValue(runId, out var run))
            {
                var updated = run with { Status = JobRunStatus.Running, StartedAt = startedAt };
                _runs[runId] = updated;
                return Task.FromResult<JobRunSnapshot?>(updated);
            }

            return Task.FromResult<JobRunSnapshot?>(null);
        }

        public Task<JobRunSnapshot?> TryCompleteAsync(Guid runId, JobRunCompletion completion, CancellationToken cancellationToken)
        {
            if (_runs.TryGetValue(runId, out var run))
            {
                var updated = run with { Status = completion.Status, CompletedAt = completion.CompletedAt, Error = completion.Error };
                _runs[runId] = updated;
                Completion.TrySetResult(updated);
                return Task.FromResult<JobRunSnapshot?>(updated);
            }

            return Task.FromResult<JobRunSnapshot?>(null);
        }

        public Task<JobRunSnapshot?> FindAsync(Guid runId, CancellationToken cancellationToken)
        {
            _runs.TryGetValue(runId, out var run);
            return Task.FromResult<JobRunSnapshot?>(run);
        }

        public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken)
        {
            var query = _runs.Values.AsEnumerable();
            if (!string.IsNullOrWhiteSpace(kind))
            {
                query = query.Where(r => r.Kind == kind);
            }

            return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(query.OrderByDescending(r => r.CreatedAt).Take(limit).ToArray());
        }

        public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult<IReadOnlyList<JobRunSnapshot>>(_runs.Values.Where(r => r.Status is JobRunStatus.Pending or JobRunStatus.Running).ToArray());
        }

        public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken)
        {
            var run = _runs.Values
                .Where(r => r.Kind == kind)
                .OrderByDescending(r => r.CreatedAt)
                .FirstOrDefault();
            return Task.FromResult<JobRunSnapshot?>(run);
        }

        public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken)
        {
            var results = new Dictionary<string, JobRunSnapshot>(StringComparer.Ordinal);
            foreach (var kind in kinds.Distinct(StringComparer.Ordinal))
            {
                var run = _runs.Values
                    .Where(r => r.Kind == kind)
                    .OrderByDescending(r => r.CreatedAt)
                    .FirstOrDefault();
                if (run is not null)
                {
                    results[kind] = run;
                }
            }

            return Task.FromResult<IReadOnlyDictionary<string, JobRunSnapshot>>(results);
        }
    }

    private sealed class InMemoryLeaseStore : ILeaseStore
    {
        public JobLease? NextLease { get; set; } = new JobLease("job:test:run", "holder", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, TimeSpan.FromSeconds(2), DateTimeOffset.UtcNow.AddSeconds(2));
        public int HeartbeatCount { get; private set; }
        public int ReleaseCount { get; private set; }
        private readonly TaskCompletionSource<bool> _released = new(TaskCreationOptions.RunContinuationsAsynchronously);

        public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
        {
            return Task.FromResult(NextLease);
        }

        public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
        {
            HeartbeatCount++;
            NextLease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
            return Task.FromResult<JobLease?>(NextLease);
        }

        public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
        {
            ReleaseCount++;
            _released.TrySetResult(true);
            return Task.FromResult(true);
        }

        public Task WaitForReleaseAsync(TimeSpan timeout)
            => _released.Task.WaitAsync(timeout);
    }

    private sealed class FailingLeaseStore : ILeaseStore
    {
        public bool ThrowOnHeartbeat { get; set; }
        public bool ThrowOnRelease { get; set; }

        public int HeartbeatCount { get; private set; }
        public int ReleaseAttempts { get; private set; }

        public Task<JobLease?> TryAcquireAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
        {
            var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
            return Task.FromResult<JobLease?>(lease);
        }

        public Task<JobLease?> HeartbeatAsync(string key, string holder, TimeSpan leaseDuration, DateTimeOffset now, CancellationToken cancellationToken)
        {
            HeartbeatCount++;
            if (ThrowOnHeartbeat)
            {
                throw new InvalidOperationException("Lease heartbeat failed");
            }

            var lease = new JobLease(key, holder, now, now, leaseDuration, now.Add(leaseDuration));
            return Task.FromResult<JobLease?>(lease);
        }

        public Task<bool> ReleaseAsync(string key, string holder, CancellationToken cancellationToken)
        {
            ReleaseAttempts++;
            if (ThrowOnRelease)
            {
                throw new InvalidOperationException("Failed to release lease");
            }

            return Task.FromResult(true);
        }
    }

    private sealed class TestTimeProvider : TimeProvider
    {
        private DateTimeOffset _now = DateTimeOffset.Parse("2024-01-01T00:00:00Z");

        public override DateTimeOffset GetUtcNow() => _now = _now.AddMilliseconds(100);
    }
}
