using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.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;

/// <summary>
///     End-to-end wiring tests for the host-side browse-scope authorisation chain:
///     API-key constraints → <see cref="GatewayRequestIdentityAccessor.Push"/> →
///     <see cref="GatewayBrowseScopeProvider.ResolveBrowseSubtrees"/> → lib
///     <see cref="GalaxyRepositoryGrpcService"/> → filtered <c>BrowseChildren</c> reply.
///
///     These tests are NOT replaceable by the isolated
///     <c>GatewayBrowseScopeProviderTests</c> (which tests the provider in isolation) or
///     the lib's own test suite (which uses a fake <c>IGalaxyBrowseScopeProvider</c>).
///     They verify that the gateway's concrete <see cref="GatewayBrowseScopeProvider"/> is
///     correctly wired to the lib's <see cref="GalaxyRepositoryGrpcService"/> and that the
///     filter actually propagates end-to-end into the returned child list.
/// </summary>
public sealed class GalaxyRepositoryHostWiringTests
{
    /// <summary>
    ///     Verifies the full production authz-filtering chain end-to-end.
    ///     Phase 1: no identity pushed → <c>BrowseChildren</c> returns both children
    ///     (unconstrained).  Phase 2: identity with <c>BrowseSubtrees = ["NonExistent"]</c>
    ///     pushed via <see cref="GatewayRequestIdentityAccessor"/> → the real
    ///     <see cref="GatewayBrowseScopeProvider"/> threads the constraint into the lib
    ///     service → <c>BrowseChildren</c> returns empty children.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Fact]
    public async Task BrowseChildren_BrowseSubtreesConstraintThroughHostWiring_FiltersChildren()
    {
        // Arrange: construct the REAL host seam — not stubs.
        GatewayRequestIdentityAccessor identityAccessor = new();
        GatewayBrowseScopeProvider scopeProvider = new(identityAccessor);

        GalaxyObject[] objects = CreateFilterObjects();
        GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
        {
            Status = GalaxyCacheStatus.Healthy,
            Sequence = 7,
            LastSuccessAt = DateTimeOffset.UtcNow,
            Objects = objects,
            Index = GalaxyHierarchyIndex.Build(objects),
            ObjectCount = objects.Length,
        };

        // Inject the REAL GatewayBrowseScopeProvider as the IGalaxyBrowseScopeProvider.
        GalaxyRepositoryGrpcService service = new(
            new StubGalaxyRepository(),
            new StubGalaxyHierarchyCache(entry),
            new GalaxyDeployNotifier(),
            scopeProvider);

        // Phase 1: no identity on the accessor → browse is unconstrained.
        // Pump_001 and Valve_001 are both children of Line3 (GobjectId = 2).
        BrowseChildrenReply unconstrained = await service.BrowseChildren(
            new BrowseChildrenRequest { ParentGobjectId = 2 },
            new TestServerCallContext());

        Assert.Equal(2, unconstrained.Children.Count);

        // Phase 2: push a constraint whose BrowseSubtrees do not match any seeded object.
        // GatewayBrowseScopeProvider.ResolveBrowseSubtrees must return ["NonExistent"],
        // the lib service must honour it, and the child list must be empty.
        ApiKeyIdentity identity = new(
            KeyId: "test-key",
            KeyPrefix: "mxgw_test",
            DisplayName: "constraint-test",
            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);
        }
    }

    /// <summary>
    ///     Returns a minimal hierarchy: Area1 at root, Line3 under Area1,
    ///     Pump_001 and Valve_001 under Line3.
    /// </summary>
    private static GalaxyObject[] CreateFilterObjects() =>
    [
        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,
        },
        new GalaxyObject
        {
            GobjectId = 3,
            TagName = "Pump_001",
            ContainedName = "Pump",
            BrowseName = "Pump_001",
            ParentGobjectId = 2,
            CategoryId = 10,
        },
        new GalaxyObject
        {
            GobjectId = 4,
            TagName = "Valve_001",
            ContainedName = "Valve",
            BrowseName = "Valve_001",
            ParentGobjectId = 2,
            CategoryId = 11,
        },
    ];

    /// <summary>Immediately-ready stub: Current returns the seeded entry, loads are instant.</summary>
    private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
    {
        /// <summary>Gets the seeded cache entry passed to the stub's constructor.</summary>
        public GalaxyHierarchyCacheEntry Current { get; } = current;

        /// <summary>No-op: the stub's entry is always considered already loaded.</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <summary>No-op: the stub's entry is always considered already loaded.</summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }

    /// <summary>
    ///     Stub repository that throws on every method: <c>BrowseChildren</c> uses only
    ///     the hierarchy cache, so the repository is never called during these tests.
    /// </summary>
    private sealed class StubGalaxyRepository : IGalaxyRepository
    {
        /// <summary>Always throws: <c>BrowseChildren</c> never calls the repository directly.</summary>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>Never returns; always throws.</returns>
        public Task<bool> TestConnectionAsync(CancellationToken ct = default) =>
            throw new NotSupportedException("Not called during BrowseChildren.");

        /// <summary>Always throws: <c>BrowseChildren</c> never calls the repository directly.</summary>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>Never returns; always throws.</returns>
        public Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) =>
            throw new NotSupportedException("Not called during BrowseChildren.");

        /// <summary>Always throws: <c>BrowseChildren</c> never calls the repository directly.</summary>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>Never returns; always throws.</returns>
        public Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) =>
            throw new NotSupportedException("Not called during BrowseChildren.");

        /// <summary>Always throws: <c>BrowseChildren</c> never calls the repository directly.</summary>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>Never returns; always throws.</returns>
        public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) =>
            throw new NotSupportedException("Not called during BrowseChildren.");

        /// <summary>Always throws: <c>BrowseChildren</c> never calls the repository directly.</summary>
        /// <param name="ct">Cancellation token.</param>
        /// <returns>Never returns; always throws.</returns>
        public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default) =>
            throw new NotSupportedException("Not called during BrowseChildren.");
    }
}
