using Grpc.Core;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Grpc;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Grpc;

public sealed class GalaxyRepositoryGrpcServiceTests
{
    /// <summary>Verifies that DiscoverHierarchy returns the requested page and totals.</summary>
    [Fact]
    public async Task DiscoverHierarchy_ReturnsRequestedPageAndTotals()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));

        DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                PageSize = 2,
            },
            new TestServerCallContext());

        Assert.Equal(2, reply.Objects.Count);
        Assert.Equal("Object_001", reply.Objects[0].TagName);
        Assert.Equal("Object_002", reply.Objects[1].TagName);
        Assert.StartsWith("7:", reply.NextPageToken, StringComparison.Ordinal);
        Assert.EndsWith(":2", reply.NextPageToken, StringComparison.Ordinal);
        Assert.Equal(3, reply.TotalObjectCount);
    }

    /// <summary>Verifies that DiscoverHierarchy with a page token returns remaining objects.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithNextPageToken_ReturnsRemainingObjects()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));
        DiscoverHierarchyReply firstPage = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                PageSize = 2,
            },
            new TestServerCallContext());

        DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                PageSize = 2,
                PageToken = firstPage.NextPageToken,
            },
            new TestServerCallContext());

        GalaxyObject item = Assert.Single(reply.Objects);
        Assert.Equal("Object_003", item.TagName);
        Assert.Equal("", reply.NextPageToken);
        Assert.Equal(3, reply.TotalObjectCount);
    }

    /// <summary>Verifies that DiscoverHierarchy with invalid paging arguments returns InvalidArgument.</summary>
    /// <param name="pageToken">The page token to test.</param>
    /// <param name="pageSize">The page size to test.</param>
    [Theory]
    [InlineData("-1", 1)]
    [InlineData("not-an-offset", 1)]
    [InlineData("7:4", 1)]
    [InlineData("6:2", 1)]
    [InlineData("", -1)]
    public async Task DiscoverHierarchy_WithInvalidPagingArguments_ReturnsInvalidArgument(
        string pageToken,
        int pageSize)
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.DiscoverHierarchy(
                new DiscoverHierarchyRequest
                {
                    PageSize = pageSize,
                    PageToken = pageToken,
                },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
    }

    /// <summary>Verifies that DiscoverHierarchy with subtree root and depth filters descendants.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithSubtreeRootAndDepth_FiltersDescendants()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));

        DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                RootContainedPath = "Area1/Line3",
                MaxDepth = 1,
                PageSize = 10,
            },
            new TestServerCallContext());

        Assert.Equal(["Line3", "Pump_001", "Valve_001"], reply.Objects.Select(obj => obj.TagName));
        Assert.Equal(3, reply.TotalObjectCount);
    }

    /// <summary>Verifies that DiscoverHierarchy applies server-side filters and omits attributes.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithServerSideFilters_AppliesAllFiltersAndOmitsAttributes()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));

        DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                RootTagName = "Area1",
                TagNameGlob = "Pump_*",
                AlarmBearingOnly = true,
                HistorizedOnly = true,
                IncludeAttributes = false,
                PageSize = 10,
                CategoryIds = { 10 },
                TemplateChainContains = { "Pump" },
            },
            new TestServerCallContext());

        GalaxyObject obj = Assert.Single(reply.Objects);
        Assert.Equal("Pump_001", obj.TagName);
        Assert.Empty(obj.Attributes);
        Assert.Equal(1, reply.TotalObjectCount);
    }

    /// <summary>Verifies that DiscoverHierarchy with filtered paging returns post-filter total.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithFilteredPaging_ReturnsPostFilterTotal()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));

        DiscoverHierarchyReply first = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                RootGobjectId = 1,
                PageSize = 1,
                CategoryIds = { 10 },
            },
            new TestServerCallContext());

        DiscoverHierarchyReply second = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                RootGobjectId = 1,
                PageSize = 1,
                PageToken = first.NextPageToken,
                CategoryIds = { 10 },
            },
            new TestServerCallContext());

        GalaxyObject firstObject = Assert.Single(first.Objects);
        GalaxyObject secondObject = Assert.Single(second.Objects);
        Assert.Equal(2, first.TotalObjectCount);
        Assert.Equal(2, second.TotalObjectCount);
        Assert.NotEqual(firstObject.TagName, secondObject.TagName);
    }

    /// <summary>Verifies that DiscoverHierarchy with mismatched filter token returns InvalidArgument.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithMismatchedFilterToken_ReturnsInvalidArgument()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));
        DiscoverHierarchyReply first = await service.DiscoverHierarchy(
            new DiscoverHierarchyRequest
            {
                PageSize = 1,
                CategoryIds = { 10 },
            },
            new TestServerCallContext());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.DiscoverHierarchy(
                new DiscoverHierarchyRequest
                {
                    PageSize = 1,
                    PageToken = first.NextPageToken,
                    CategoryIds = { 11 },
                },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
        Assert.Contains("filters", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Verifies that DiscoverHierarchy with missing root returns NotFound.</summary>
    [Fact]
    public async Task DiscoverHierarchy_WithMissingRoot_ReturnsNotFound()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.DiscoverHierarchy(
                new DiscoverHierarchyRequest
                {
                    RootTagName = "Missing",
                },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.NotFound, exception.StatusCode);
    }

    private static GalaxyRepositoryGrpcService CreateService(GalaxyHierarchyCacheEntry entry)
    {
        GalaxyRepositoryOptions options = new()
        {
            ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;",
        };
        return new GalaxyRepositoryGrpcService(
            new global::ZB.MOM.WW.MxGateway.Server.Galaxy.GalaxyRepository(options),
            new StubGalaxyHierarchyCache(entry),
            new GalaxyDeployNotifier(),
            new GatewayRequestIdentityAccessor());
    }

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

    private static IReadOnlyList<GalaxyObject> CreateObjects(int count)
    {
        return Enumerable.Range(1, count)
            .Select(index => new GalaxyObject
            {
                GobjectId = index,
                TagName = $"Object_{index:000}",
                BrowseName = $"Object_{index:000}",
            })
            .ToArray();
    }

    private static IReadOnlyList<GalaxyObject> CreateFilterObjects()
    {
        return
        [
            new GalaxyObject
            {
                GobjectId = 1,
                TagName = "Area1",
                ContainedName = "Area1",
                BrowseName = "Area1",
                IsArea = true,
                CategoryId = 13,
            },
            new GalaxyObject
            {
                GobjectId = 2,
                TagName = "Line3",
                ContainedName = "Line3",
                BrowseName = "Line3",
                ParentGobjectId = 1,
                CategoryId = 10,
                TemplateChain = { "$Line", "$Base" },
            },
            new GalaxyObject
            {
                GobjectId = 3,
                TagName = "Pump_001",
                ContainedName = "Pump",
                BrowseName = "Pump_001",
                ParentGobjectId = 2,
                CategoryId = 10,
                TemplateChain = { "$Pump", "$Base" },
                Attributes =
                {
                    new GalaxyAttribute
                    {
                        AttributeName = "PV",
                        FullTagReference = "Pump_001.PV",
                        IsAlarm = true,
                        IsHistorized = true,
                        SecurityClassification = 2,
                    },
                },
            },
            new GalaxyObject
            {
                GobjectId = 4,
                TagName = "Valve_001",
                ContainedName = "Valve",
                BrowseName = "Valve_001",
                ParentGobjectId = 2,
                CategoryId = 11,
                TemplateChain = { "$Valve" },
                Attributes =
                {
                    new GalaxyAttribute
                    {
                        AttributeName = "PV",
                        FullTagReference = "Valve_001.PV",
                    },
                },
            },
            new GalaxyObject
            {
                GobjectId = 5,
                TagName = "Other_001",
                ContainedName = "Other",
                BrowseName = "Other_001",
                CategoryId = 10,
            },
        ];
    }

    private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
    {
        /// <inheritdoc />
        public GalaxyHierarchyCacheEntry Current { get; } = current;

        /// <inheritdoc />
        public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <inheritdoc />
        public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }

    /// <summary>Verifies that BrowseChildren returns root objects and the current cache sequence when called with no parent.</summary>
    [Fact]
    public async Task BrowseChildren_RootCall_ReturnsRootsWithCacheSequence()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));

        BrowseChildrenReply reply = await service.BrowseChildren(
            new BrowseChildrenRequest(),
            new TestServerCallContext());

        Assert.Equal(2, reply.Children.Count);
        Assert.Equal("Area1", reply.Children[0].TagName);
        Assert.Equal("Other_001", reply.Children[1].TagName);
        Assert.Equal(7UL, reply.CacheSequence);
        Assert.Equal(2, reply.TotalChildCount);
        Assert.Equal(reply.Children.Count, reply.ChildHasChildren.Count);
    }

    /// <summary>Verifies that BrowseChildren returns Unavailable when the cache's first load never completes.</summary>
    [Fact]
    public async Task BrowseChildren_FirstLoadNotComplete_ReturnsUnavailable()
    {
        GalaxyRepositoryOptions options = new()
        {
            ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;",
        };
        GalaxyRepositoryGrpcService service = new(
            new global::ZB.MOM.WW.MxGateway.Server.Galaxy.GalaxyRepository(options),
            new NeverLoadsHierarchyCache(),
            new GalaxyDeployNotifier(),
            new GatewayRequestIdentityAccessor());

        // No caller-supplied CT so WaitForCacheBootstrap exits via its 5s internal budget
        // (instead of re-throwing OperationCanceledException from the caller's CT). The
        // handler then sees Status=Unknown and returns Unavailable.
        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.BrowseChildren(
                new BrowseChildrenRequest(),
                new TestServerCallContext()));

        Assert.Equal(StatusCode.Unavailable, exception.StatusCode);
    }

    /// <summary>Verifies that a page token bound to a stale cache sequence is rejected with InvalidArgument.</summary>
    [Fact]
    public async Task BrowseChildren_StaleToken_ReturnsInvalidArgument()
    {
        GalaxyRepositoryGrpcService firstService = CreateService(CreateEntry(CreateFilterObjects()));
        BrowseChildrenReply firstReply = await firstService.BrowseChildren(
            new BrowseChildrenRequest { PageSize = 1 },
            new TestServerCallContext());
        Assert.False(string.IsNullOrEmpty(firstReply.NextPageToken));

        GalaxyHierarchyCacheEntry newerEntry = CreateEntry(CreateFilterObjects()) with { Sequence = 8 };
        GalaxyRepositoryGrpcService secondService = CreateService(newerEntry);

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await secondService.BrowseChildren(
                new BrowseChildrenRequest
                {
                    PageSize = 1,
                    PageToken = firstReply.NextPageToken,
                },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
        Assert.Contains("stale", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Verifies that switching filters between paged BrowseChildren calls is rejected.</summary>
    [Fact]
    public async Task BrowseChildren_FilterChangeBetweenPages_ReturnsInvalidArgument()
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateFilterObjects()));
        BrowseChildrenReply firstReply = await service.BrowseChildren(
            new BrowseChildrenRequest
            {
                ParentGobjectId = 2,
                PageSize = 1,
            },
            new TestServerCallContext());
        Assert.False(string.IsNullOrEmpty(firstReply.NextPageToken));

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.BrowseChildren(
                new BrowseChildrenRequest
                {
                    ParentGobjectId = 2,
                    PageSize = 1,
                    PageToken = firstReply.NextPageToken,
                    TagNameGlob = "Pump*",
                },
                new TestServerCallContext()));

        Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
        Assert.Contains("filters", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Verifies that an ApiKeyIdentity browse-subtrees constraint that matches nothing produces an empty child list.</summary>
    [Fact]
    public async Task BrowseChildren_BrowseSubtreesConstraint_FiltersChildren()
    {
        GalaxyRepositoryOptions options = new()
        {
            ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;",
        };
        GatewayRequestIdentityAccessor identityAccessor = new();
        GalaxyRepositoryGrpcService service = new(
            new global::ZB.MOM.WW.MxGateway.Server.Galaxy.GalaxyRepository(options),
            new StubGalaxyHierarchyCache(CreateEntry(CreateFilterObjects())),
            new GalaxyDeployNotifier(),
            identityAccessor);

        // Sanity: with no identity pushed, both Pump and Valve come back under Line3 (id=2).
        BrowseChildrenReply unconstrained = await service.BrowseChildren(
            new BrowseChildrenRequest { ParentGobjectId = 2 },
            new TestServerCallContext());
        Assert.Equal(2, unconstrained.Children.Count);

        ApiKeyIdentity identity = new(
            KeyId: "test-key",
            KeyPrefix: "mxgw_test",
            DisplayName: "constraint-only",
            Scopes: new HashSet<string>(StringComparer.Ordinal) { GatewayScopes.MetadataRead },
            Constraints: ApiKeyConstraints.Empty with { BrowseSubtrees = new[] { "NonExistent" } });

        using (identityAccessor.Push(identity))
        {
            BrowseChildrenReply constrained = await service.BrowseChildren(
                new BrowseChildrenRequest { ParentGobjectId = 2 },
                new TestServerCallContext());
            Assert.Empty(constrained.Children);
            Assert.Equal(0, constrained.TotalChildCount);
        }
    }

    private sealed class NeverLoadsHierarchyCache : IGalaxyHierarchyCache
    {
        /// <inheritdoc />
        public GalaxyHierarchyCacheEntry Current { get; } =
            GalaxyHierarchyCacheEntry.Empty with { Status = GalaxyCacheStatus.Unknown };

        /// <inheritdoc />
        public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <inheritdoc />
        public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) =>
            Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
    }
}
