using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;

namespace ZB.MOM.WW.MxGateway.Client.Tests;

public sealed class GalaxyRepositoryClientTests
{
    /// <summary>
    /// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
    {
        using CancellationTokenSource cancellation = new();
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.TestConnectionReply = new TestConnectionReply { Ok = true };
        await using GalaxyRepositoryClient client = CreateClient(transport);

        bool ok = await client.TestConnectionAsync(cancellation.Token);

        Assert.True(ok);
        var call = Assert.Single(transport.TestConnectionCalls);
        Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
    }

    /// <summary>
    /// Verifies that TestConnectionAsync returns false when the server reports NotOk.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.TestConnectionReply = new TestConnectionReply { Ok = false };
        await using GalaxyRepositoryClient client = CreateClient(transport);

        bool ok = await client.TestConnectionAsync();

        Assert.False(ok);
    }

    /// <summary>
    /// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.GetLastDeployTimeReply = new GetLastDeployTimeReply { Present = false };
        await using GalaxyRepositoryClient client = CreateClient(transport);

        DateTime? deployTime = await client.GetLastDeployTimeAsync();

        Assert.Null(deployTime);
        Assert.Single(transport.GetLastDeployTimeCalls);
    }

    /// <summary>
    /// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
    {
        DateTime expected = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc);
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.GetLastDeployTimeReply = new GetLastDeployTimeReply
        {
            Present = true,
            TimeOfLastDeploy = Timestamp.FromDateTime(expected),
        };
        await using GalaxyRepositoryClient client = CreateClient(transport);

        DateTime? deployTime = await client.GetLastDeployTimeAsync();

        Assert.NotNull(deployTime);
        Assert.Equal(expected, deployTime!.Value);
    }

    /// <summary>
    /// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
        {
            NextPageToken = "page-2",
            TotalObjectCount = 2,
            Objects =
            {
                new GalaxyObject
                {
                    GobjectId = 12,
                    TagName = "DelmiaReceiver_001",
                    ContainedName = "DelmiaReceiver",
                    BrowseName = "TestMachine_001/DelmiaReceiver",
                    ParentGobjectId = 5,
                    Attributes =
                    {
                        new GalaxyAttribute
                        {
                            AttributeName = "DownloadPath",
                            FullTagReference = "DelmiaReceiver_001.DownloadPath",
                            MxDataType = 8,
                            DataTypeName = "MxString",
                        },
                    },
                },
            },
        });
        transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
        {
            TotalObjectCount = 2,
            Objects =
            {
                new GalaxyObject
                {
                    GobjectId = 13,
                    TagName = "DelmiaReceiver_002",
                },
            },
        });
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<GalaxyObject> objects = await client.DiscoverHierarchyAsync();

        Assert.Equal(2, objects.Count);
        Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
        Assert.Equal(5000, transport.DiscoverHierarchyCalls[0].Request.PageSize);
        Assert.Equal("", transport.DiscoverHierarchyCalls[0].Request.PageToken);
        Assert.Equal("page-2", transport.DiscoverHierarchyCalls[1].Request.PageToken);
        GalaxyObject obj = objects[0];
        Assert.Equal(12, obj.GobjectId);
        Assert.Equal("DelmiaReceiver_001", obj.TagName);
        GalaxyAttribute attribute = Assert.Single(obj.Attributes);
        Assert.Equal("DownloadPath", attribute.AttributeName);
        Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
    }

    /// <summary>
    /// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
    {
        using CancellationTokenSource cancellation = new();
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        await using GalaxyRepositoryClient client = CreateClient(transport);

        await client.DiscoverHierarchyAsync(cancellation.Token);

        var call = Assert.Single(transport.DiscoverHierarchyCalls);
        // The retry pipeline links the caller token with a per-call timeout token,
        // so the transport sees the linked token rather than the caller's directly.
        // Verify the link relationship by cancelling the caller and checking the
        // call-side token reflects it.
        Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
    }

    /// <summary>
    /// Verifies that TestConnectionAsync retries on transient gRPC failures.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
        {
            NextPageToken = "7:1",
        });
        transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
        {
            NextPageToken = "7:1",
        });
        await using GalaxyRepositoryClient client = CreateClient(transport);

        MxGatewayException exception = await Assert.ThrowsAsync<MxGatewayException>(
            async () => await client.DiscoverHierarchyAsync());

        Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
    }

    /// <summary>
    /// Verifies that DiscoverHierarchyAsync maps typed filter options correctly to the request.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        await using GalaxyRepositoryClient client = CreateClient(transport);

        await client.DiscoverHierarchyAsync(new DiscoverHierarchyOptions
        {
            RootContainedPath = "Area1/Line3",
            MaxDepth = 2,
            CategoryIds = [10, 13],
            TemplateChainContains = ["Pump"],
            TagNameGlob = "Pump_*",
            IncludeAttributes = false,
            AlarmBearingOnly = true,
            HistorizedOnly = true,
        });

        DiscoverHierarchyRequest request = Assert.Single(transport.DiscoverHierarchyCalls).Request;
        Assert.Equal(DiscoverHierarchyRequest.RootOneofCase.RootContainedPath, request.RootCase);
        Assert.Equal("Area1/Line3", request.RootContainedPath);
        Assert.Equal(2, request.MaxDepth);
        Assert.Equal([10, 13], request.CategoryIds);
        Assert.Equal(["Pump"], request.TemplateChainContains);
        Assert.Equal("Pump_*", request.TagNameGlob);
        Assert.True(request.HasIncludeAttributes);
        Assert.False(request.IncludeAttributes);
        Assert.True(request.AlarmBearingOnly);
        Assert.True(request.HistorizedOnly);
    }

    /// <summary>
    /// Verifies that TestConnectionAsync retries on transient gRPC failures.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.TestConnectionExceptions.Enqueue(CreateTransientRpcException());
        transport.TestConnectionReply = new TestConnectionReply { Ok = true };
        await using GalaxyRepositoryClient client = CreateClient(transport);

        bool ok = await client.TestConnectionAsync();

        Assert.True(ok);
        Assert.Equal(2, transport.TestConnectionCalls.Count);
    }

    /// <summary>
    /// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.DiscoverHierarchyExceptions.Enqueue(CreateTransientRpcException());
        transport.DiscoverHierarchyReply = new DiscoverHierarchyReply();
        await using GalaxyRepositoryClient client = CreateClient(transport);

        await client.DiscoverHierarchyAsync();

        Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
    }

    /// <summary>
    /// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        DateTime deployTime = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc);
        transport.WatchDeployEvents.Add(new DeployEvent
        {
            Sequence = 1,
            ObservedAt = Timestamp.FromDateTime(deployTime),
            TimeOfLastDeploy = Timestamp.FromDateTime(deployTime),
            TimeOfLastDeployPresent = true,
            ObjectCount = 7,
            AttributeCount = 42,
        });
        await using GalaxyRepositoryClient client = CreateClient(transport);

        List<DeployEvent> received = [];
        await foreach (DeployEvent evt in client.WatchDeployEventsAsync())
        {
            received.Add(evt);
        }

        DeployEvent only = Assert.Single(received);
        Assert.Equal(1ul, only.Sequence);
        Assert.Equal(7, only.ObjectCount);
        Assert.Equal(42, only.AttributeCount);
        Assert.True(only.TimeOfLastDeployPresent);
        var call = Assert.Single(transport.WatchDeployEventsCalls);
        Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
        // No last_seen_deploy_time supplied → request leaves the field unset.
        Assert.Null(call.Request.LastSeenDeployTime);
    }

    /// <summary>
    /// Verifies that WatchDeployEventsAsync delivers multiple events in order.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        DateTime t0 = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc);
        for (int index = 1; index <= 3; index++)
        {
            transport.WatchDeployEvents.Add(new DeployEvent
            {
                Sequence = (ulong)index,
                ObservedAt = Timestamp.FromDateTime(t0.AddSeconds(index)),
                TimeOfLastDeploy = Timestamp.FromDateTime(t0.AddSeconds(index)),
                TimeOfLastDeployPresent = true,
                ObjectCount = 10 + index,
                AttributeCount = 100 + index,
            });
        }

        DateTimeOffset lastSeen = new(t0, TimeSpan.Zero);
        await using GalaxyRepositoryClient client = CreateClient(transport);

        List<DeployEvent> received = [];
        await foreach (DeployEvent evt in client.WatchDeployEventsAsync(lastSeen))
        {
            received.Add(evt);
        }

        Assert.Equal(3, received.Count);
        Assert.Equal(new ulong[] { 1, 2, 3 }, received.Select(e => e.Sequence).ToArray());
        Assert.Equal(new[] { 11, 12, 13 }, received.Select(e => e.ObjectCount).ToArray());
        var call = Assert.Single(transport.WatchDeployEventsCalls);
        Assert.NotNull(call.Request.LastSeenDeployTime);
        Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
    }

    /// <summary>
    /// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        // Add many events; the test will cancel after the first.
        for (int index = 1; index <= 10; index++)
        {
            transport.WatchDeployEvents.Add(new DeployEvent { Sequence = (ulong)index });
        }

        using CancellationTokenSource cancellation = new();
        // Cancel before the second yield by wiring the fake's pre-yield hook.
        int yields = 0;
        transport.WatchDeployEventsBeforeYield = _ =>
        {
            yields++;
            if (yields >= 2)
            {
                cancellation.Cancel();
            }

            return Task.CompletedTask;
        };

        await using GalaxyRepositoryClient client = CreateClient(transport);

        List<DeployEvent> received = [];
        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
        {
            await foreach (DeployEvent evt in client
                .WatchDeployEventsAsync(cancellationToken: cancellation.Token))
            {
                received.Add(evt);
            }
        });

        // The first event yields before cancellation triggers on the second pass.
        Assert.Single(received);
        Assert.Equal(1ul, received[0].Sequence);
    }

    /// <summary>
    /// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        GalaxyRepositoryClient client = CreateClient(transport);

        await client.DisposeAsync();

        Assert.Throws<ObjectDisposedException>(() =>
            client.WatchDeployEventsAsync());
    }

    /// <summary>
    /// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task TestConnectionAsync_ThrowsAfterDisposal()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        GalaxyRepositoryClient client = CreateClient(transport);

        await client.DisposeAsync();

        await Assert.ThrowsAsync<ObjectDisposedException>(() => client.TestConnectionAsync());
    }

    private static GalaxyRepositoryClient CreateClient(FakeGalaxyRepositoryTransport transport)
    {
        return new GalaxyRepositoryClient(transport.Options, transport);
    }

    private static FakeGalaxyRepositoryTransport CreateTransport()
    {
        return new FakeGalaxyRepositoryTransport(new MxGatewayClientOptions
        {
            Endpoint = new Uri("http://localhost:5000"),
            ApiKey = "test-api-key",
        });
    }

    private static RpcException CreateTransientRpcException()
    {
        return new RpcException(new Status(StatusCode.Unavailable, "gateway unavailable"));
    }
}
