using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc;

public class SiteStreamGrpcClientFactoryTests
{
    private readonly ILoggerFactory _loggerFactory = NullLoggerFactory.Instance;

    [Fact]
    public void GetOrCreate_ReturnsSameClientForSameSite()
    {
        using var factory = new SiteStreamGrpcClientFactory(_loggerFactory);

        var client1 = factory.GetOrCreate("site-a", "http://localhost:5100");
        var client2 = factory.GetOrCreate("site-a", "http://localhost:5100");

        Assert.Same(client1, client2);
    }

    [Fact]
    public void GetOrCreate_ReturnsDifferentClientsForDifferentSites()
    {
        using var factory = new SiteStreamGrpcClientFactory(_loggerFactory);

        var client1 = factory.GetOrCreate("site-a", "http://localhost:5100");
        var client2 = factory.GetOrCreate("site-b", "http://localhost:5200");

        Assert.NotSame(client1, client2);
    }

    [Fact]
    public async Task RemoveSite_DisposesClient()
    {
        var factory = new SiteStreamGrpcClientFactory(_loggerFactory);

        var client1 = factory.GetOrCreate("site-a", "http://localhost:5100");
        await factory.RemoveSiteAsync("site-a");

        // After removal, GetOrCreate should return a new instance
        var client2 = factory.GetOrCreate("site-a", "http://localhost:5100");
        Assert.NotSame(client1, client2);
    }

    [Fact]
    public async Task RemoveSite_NonExistent_DoesNotThrow()
    {
        var factory = new SiteStreamGrpcClientFactory(_loggerFactory);
        await factory.RemoveSiteAsync("does-not-exist"); // Should not throw
    }

    [Fact]
    public async Task DisposeAsync_DisposesAllClients()
    {
        var factory = new SiteStreamGrpcClientFactory(_loggerFactory);

        factory.GetOrCreate("site-a", "http://localhost:5100");
        factory.GetOrCreate("site-b", "http://localhost:5200");

        await factory.DisposeAsync();

        // After dispose, creating new clients should work (new instances)
        // This tests that Dispose doesn't throw
    }

    [Fact]
    public void GetOrCreate_EndpointChanged_ReturnsClientBoundToNewEndpoint()
    {
        // Communication-012 regression: when the same site is requested with a
        // *different* endpoint (the NodeA→NodeB failover flip), the factory must
        // hand back a client bound to the new endpoint, not the stale cached one.
        using var factory = new TrackingEndpointFactory();

        var nodeA = factory.GetOrCreate("site-a", "http://localhost:5100");
        var nodeB = factory.GetOrCreate("site-a", "http://localhost:5200");

        Assert.NotSame(nodeA, nodeB);
        Assert.Equal("http://localhost:5100", nodeA.Endpoint);
        Assert.Equal("http://localhost:5200", nodeB.Endpoint);
    }

    [Fact]
    public void GetOrCreate_DifferentEndpointSameSite_KeepsBothClientsAlive()
    {
        // Arch review 02 (High): the cache is keyed by (site, endpoint). Both node
        // channels for a site coexist, so a per-session NodeA→NodeB failover flip
        // must NOT dispose the other endpoint's channel — another debug session may
        // be using it. (Supersedes Communication-013's dispose-on-endpoint-change.)
        using var factory = new TrackingEndpointFactory();

        var a = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100");
        var b = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5200");

        Assert.NotSame(a, b);
        Assert.False(a.Disposed, "the other endpoint's channel must stay alive");
        Assert.Same(a, factory.GetOrCreate("site-a", "http://localhost:5100")); // still cached
    }

    [Fact]
    public void TryGet_ReturnsCachedClientOrNull_WithoutCreating()
    {
        using var factory = new TrackingEndpointFactory();

        Assert.Null(factory.TryGet("site-a", "http://localhost:5100"));
        var a = factory.GetOrCreate("site-a", "http://localhost:5100");
        Assert.Same(a, factory.TryGet("site-a", "http://localhost:5100"));
        Assert.Null(factory.TryGet("site-a", "http://localhost:5200")); // other endpoint not cached
        Assert.Equal(1, factory.CreatedCount); // TryGet never creates
    }

    [Fact]
    public async Task RemoveSiteAsync_DisposesAllEndpointsForTheSite_OnlyThatSite()
    {
        using var factory = new TrackingEndpointFactory();
        var a = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100");
        var b = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5200");
        var other = (TrackingEndpointClient)factory.GetOrCreate("site-b", "http://localhost:5100");

        await factory.RemoveSiteAsync("site-a");

        Assert.True(a.Disposed);
        Assert.True(b.Disposed);
        Assert.False(other.Disposed);
    }

    [Fact]
    public void GetOrCreate_SameEndpoint_DoesNotDisposeOrRecreate()
    {
        // Endpoint unchanged → the cached client is reused untouched.
        using var factory = new TrackingEndpointFactory();

        var first = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100");
        var second = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100");

        Assert.Same(first, second);
        Assert.False(first.Disposed);
    }

    /// <summary>Test client that records its endpoint and disposal (no real channel).</summary>
    private sealed class TrackingEndpointClient : SiteStreamGrpcClient
    {
        public TrackingEndpointClient(string endpoint) : base(endpoint) { }
        public bool Disposed { get; private set; }
        public override void Dispose() => Disposed = true;
        public override ValueTask DisposeAsync()
        {
            Disposed = true;
            return ValueTask.CompletedTask;
        }
    }

    /// <summary>Factory that hands out endpoint-tracking clients.</summary>
    private sealed class TrackingEndpointFactory : SiteStreamGrpcClientFactory
    {
        public TrackingEndpointFactory() : base(NullLoggerFactory.Instance) { }
        public int CreatedCount { get; private set; }
        protected override SiteStreamGrpcClient CreateClient(string siteIdentifier, string grpcEndpoint)
        {
            CreatedCount++;
            return new TrackingEndpointClient(grpcEndpoint);
        }
    }
}
