using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Feedser.Storage.Mongo.Exporting;

namespace StellaOps.Feedser.Storage.Mongo.Tests;

public sealed class ExportStateManagerTests
{
    [Fact]
    public async Task StoreFullExportInitializesBaseline()
    {
        var store = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-07-20T12:00:00Z"));
        var manager = new ExportStateManager(store, timeProvider);

        var record = await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240720T120000Z",
            exportDigest: "sha256:abcd",
            cursor: "cursor-1",
            targetRepository: "registry.local/json",
            exporterVersion: "1.0.0",
            resetBaseline: true,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        Assert.Equal("export:json", record.Id);
        Assert.Equal("20240720T120000Z", record.BaseExportId);
        Assert.Equal("sha256:abcd", record.BaseDigest);
        Assert.Equal("sha256:abcd", record.LastFullDigest);
        Assert.Null(record.LastDeltaDigest);
        Assert.Equal("cursor-1", record.ExportCursor);
        Assert.Equal("registry.local/json", record.TargetRepository);
        Assert.Equal("1.0.0", record.ExporterVersion);
        Assert.Equal(timeProvider.Now, record.UpdatedAt);
    }

    [Fact]
    public async Task StoreFullExport_ResetBaselineOverridesExisting()
    {
        var store = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-07-20T12:00:00Z"));
        var manager = new ExportStateManager(store, timeProvider);

        await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240720T120000Z",
            exportDigest: "sha256:base",
            cursor: "cursor-base",
            targetRepository: null,
            exporterVersion: "1.0.0",
            resetBaseline: true,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        timeProvider.Advance(TimeSpan.FromMinutes(5));
        var withoutReset = await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240720T120500Z",
            exportDigest: "sha256:new",
            cursor: "cursor-new",
            targetRepository: null,
            exporterVersion: "1.0.1",
            resetBaseline: false,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        Assert.Equal("20240720T120000Z", withoutReset.BaseExportId);
        Assert.Equal("sha256:base", withoutReset.BaseDigest);
        Assert.Equal("sha256:new", withoutReset.LastFullDigest);
        Assert.Equal("cursor-new", withoutReset.ExportCursor);
        Assert.Equal(timeProvider.Now, withoutReset.UpdatedAt);

        timeProvider.Advance(TimeSpan.FromMinutes(5));
        var reset = await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240720T121000Z",
            exportDigest: "sha256:final",
            cursor: "cursor-final",
            targetRepository: null,
            exporterVersion: "1.0.2",
            resetBaseline: true,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        Assert.Equal("20240720T121000Z", reset.BaseExportId);
        Assert.Equal("sha256:final", reset.BaseDigest);
        Assert.Equal("sha256:final", reset.LastFullDigest);
        Assert.Null(reset.LastDeltaDigest);
        Assert.Equal("cursor-final", reset.ExportCursor);
        Assert.Equal(timeProvider.Now, reset.UpdatedAt);
    }

    [Fact]
    public async Task StoreFullExport_ResetsBaselineWhenRepositoryChanges()
    {
        var store = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-07-21T08:00:00Z"));
        var manager = new ExportStateManager(store, timeProvider);

        await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240721T080000Z",
            exportDigest: "sha256:base",
            cursor: "cursor-base",
            targetRepository: "registry/v1/json",
            exporterVersion: "1.0.0",
            resetBaseline: true,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        timeProvider.Advance(TimeSpan.FromMinutes(10));
        var updated = await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240721T081000Z",
            exportDigest: "sha256:new",
            cursor: "cursor-new",
            targetRepository: "registry/v2/json",
            exporterVersion: "1.1.0",
            resetBaseline: false,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        Assert.Equal("20240721T081000Z", updated.BaseExportId);
        Assert.Equal("sha256:new", updated.BaseDigest);
        Assert.Equal("sha256:new", updated.LastFullDigest);
        Assert.Equal("registry/v2/json", updated.TargetRepository);
    }

    [Fact]
    public async Task StoreDeltaExportRequiresBaseline()
    {
        var store = new InMemoryExportStateStore();
        var manager = new ExportStateManager(store);

        await Assert.ThrowsAsync<InvalidOperationException>(() => manager.StoreDeltaExportAsync(
            exporterId: "export:json",
            deltaDigest: "sha256:def",
            cursor: null,
            exporterVersion: "1.0.1",
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None));
    }

    [Fact]
    public async Task StoreDeltaExportUpdatesExistingState()
    {
        var store = new InMemoryExportStateStore();
        var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2024-07-20T12:00:00Z"));
        var manager = new ExportStateManager(store, timeProvider);

        await manager.StoreFullExportAsync(
            exporterId: "export:json",
            exportId: "20240720T120000Z",
            exportDigest: "sha256:abcd",
            cursor: "cursor-1",
            targetRepository: null,
            exporterVersion: "1.0.0",
            resetBaseline: true,
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        timeProvider.Advance(TimeSpan.FromMinutes(10));
        var delta = await manager.StoreDeltaExportAsync(
            exporterId: "export:json",
            deltaDigest: "sha256:ef01",
            cursor: "cursor-2",
            exporterVersion: "1.0.1",
            manifest: Array.Empty<ExportFileRecord>(),
            cancellationToken: CancellationToken.None);

        Assert.Equal("sha256:ef01", delta.LastDeltaDigest);
        Assert.Equal("cursor-2", delta.ExportCursor);
        Assert.Equal("1.0.1", delta.ExporterVersion);
        Assert.Equal(timeProvider.Now, delta.UpdatedAt);
        Assert.Equal("sha256:abcd", delta.LastFullDigest);
    }

    private sealed class InMemoryExportStateStore : IExportStateStore
    {
        private readonly Dictionary<string, ExportStateRecord> _records = new(StringComparer.Ordinal);

        public Task<ExportStateRecord?> FindAsync(string id, CancellationToken cancellationToken)
        {
            _records.TryGetValue(id, out var record);
            return Task.FromResult<ExportStateRecord?>(record);
        }

        public Task<ExportStateRecord> UpsertAsync(ExportStateRecord record, CancellationToken cancellationToken)
        {
            _records[record.Id] = record;
            return Task.FromResult(record);
        }
    }

    private sealed class TestTimeProvider : TimeProvider
    {
        public TestTimeProvider(DateTimeOffset start) => Now = start;

        public DateTimeOffset Now { get; private set; }

        public void Advance(TimeSpan delta) => Now = Now.Add(delta);

        public override DateTimeOffset GetUtcNow() => Now;
    }
}
