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

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

/// <summary>
/// Tests for the <see cref="LazyBrowseNode"/> walker over the BrowseChildren RPC.
/// </summary>
public sealed class LazyBrowseNodeTests
{
    /// <summary>
    /// Verifies that calling BrowseAsync with no parent returns the root nodes
    /// from the first BrowseChildren reply and surfaces the per-child has-children hint.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Browse_NoParent_ReturnsRoots()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true), BuildObject(2, "Other")],
            childHasChildren: [true, false],
            cacheSequence: 1));
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();

        Assert.Equal(2, roots.Count);
        Assert.Equal("Plant", roots[0].Object.TagName);
        Assert.True(roots[0].HasChildrenHint);
        Assert.False(roots[0].IsExpanded);
        Assert.Equal("Other", roots[1].Object.TagName);
        Assert.False(roots[1].HasChildrenHint);
        Assert.False(roots[1].IsExpanded);
    }

    /// <summary>
    /// Verifies that ExpandAsync populates Children and marks the node expanded after one RPC.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_PopulatesChildrenAndMarksExpanded()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 1));
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(10, "Line1")],
            childHasChildren: [false],
            cacheSequence: 1));
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
        await roots[0].ExpandAsync();

        Assert.True(roots[0].IsExpanded);
        Assert.Single(roots[0].Children);
        Assert.Equal("Line1", roots[0].Children[0].Object.TagName);
        Assert.Equal(2, transport.BrowseChildrenCalls.Count);
    }

    /// <summary>
    /// Verifies that a second ExpandAsync call is a no-op and issues no additional RPC.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_CalledTwice_NoSecondRpc()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 1));
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(10, "Line1")],
            childHasChildren: [false],
            cacheSequence: 1));
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
        await roots[0].ExpandAsync();
        await roots[0].ExpandAsync();

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

    /// <summary>
    /// Verifies that an RPC failure (NotFound) during expand is wrapped in MxGatewayException.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_UnknownParent_ThrowsMxGatewayException()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 1));
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();

        // Queue the failure for the upcoming ExpandAsync call so it consumes
        // the exception on its first RPC rather than the BrowseAsync above.
        transport.BrowseChildrenExceptions.Enqueue(
            new MxGatewayException(
                "Parent not found",
                new RpcException(new Status(StatusCode.NotFound, "Parent not found"))));

        await Assert.ThrowsAsync<MxGatewayException>(async () => await roots[0].ExpandAsync());
        Assert.False(roots[0].IsExpanded);
        Assert.Empty(roots[0].Children);
    }

    /// <summary>
    /// Verifies that ExpandAsync drains multi-page sibling replies and forwards the page token.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_MultiPageSiblings_GathersAllPages()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        // Roots
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(7, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 1));
        // First child page (2 children) with a next token
        BrowseChildrenReply childPage1 = BuildReply(
            children: [BuildObject(70, "ChildA"), BuildObject(71, "ChildB")],
            childHasChildren: [false, false],
            cacheSequence: 1);
        childPage1.NextPageToken = "7:abc:2";
        transport.BrowseChildrenReplies.Enqueue(childPage1);
        // Second child page (1 child) with no next token
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(72, "ChildC")],
            childHasChildren: [false],
            cacheSequence: 1));
        await using GalaxyRepositoryClient client = CreateClient(transport);

        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
        await roots[0].ExpandAsync();

        Assert.Equal(3, roots[0].Children.Count);
        Assert.Equal(3, transport.BrowseChildrenCalls.Count);
        Assert.Equal("7:abc:2", transport.BrowseChildrenCalls[2].Request.PageToken);
    }

    /// <summary>
    /// Verifies that ten concurrent ExpandAsync calls issue exactly one RPC, not ten.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_CalledConcurrently_OnlyFiresOneRpc()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 7));
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(2, "Mixer_001")],
            childHasChildren: [false],
            cacheSequence: 7));

        await using GalaxyRepositoryClient client = CreateClient(transport);
        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();

        // Fire ten concurrent expands of the same node.
        Task[] tasks = Enumerable.Range(0, 10)
            .Select(_ => roots[0].ExpandAsync())
            .ToArray();
        await Task.WhenAll(tasks);

        Assert.True(roots[0].IsExpanded);
        Assert.Single(roots[0].Children);
        // 1 roots fetch + exactly 1 expand fetch = 2 total
        Assert.Equal(2, transport.BrowseChildrenCalls.Count);
    }

    /// <summary>
    /// Verifies that reading Children/IsExpanded concurrently with an in-flight ExpandAsync
    /// never throws (no torn enumeration of a mid-append list) and, once IsExpanded flips to
    /// true, the published Children snapshot is fully populated. Pins the safe-publication
    /// contract on the lock-free readers.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Expand_ConcurrentReadOfChildren_NeverTearsAndPublishesAtomically()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(1, "Plant", isArea: true)],
            childHasChildren: [true],
            cacheSequence: 1));

        // Multi-page child set so the expand loop spends meaningful time appending,
        // widening the window for a concurrent reader to observe a torn list.
        BrowseChildrenReply childPage1 = BuildReply(
            children: [BuildObject(10, "A"), BuildObject(11, "B"), BuildObject(12, "C")],
            childHasChildren: [false, false, false],
            cacheSequence: 1);
        childPage1.NextPageToken = "1:p:3";
        transport.BrowseChildrenReplies.Enqueue(childPage1);
        transport.BrowseChildrenReplies.Enqueue(BuildReply(
            children: [BuildObject(13, "D"), BuildObject(14, "E")],
            childHasChildren: [false, false],
            cacheSequence: 1));

        await using GalaxyRepositoryClient client = CreateClient(transport);
        IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
        LazyBrowseNode node = roots[0];

        // Gate the child-page RPCs so the expand stays mid-flight while the reader spins.
        using SemaphoreSlim release = new(0, 1);
        bool firstChildCall = true;
        transport.BrowseChildrenGate = async () =>
        {
            if (firstChildCall)
            {
                firstChildCall = false;
                await release.WaitAsync().ConfigureAwait(false);
            }
        };

        using CancellationTokenSource readerStop = new();
        Exception? readerFailure = null;
        Task reader = Task.Run(() =>
        {
            try
            {
                while (!readerStop.IsCancellationRequested)
                {
                    bool expanded = node.IsExpanded;

                    // Enumerate the snapshot; a torn/mid-append list would throw here.
                    int count = 0;
                    foreach (LazyBrowseNode _ in node.Children)
                    {
                        count++;
                    }

                    // If the node reports expanded, the published snapshot must be complete.
                    if (expanded)
                    {
                        Assert.Equal(5, count);
                    }
                }
            }
            catch (Exception ex)
            {
                readerFailure = ex;
            }
        });

        Task expand = node.ExpandAsync();
        // Let the reader spin against the empty pre-publication snapshot for a moment.
        await Task.Delay(50);
        release.Release();
        await expand;

        // Let the reader observe the post-publication state, then stop it.
        await Task.Delay(50);
        readerStop.Cancel();
        await reader;

        Assert.Null(readerFailure);
        Assert.True(node.IsExpanded);
        Assert.Equal(5, node.Children.Count);
    }

    /// <summary>
    /// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task Browse_WithFilter_ForwardsToRequest()
    {
        FakeGalaxyRepositoryTransport transport = CreateTransport();
        await using GalaxyRepositoryClient client = CreateClient(transport);

        await client.BrowseAsync(new BrowseChildrenOptions
        {
            TagNameGlob = "Mixer*",
            AlarmBearingOnly = true,
        });

        BrowseChildrenRequest request = Assert.Single(transport.BrowseChildrenCalls).Request;
        Assert.Equal("Mixer*", request.TagNameGlob);
        Assert.True(request.AlarmBearingOnly);
    }

    private static GalaxyObject BuildObject(int id, string tag, bool isArea = false)
        => new() { GobjectId = id, TagName = tag, BrowseName = tag, IsArea = isArea };

    private static BrowseChildrenReply BuildReply(
        IReadOnlyList<GalaxyObject> children,
        IReadOnlyList<bool> childHasChildren,
        ulong cacheSequence)
    {
        BrowseChildrenReply reply = new() { TotalChildCount = children.Count, CacheSequence = cacheSequence };
        reply.Children.AddRange(children);
        reply.ChildHasChildren.AddRange(childHasChildren);
        return reply;
    }

    private static GalaxyRepositoryClient CreateClient(FakeGalaxyRepositoryTransport transport)
        => new(transport.Options, transport);

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