using Grpc.Core;
using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.Grpc;

namespace ZB.MOM.WW.GalaxyRepository.Tests;

/// <summary>
///     Verifies that <see cref="GalaxyRepositoryGrpcService"/> scopes browse/discover
///     results through the injected <see cref="IGalaxyBrowseScopeProvider"/>. The default
///     (null-returning) provider must preserve full-hierarchy behavior, and a provider
///     returning a glob that matches nothing must filter the result to empty.
/// </summary>
public sealed class GalaxyRepositoryGrpcServiceScopeTests
{
    /// <summary>
    ///     A scope provider built with a <see langword="null"/> result behaves like the
    ///     default <see cref="NullGalaxyBrowseScopeProvider"/>: DiscoverHierarchy returns
    ///     the full hierarchy.
    /// </summary>
    [Fact]
    public async Task DiscoverHierarchy_DefaultScope_ReturnsFullHierarchy()
    {
        GalaxyRepositoryGrpcService service = CreateService(
            BuildSampleEntry(),
            new FakeBrowseScopeProvider(subtrees: null));

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

        // The sample hierarchy has six objects; with no scoping all are returned.
        Assert.Equal(6, reply.TotalObjectCount);
        Assert.Equal(6, reply.Objects.Count);
    }

    /// <summary>
    ///     A scope provider returning a glob that matches no contained path filters the
    ///     children to empty, mirroring mxaccessgw's browse-subtree constraint behavior.
    /// </summary>
    [Fact]
    public async Task BrowseChildren_ScopedProvider_FiltersChildren()
    {
        GalaxyHierarchyCacheEntry entry = BuildSampleEntry();

        // Sanity: with the default (unscoped) provider, LineA(2) has two children.
        GalaxyRepositoryGrpcService unscopedService = CreateService(
            entry,
            new FakeBrowseScopeProvider(subtrees: null));
        BrowseChildrenReply unscoped = await unscopedService.BrowseChildren(
            new BrowseChildrenRequest { ParentGobjectId = 2 },
            new TestServerCallContext());
        Assert.Equal(2, unscoped.Children.Count);

        // A glob matching nothing scopes the result to empty.
        GalaxyRepositoryGrpcService scopedService = CreateService(
            entry,
            new FakeBrowseScopeProvider(subtrees: ["NonExistent"]));
        BrowseChildrenReply scoped = await scopedService.BrowseChildren(
            new BrowseChildrenRequest { ParentGobjectId = 2 },
            new TestServerCallContext());

        Assert.Empty(scoped.Children);
        Assert.Equal(0, scoped.TotalChildCount);
    }

    /// <summary>
    ///     When the scope provider returns a non-empty glob, the deploy event's
    ///     object/attribute counts are re-projected against the scoped subtree and override
    ///     the raw counts the notifier published.
    /// </summary>
    [Fact]
    public async Task WatchDeployEvents_ScopedProvider_EmitsFilteredCounts()
    {
        GalaxyHierarchyCacheEntry entry = BuildSampleEntry();

        // Sanity: the full hierarchy projects to six objects / four attributes.
        GalaxyHierarchyQueryResult full = GalaxyHierarchyProjector.Project(
            entry,
            new DiscoverHierarchyRequest());
        Assert.Equal(6, full.TotalObjectCount);
        Assert.Equal(4, full.Objects.Sum(obj => obj.Attributes.Count));

        // The glob selects only LineA's two leaf objects (Pump01, Valve01), each with one
        // attribute. That scoped projection (2 objects / 2 attributes) is a non-empty subset
        // distinct from both the full count and the raw notifier values below.
        GalaxyHierarchyQueryResult scopedProjection = GalaxyHierarchyProjector.Project(
            entry,
            new DiscoverHierarchyRequest(),
            browseSubtreeGlobs: ["PlantArea/LineA/*"]);
        Assert.Equal(2, scopedProjection.TotalObjectCount);
        Assert.Equal(2, scopedProjection.Objects.Sum(obj => obj.Attributes.Count));

        // Publish a deploy event whose RAW counts differ from both full and scoped, so an
        // assertion on the scoped values proves the override actually happened.
        RecordingDeployNotifier notifier = new();
        notifier.Publish(new GalaxyDeployEventInfo(
            Sequence: 42,
            ObservedAt: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
            TimeOfLastDeploy: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
            ObjectCount: 999,
            AttributeCount: 888));

        GalaxyRepositoryGrpcService service = CreateService(
            entry,
            new FakeBrowseScopeProvider(subtrees: ["PlantArea/LineA/*"]),
            notifier);

        // RecordingDeployNotifier yields the latest event then completes, so the stream
        // ends after the single event without needing cancellation.
        CapturingStreamWriter responseStream = new();
        await service.WatchDeployEvents(
            new WatchDeployEventsRequest(),
            responseStream,
            new TestServerCallContext());

        DeployEvent emitted = Assert.Single(responseStream.Written);
        Assert.Equal(scopedProjection.TotalObjectCount, emitted.ObjectCount);
        Assert.Equal(2, emitted.AttributeCount);
        // The raw notifier values were overridden by the scoped re-projection.
        Assert.NotEqual(999, emitted.ObjectCount);
        Assert.NotEqual(888, emitted.AttributeCount);
    }

    private static GalaxyRepositoryGrpcService CreateService(
        GalaxyHierarchyCacheEntry entry,
        IGalaxyBrowseScopeProvider scope,
        IGalaxyDeployNotifier? notifier = null)
    {
        // No test here calls TestConnection, so a fake repository (no real SQL) is enough
        // and removes any latent localhost-connection risk.
        return new GalaxyRepositoryGrpcService(
            new FakeGalaxyRepository(
                Array.Empty<GalaxyHierarchyRow>(),
                Array.Empty<GalaxyAttributeRow>(),
                deployTime: null),
            new StubGalaxyHierarchyCache(entry),
            notifier ?? new RecordingDeployNotifier(),
            scope);
    }

    // A small but representative galaxy, materialized through the real cache refresh path
    // so the projectors run against a real GalaxyHierarchyIndex:
    //   PlantArea (area, id 1)
    //     ├─ LineA (area, id 2)
    //     │    ├─ Pump01 (id 10)
    //     │    └─ Valve01 (id 11)
    //     └─ Mixer01 (id 12)
    //   StandaloneTank (id 20, root)
    private static GalaxyHierarchyCacheEntry BuildSampleEntry()
    {
        List<GalaxyHierarchyRow> hierarchy =
        [
            Hierarchy(1, "PlantArea", parent: 0, isArea: true, category: 100),
            Hierarchy(2, "LineA", parent: 1, isArea: true, category: 100),
            Hierarchy(10, "Pump01", parent: 2, category: 200, templates: ["$Pump"]),
            Hierarchy(11, "Valve01", parent: 2, category: 201, templates: ["$Valve"]),
            Hierarchy(12, "Mixer01", parent: 1, category: 202, templates: ["$Mixer"]),
            Hierarchy(20, "StandaloneTank", parent: 0, category: 203, templates: ["$Tank"]),
        ];

        List<GalaxyAttributeRow> attributes =
        [
            Attribute(10, "Pump01.PV"),
            Attribute(11, "Valve01.Cmd"),
            Attribute(12, "Mixer01.Fault"),
            Attribute(20, "StandaloneTank.Level"),
        ];

        FakeGalaxyRepository repository = new(
            hierarchy,
            attributes,
            deployTime: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
        using GalaxyHierarchyCache cache = new(repository, new RecordingDeployNotifier());
        cache.RefreshAsync(CancellationToken.None).GetAwaiter().GetResult();
        GalaxyHierarchyCacheEntry entry = cache.Current;
        Assert.True(entry.HasData);
        return entry;
    }

    private static GalaxyHierarchyRow Hierarchy(
        int id,
        string tagName,
        int parent,
        bool isArea = false,
        int category = 0,
        IReadOnlyList<string>? templates = null) => new()
        {
            GobjectId = id,
            TagName = tagName,
            ContainedName = tagName,
            BrowseName = tagName,
            ParentGobjectId = parent,
            IsArea = isArea,
            CategoryId = category,
            TemplateChain = templates ?? Array.Empty<string>(),
        };

    private static GalaxyAttributeRow Attribute(int gobjectId, string fullTagReference) => new()
    {
        GobjectId = gobjectId,
        AttributeName = fullTagReference.Split('.')[^1],
        FullTagReference = fullTagReference,
    };

    /// <summary>An <see cref="IGalaxyBrowseScopeProvider"/> that returns a fixed glob list.</summary>
    private sealed class FakeBrowseScopeProvider(IReadOnlyList<string>? subtrees) : IGalaxyBrowseScopeProvider
    {
        public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context) => subtrees;
    }

    /// <summary>Serves a fixed cache entry; never blocks on first load.</summary>
    private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
    {
        public GalaxyHierarchyCacheEntry Current { get; } = current;

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

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

    /// <summary>Records every <see cref="DeployEvent"/> the service streams.</summary>
    private sealed class CapturingStreamWriter : IServerStreamWriter<DeployEvent>
    {
        public List<DeployEvent> Written { get; } = [];

        public WriteOptions? WriteOptions { get; set; }

        public Task WriteAsync(DeployEvent message)
        {
            Written.Add(message);
            return Task.CompletedTask;
        }
    }

    /// <summary>Minimal in-memory <see cref="ServerCallContext"/> for direct service unit tests.</summary>
    private sealed class TestServerCallContext : ServerCallContext
    {
        private readonly Metadata _requestHeaders = [];
        private readonly Metadata _responseTrailers = [];
        private readonly Dictionary<object, object> _userState = [];
        private Status _status;
        private WriteOptions? _writeOptions;

        protected override string MethodCore => "/zb.galaxy.v1.GalaxyRepository/Test";

        protected override string HostCore => "localhost";

        protected override string PeerCore => "ipv4:127.0.0.1:5000";

        protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);

        protected override Metadata RequestHeadersCore => _requestHeaders;

        protected override CancellationToken CancellationTokenCore => CancellationToken.None;

        protected override Metadata ResponseTrailersCore => _responseTrailers;

        protected override Status StatusCore
        {
            get => _status;
            set => _status = value;
        }

        protected override WriteOptions? WriteOptionsCore
        {
            get => _writeOptions;
            set => _writeOptions = value;
        }

        protected override AuthContext AuthContextCore { get; } = new(
            string.Empty,
            new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));

        protected override IDictionary<object, object> UserStateCore => _userState;

        protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask;

        protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) =>
            throw new NotSupportedException();
    }
}
