using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Scripting;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Scripting;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;

/// <summary>
/// Arch-review R2 N1: the Inbound API must be a REAL subscriber of
/// IScriptArtifactChangeBus — a bundle-import publish must invalidate a cached
/// handler immediately, without waiting for the per-request revision-check
/// self-heal (which remains the correctness fallback per the invalidation
/// contract's normative semantics).
/// </summary>
public class ScriptArtifactChangeSubscriberTests
{
    /// <summary>Minimal in-test bus: records subscriptions, delivers synchronously.</summary>
    private sealed class RecordingBus : IScriptArtifactChangeBus
    {
        private readonly List<Action<ScriptArtifactsChanged>> _handlers = new();
        public int SubscriberCount => _handlers.Count;

        public void Publish(ScriptArtifactsChanged notification)
        {
            foreach (var handler in _handlers.ToArray()) handler(notification);
        }

        public IDisposable Subscribe(Action<ScriptArtifactsChanged> handler)
        {
            _handlers.Add(handler);
            return new Token(() => _handlers.Remove(handler));
        }

        private sealed class Token(Action dispose) : IDisposable
        {
            public void Dispose() => dispose();
        }
    }

    private readonly InboundScriptExecutor _executor = new(
        NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
    private readonly ApiMethodCache _methodCache = new(TimeSpan.FromMinutes(5));
    private readonly RecordingBus _bus = new();
    private readonly RouteHelper _route = new(
        Substitute.For<IInstanceLocator>(), Substitute.For<IInstanceRouter>());

    private ScriptArtifactChangeSubscriber CreateSubscriber(IScriptArtifactChangeBus? bus) =>
        new(_executor, _methodCache, NullLogger<ScriptArtifactChangeSubscriber>.Instance, bus);

    private Task<InboundScriptResult> Run(ApiMethod m) => _executor.ExecuteAsync(
        m, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));

    private static ScriptArtifactsChanged ApiMethodChanged(params string[] names) =>
        new(ScriptArtifactKinds.ApiMethod, names, "BundleImport", DateTimeOffset.UtcNow);

    [Fact]
    public async Task ApiMethodPublish_InvalidatesCachedHandler_WithoutRevisionCheckSelfHeal()
    {
        var subscriber = CreateSubscriber(_bus);
        await subscriber.StartAsync(CancellationToken.None);

        // Pin a SCRIPT-LESS handler: the RegisterHandler seam is exempt from the
        // per-request revision check, so the ONLY mechanism that can stop it
        // serving is an explicit InvalidateMethod — exactly what the bus must trigger.
        _executor.RegisterHandler("m", async _ => { await Task.CompletedTask; return 99; });
        var row = new ApiMethod("m", "return 1;") { Id = 1, TimeoutSeconds = 10 };

        var before = await Run(row);
        Assert.Contains("99", before.ResultJson); // pinned handler serves; no self-heal fires

        _bus.Publish(ApiMethodChanged("m"));

        var after = await Run(row);
        Assert.True(after.Success);
        Assert.Contains("1", after.ResultJson); // recompiled from the fresh row — invalidation, not self-heal
    }

    [Fact]
    public async Task ApiMethodPublish_PurgesKnownBadRecord()
    {
        var subscriber = CreateSubscriber(_bus);
        await subscriber.StartAsync(CancellationToken.None);

        var broken = new ApiMethod("bad", "%%% not C# %%%") { Id = 2, TimeoutSeconds = 10 };
        Assert.False(_executor.CompileAndRegister(broken));
        Assert.Equal(1, _executor.KnownBadMethodCount);

        _bus.Publish(ApiMethodChanged("bad"));

        Assert.Equal(0, _executor.KnownBadMethodCount);
    }

    [Fact]
    public async Task NonApiMethodKinds_AreIgnored()
    {
        var subscriber = CreateSubscriber(_bus);
        await subscriber.StartAsync(CancellationToken.None);

        _executor.RegisterHandler("m", async _ => { await Task.CompletedTask; return 99; });
        var row = new ApiMethod("m", "return 1;") { Id = 3, TimeoutSeconds = 10 };

        _bus.Publish(new ScriptArtifactsChanged(
            ScriptArtifactKinds.SharedScript, new[] { "m" }, "BundleImport", DateTimeOffset.UtcNow));
        _bus.Publish(new ScriptArtifactsChanged(
            ScriptArtifactKinds.Template, new[] { "m" }, "BundleImport", DateTimeOffset.UtcNow));

        var result = await Run(row);
        Assert.Contains("99", result.ResultJson); // pinned handler untouched
    }

    [Fact]
    public async Task StopAsync_Unsubscribes()
    {
        var subscriber = CreateSubscriber(_bus);
        await subscriber.StartAsync(CancellationToken.None);
        Assert.Equal(1, _bus.SubscriberCount);

        await subscriber.StopAsync(CancellationToken.None);

        Assert.Equal(0, _bus.SubscriberCount);
    }

    /// <summary>
    /// WP2.6b: an ApiMethod change notification must also drop the resolved-row cache
    /// entry (not just the compiled handler) — proving EndpointExtensions' next lookup
    /// re-fetches instead of serving a stale cached row for the rest of the TTL window.
    /// </summary>
    [Fact]
    public async Task ApiMethodPublish_InvalidatesResolvedMethodCache()
    {
        var subscriber = CreateSubscriber(_bus);
        await subscriber.StartAsync(CancellationToken.None);

        var fetchCount = 0;
        Task<ApiMethod?> Fetch(CancellationToken _)
        {
            fetchCount++;
            return Task.FromResult<ApiMethod?>(new ApiMethod("m", "return 1;") { Id = 1 });
        }

        await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        Assert.Equal(1, fetchCount); // second call served from cache

        _bus.Publish(ApiMethodChanged("m"));

        await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        Assert.Equal(2, fetchCount); // cache entry was dropped — re-fetched
    }

    [Fact]
    public async Task NullBus_NoOps()
    {
        // Site roles / plain test hosts have no bus registered — the hosted
        // service must start and stop cleanly (the revision check alone applies).
        var subscriber = CreateSubscriber(bus: null);
        await subscriber.StartAsync(CancellationToken.None);
        await subscriber.StopAsync(CancellationToken.None);
    }
}
