using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

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

/// <summary>
///     Coverage for <see cref="DashboardBrowseService"/> — the in-process facade the
///     Blazor BrowsePage uses to walk the Galaxy hierarchy one level at a time. The
///     service must surface the projector's <c>child_has_children</c> hint, expose the
///     current cache sequence, and translate the projector's
///     <see cref="Grpc.Core.RpcException"/> on unknown parent into a friendly error
///     rather than propagating it into the Blazor circuit.
/// </summary>
public sealed class DashboardBrowseServiceTests
{
    /// <summary>Verifies that <see cref="DashboardBrowseService.GetRoots"/> returns root-level
    /// objects with the <c>HasChildrenHint</c> projector bit set, and reports the cache
    /// sequence of the entry it projected from.</summary>
    [Fact]
    public void GetRoots_ReturnsRootObjects_WithHasChildrenHint()
    {
        StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
        DashboardBrowseService service = new(cache);

        BrowseLevelResult result = service.GetRoots(new BrowseFilterArgs());

        Assert.Single(result.Nodes);
        Assert.Equal("Plant", result.Nodes[0].Object.TagName);
        Assert.True(result.Nodes[0].HasChildrenHint);
        Assert.Equal(11UL, result.CacheSequence);
        Assert.Null(result.Error);
    }

    /// <summary>Verifies that <see cref="DashboardBrowseService.GetChildren"/> returns the
    /// direct children of the requested parent and that leaf nodes report
    /// <c>HasChildrenHint == false</c>.</summary>
    [Fact]
    public void GetChildren_ByParentGobjectId_ReturnsDirectChildren()
    {
        StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
        DashboardBrowseService service = new(cache);

        BrowseLevelResult result = service.GetChildren(parentGobjectId: 1, new BrowseFilterArgs());

        Assert.Single(result.Nodes);
        Assert.Equal("Mixer_001", result.Nodes[0].Object.TagName);
        Assert.False(result.Nodes[0].HasChildrenHint);
        Assert.Null(result.Error);
    }

    /// <summary>Verifies that an unknown parent id does not surface the projector's
    /// <see cref="Grpc.Core.RpcException"/> — the service catches the NotFound and
    /// returns an empty <see cref="BrowseLevelResult"/> with the error string set.</summary>
    [Fact]
    public void GetChildren_UnknownParent_ReturnsEmptyResultWithErrorFlag()
    {
        StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
        DashboardBrowseService service = new(cache);

        BrowseLevelResult result = service.GetChildren(parentGobjectId: 999, new BrowseFilterArgs());

        Assert.Empty(result.Nodes);
        Assert.NotNull(result.Error);
        Assert.False(string.IsNullOrEmpty(result.Error));
    }

    /// <summary>Verifies that swapping the cache's <c>Current</c> entry (as the refresh loop
    /// does after a deploy bump) causes subsequent queries to observe the new sequence.</summary>
    [Fact]
    public void CacheSequence_AdvancesAfterRefresh_NewQueriesReflectIt()
    {
        StubGalaxyHierarchyCache cache = new(CreateEntry(CreateObjects(), sequence: 11));
        DashboardBrowseService service = new(cache);

        BrowseLevelResult before = service.GetRoots(new BrowseFilterArgs());
        Assert.Equal(11UL, before.CacheSequence);

        cache.Current = CreateEntry(CreateObjects(), sequence: 12);

        BrowseLevelResult after = service.GetRoots(new BrowseFilterArgs());
        Assert.Equal(12UL, after.CacheSequence);
    }

    /// <summary>Verifies that <see cref="DashboardBrowseService.GetChildren"/> returns an
    /// empty node list with a non-empty <c>Error</c> when the cache has not yet loaded
    /// (i.e. <see cref="GalaxyHierarchyCacheEntry.HasData"/> is false).</summary>
    [Fact]
    public void GetChildren_CacheNotLoaded_ReturnsErrorResult()
    {
        StubGalaxyHierarchyCache cache = new(GalaxyHierarchyCacheEntry.Empty);
        DashboardBrowseService service = new(cache);

        BrowseLevelResult result = service.GetChildren(parentGobjectId: 1, new BrowseFilterArgs());

        Assert.Empty(result.Nodes);
        Assert.False(string.IsNullOrEmpty(result.Error));
    }

    private static IReadOnlyList<GalaxyObject> CreateObjects()
    {
        // Fixture: an Area "Plant" (id 1, parent 0, IsArea=true) containing one
        // Instance "Mixer_001" (id 2, parent 1). Both with no attributes — the
        // service is exercised through the projector, which only needs id /
        // parent / IsArea / display name to build the level slice.
        return
        [
            new GalaxyObject
            {
                GobjectId = 1,
                ParentGobjectId = 0,
                TagName = "Plant",
                BrowseName = "Plant",
                IsArea = true,
            },
            new GalaxyObject
            {
                GobjectId = 2,
                ParentGobjectId = 1,
                TagName = "Mixer_001",
                BrowseName = "Mixer_001",
                IsArea = false,
            },
        ];
    }

    private static GalaxyHierarchyCacheEntry CreateEntry(IReadOnlyList<GalaxyObject> objects, long sequence)
    {
        return GalaxyHierarchyCacheEntry.Empty with
        {
            Status = GalaxyCacheStatus.Healthy,
            Sequence = sequence,
            LastSuccessAt = DateTimeOffset.UtcNow,
            Objects = objects,
            Index = GalaxyHierarchyIndex.Build(objects),
            ObjectCount = objects.Count,
        };
    }

    private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry initial) : IGalaxyHierarchyCache
    {
        /// <summary>Mutable so a single test can swap the entry mid-flight.</summary>
        public GalaxyHierarchyCacheEntry Current { get; set; } = initial;

        /// <summary>Does nothing; the stub never refreshes.</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <summary>Does nothing; the stub is always considered loaded.</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }
}
