using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;

namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;

[Trait("Category", "Unit")]
public sealed class ResilientConfigReaderTests : IDisposable
{
    private readonly string _root = Path.Combine(Path.GetTempPath(), $"otopcua-reader-{Guid.NewGuid():N}");

    public void Dispose()
    {
        try
        {
            if (!Directory.Exists(_root)) return;
            foreach (var f in Directory.EnumerateFiles(_root, "*", SearchOption.AllDirectories))
                File.SetAttributes(f, FileAttributes.Normal);
            Directory.Delete(_root, recursive: true);
        }
        catch { /* best-effort */ }
    }

    [Fact]
    public async Task CentralDbSucceeds_ReturnsValue_MarksFresh()
    {
        var cache = new GenerationSealedCache(_root);
        var flag = new StaleConfigFlag { };
        flag.MarkStale(); // pre-existing stale state
        var reader = new ResilientConfigReader(cache, flag, NullLogger<ResilientConfigReader>.Instance);

        var result = await reader.ReadAsync(
            "cluster-a",
            _ => ValueTask.FromResult("fresh-from-db"),
            _ => "from-cache",
            CancellationToken.None);

        result.ShouldBe("fresh-from-db");
        flag.IsStale.ShouldBeFalse("successful central-DB read clears stale flag");
    }

    [Fact]
    public async Task CentralDbFails_ExhaustsRetries_FallsBackToCache_MarksStale()
    {
        var cache = new GenerationSealedCache(_root);
        await cache.SealAsync(new GenerationSnapshot
        {
            ClusterId = "cluster-a", GenerationId = 99, CachedAt = DateTime.UtcNow,
            PayloadJson = "{\"cached\":true}",
        });
        var flag = new StaleConfigFlag();
        var reader = new ResilientConfigReader(cache, flag, NullLogger<ResilientConfigReader>.Instance,
            timeout: TimeSpan.FromSeconds(10), retryCount: 2);
        var attempts = 0;

        var result = await reader.ReadAsync(
            "cluster-a",
            _ =>
            {
                attempts++;
                throw new InvalidOperationException("SQL dead");
#pragma warning disable CS0162
                return ValueTask.FromResult("never");
#pragma warning restore CS0162
            },
            snap => snap.PayloadJson,
            CancellationToken.None);

        attempts.ShouldBe(3, "1 initial + 2 retries = 3 attempts");
        result.ShouldBe("{\"cached\":true}");
        flag.IsStale.ShouldBeTrue("cache fallback flips stale flag true");
    }

    [Fact]
    public async Task CentralDbFails_AndCacheAlsoUnavailable_Throws()
    {
        var cache = new GenerationSealedCache(_root);
        var flag = new StaleConfigFlag();
        var reader = new ResilientConfigReader(cache, flag, NullLogger<ResilientConfigReader>.Instance,
            timeout: TimeSpan.FromSeconds(10), retryCount: 0);

        await Should.ThrowAsync<GenerationCacheUnavailableException>(async () =>
        {
            await reader.ReadAsync<string>(
                "cluster-a",
                _ => throw new InvalidOperationException("SQL dead"),
                _ => "never",
                CancellationToken.None);
        });

        flag.IsStale.ShouldBeFalse("no snapshot ever served, so flag stays whatever it was");
    }

    [Fact]
    public async Task Cancellation_NotRetried()
    {
        var cache = new GenerationSealedCache(_root);
        var flag = new StaleConfigFlag();
        var reader = new ResilientConfigReader(cache, flag, NullLogger<ResilientConfigReader>.Instance,
            timeout: TimeSpan.FromSeconds(10), retryCount: 5);
        using var cts = new CancellationTokenSource();
        cts.Cancel();
        var attempts = 0;

        await Should.ThrowAsync<OperationCanceledException>(async () =>
        {
            await reader.ReadAsync<string>(
                "cluster-a",
                ct =>
                {
                    attempts++;
                    ct.ThrowIfCancellationRequested();
                    return ValueTask.FromResult("ok");
                },
                _ => "cache",
                cts.Token);
        });

        attempts.ShouldBeLessThanOrEqualTo(1);
    }
}

[Trait("Category", "Unit")]
public sealed class StaleConfigFlagTests
{
    [Fact]
    public void Default_IsFresh()
    {
        new StaleConfigFlag().IsStale.ShouldBeFalse();
    }

    [Fact]
    public void MarkStale_ThenFresh_Toggles()
    {
        var flag = new StaleConfigFlag();
        flag.MarkStale();
        flag.IsStale.ShouldBeTrue();
        flag.MarkFresh();
        flag.IsStale.ShouldBeFalse();
    }

    [Fact]
    public void ConcurrentWrites_Converge()
    {
        var flag = new StaleConfigFlag();
        Parallel.For(0, 1000, i =>
        {
            if (i % 2 == 0) flag.MarkStale(); else flag.MarkFresh();
        });
        flag.MarkFresh();
        flag.IsStale.ShouldBeFalse();
    }
}
