using System.Diagnostics;
using Grpc.Core;
using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;

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

/// <summary>
///     Adversarial-input coverage for the Galaxy Repository browse filter layer.
///     <para>
///         The genuine, testable concern is that adversarial filter strings — SQL
///         metacharacters (<c>'</c>, <c>;</c>) and <c>LIKE</c>-wildcards (<c>%</c>,
///         <c>_</c>) — are treated as opaque literals by the in-memory filter layer:
///         they must never act as wildcards, never throw, and never trigger catastrophic
///         regex backtracking in <see cref="GalaxyGlobMatcher"/>.
///     </para>
/// </summary>
public sealed class GalaxyFilterInputSafetyTests
{
    private static readonly string[] AdversarialInputs =
    [
        "'",
        "' OR '1'='1",
        "'; DROP TABLE gobject;--",
        "%",
        "_",
        "100%_off",
        "[abc]",
        "Pump'001",
    ];

    /// <summary>Returns adversarial input cases for theory tests.</summary>
    /// <returns>Theory data of adversarial input strings.</returns>
    public static TheoryData<string> AdversarialInputCases()
    {
        TheoryData<string> data = [];
        foreach (string input in AdversarialInputs)
        {
            data.Add(input);
        }

        return data;
    }

    /// <summary>
    ///     Verifies <see cref="GalaxyGlobMatcher"/> treats SQL metacharacters and
    ///     <c>LIKE</c>-wildcards as literals — a glob equal to the literal value matches,
    ///     and the same glob does not spuriously match an unrelated value.
    /// </summary>
    /// <param name="input">An adversarial input containing SQL metacharacters or LIKE wildcards.</param>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public void GlobMatcher_TreatsSqlMetacharactersAsLiterals(string input)
    {
        Assert.True(
            GalaxyGlobMatcher.IsMatch(input, input),
            $"A glob equal to the literal value should match: {input}");
        Assert.False(
            GalaxyGlobMatcher.IsMatch("UnrelatedTagName", input),
            $"Adversarial glob must not behave as a wildcard against unrelated text: {input}");
    }

    /// <summary>
    ///     Verifies the SQL <c>LIKE</c> wildcards <c>%</c> and <c>_</c> are NOT treated as
    ///     wildcards by the glob matcher; only <c>*</c> and <c>?</c> are glob wildcards.
    /// </summary>
    [Fact]
    public void GlobMatcher_DoesNotTreatLikeWildcardsAsWildcards()
    {
        // '%' would match anything if interpreted as a SQL LIKE wildcard.
        Assert.False(GalaxyGlobMatcher.IsMatch("Pump_001", "%"));
        // '_' would match a single character if interpreted as a SQL LIKE wildcard.
        Assert.False(GalaxyGlobMatcher.IsMatch("A", "_"));
        Assert.True(GalaxyGlobMatcher.IsMatch("_", "_"));
        // '*' and '?' remain glob wildcards.
        Assert.True(GalaxyGlobMatcher.IsMatch("Pump_001", "Pump*"));
        Assert.True(GalaxyGlobMatcher.IsMatch("Pump_001", "Pump_00?"));
    }

    /// <summary>
    ///     <see cref="GalaxyGlobMatcher"/> caches
    ///     the compiled regex per glob pattern. Repeated calls with the same pattern, and
    ///     interleaved calls with different patterns, must keep returning the correct
    ///     literal-vs-wildcard result rather than a stale cached match.
    /// </summary>
    [Fact]
    public void GlobMatcher_RepeatedAndInterleavedPatterns_StayCorrect()
    {
        for (int i = 0; i < 5; i++)
        {
            Assert.True(GalaxyGlobMatcher.IsMatch("Pump_001", "Pump_*"));
            Assert.False(GalaxyGlobMatcher.IsMatch("Valve_001", "Pump_*"));
            Assert.True(GalaxyGlobMatcher.IsMatch("Valve_001", "Valve_00?"));
            Assert.False(GalaxyGlobMatcher.IsMatch("Pump_001", "Valve_00?"));
            // A glob equal to a SQL metacharacter still matches only its literal.
            Assert.True(GalaxyGlobMatcher.IsMatch("%", "%"));
            Assert.False(GalaxyGlobMatcher.IsMatch("anything", "%"));
        }
    }

    /// <summary>
    ///     Adversarial coverage for many distinct client-supplied globs: submitting a
    ///     large number of unique <c>TagNameGlob</c> values (which the shared library's
    ///     <see cref="GalaxyGlobMatcher"/> caches internally) must keep returning the
    ///     correct literal-vs-wildcard result for every glob and never throw — a client
    ///     cannot poison the matcher by flooding it with distinct patterns.
    ///     <para>
    ///         The matcher's internal compiled-regex cache and its bound are now owned by
    ///         the <c>ZB.MOM.WW.GalaxyRepository</c> library and covered by its own test
    ///         suite, so this host test asserts on observable behaviour rather than the
    ///         (no longer exposed) cache-size counters.
    ///     </para>
    /// </summary>
    [Fact]
    public void GlobMatcher_WithManyDistinctPatterns_StayCorrectAndDoNotThrow()
    {
        const int submissions = 2048;
        for (int i = 0; i < submissions; i++)
        {
            string uniqueGlob = $"client_supplied_{i}_*";
            Assert.True(
                GalaxyGlobMatcher.IsMatch($"client_supplied_{i}_thing", uniqueGlob),
                $"Each distinct glob must still match its own value: {uniqueGlob}");
            Assert.False(
                GalaxyGlobMatcher.IsMatch("UnrelatedTagName", uniqueGlob),
                $"A distinct glob must not wildcard-match unrelated text: {uniqueGlob}");
        }
    }

    /// <summary>
    ///     Verifies a pathological glob does not cause catastrophic regex backtracking —
    ///     <see cref="GalaxyGlobMatcher"/> escapes every literal character and applies a
    ///     100 ms regex timeout, so a long adversarial input completes promptly.
    /// </summary>
    [Fact]
    public void GlobMatcher_WithPathologicalInput_DoesNotHang()
    {
        string pathologicalGlob = new string('a', 5000) + "!";
        string pathologicalValue = new string('a', 5000);

        Stopwatch stopwatch = Stopwatch.StartNew();
        bool matched = GalaxyGlobMatcher.IsMatch(pathologicalValue, pathologicalGlob);
        stopwatch.Stop();

        Assert.False(matched);
        Assert.True(
            stopwatch.Elapsed < TimeSpan.FromSeconds(2),
            $"Glob matching took {stopwatch.ElapsedMilliseconds} ms — expected sub-second.");
    }

    /// <summary>
    ///     Verifies the <see cref="GalaxyHierarchyProjector"/> <c>TagNameGlob</c> filter
    ///     treats an adversarial glob as a literal: it never wildcard-matches the whole
    ///     hierarchy and never throws.
    /// </summary>
    /// <param name="glob">An adversarial glob containing SQL metacharacters or LIKE wildcards.</param>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public void Projector_TagNameGlob_WithAdversarialInput_DoesNotMatchEverything(string glob)
    {
        GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects());

        GalaxyHierarchyQueryResult result = GalaxyHierarchyProjector.Project(
            entry,
            new DiscoverHierarchyRequest { TagNameGlob = glob },
            browseSubtreeGlobs: Array.Empty<string>());

        // None of the seeded tag names equal an adversarial string, so a correctly
        // literal filter returns zero matches rather than the whole hierarchy.
        Assert.Equal(0, result.TotalObjectCount);
        Assert.Empty(result.Objects);
    }

    /// <summary>
    ///     Verifies an adversarial <c>RootTagName</c> resolves through the projector as a
    ///     literal — an exact-match lookup that finds nothing and surfaces NotFound,
    ///     never matching unrelated objects or throwing an unexpected exception.
    /// </summary>
    /// <param name="rootTagName">An adversarial tag name containing SQL metacharacters or LIKE wildcards.</param>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public void Projector_RootTagName_WithAdversarialInput_ThrowsNotFound(string rootTagName)
    {
        GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects());

        RpcException exception = Assert.Throws<RpcException>(
            () => GalaxyHierarchyProjector.Project(
                entry,
                new DiscoverHierarchyRequest { RootTagName = rootTagName },
                browseSubtreeGlobs: Array.Empty<string>()));

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

    /// <summary>
    ///     Verifies an adversarial <c>TemplateChainContains</c> filter is a literal
    ///     substring test — it never matches unrelated template chains and never throws.
    /// </summary>
    /// <param name="filter">An adversarial filter containing SQL metacharacters or LIKE wildcards.</param>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public void Projector_TemplateChainContains_WithAdversarialInput_MatchesNothing(string filter)
    {
        GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects());
        DiscoverHierarchyRequest request = new();
        request.TemplateChainContains.Add(filter);

        GalaxyHierarchyQueryResult result = GalaxyHierarchyProjector.Project(
            entry,
            request,
            browseSubtreeGlobs: Array.Empty<string>());

        Assert.Equal(0, result.TotalObjectCount);
    }

    /// <summary>
    ///     Verifies the <see cref="GalaxyRepositoryGrpcService.DiscoverHierarchy"/> RPC
    ///     handles an adversarial <c>TagNameGlob</c> end-to-end: the request succeeds with
    ///     zero matches rather than returning the whole hierarchy or faulting.
    /// </summary>
    /// <param name="glob">An adversarial glob containing SQL metacharacters or LIKE wildcards.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public async Task DiscoverHierarchy_WithAdversarialTagNameGlob_ReturnsZeroMatches(string glob)
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects()));

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

        Assert.Equal(0, reply.TotalObjectCount);
        Assert.Empty(reply.Objects);
    }

    /// <summary>
    ///     Verifies the <see cref="GalaxyRepositoryGrpcService.DiscoverHierarchy"/> RPC
    ///     maps an adversarial <c>RootTagName</c> to NotFound rather than executing it as
    ///     a query fragment or matching unrelated objects.
    /// </summary>
    /// <param name="rootTagName">An adversarial tag name containing SQL metacharacters or LIKE wildcards.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Theory]
    [MemberData(nameof(AdversarialInputCases))]
    public async Task DiscoverHierarchy_WithAdversarialRootTagName_ReturnsNotFound(string rootTagName)
    {
        GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects()));

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            async () => await service.DiscoverHierarchy(
                new DiscoverHierarchyRequest { RootTagName = rootTagName, PageSize = 100 },
                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 ZB.MOM.WW.GalaxyRepository.GalaxyRepository(options),
            new StubGalaxyHierarchyCache(entry),
            new GalaxyDeployNotifier(),
            new NullGalaxyBrowseScopeProvider());
    }

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

    private static IReadOnlyList<GalaxyObject> CreateObjects()
    {
        return
        [
            new GalaxyObject
            {
                GobjectId = 1,
                TagName = "Area1",
                ContainedName = "Area1",
                BrowseName = "Area1",
                IsArea = true,
                CategoryId = 13,
            },
            new GalaxyObject
            {
                GobjectId = 2,
                TagName = "Pump_001",
                ContainedName = "Pump",
                BrowseName = "Pump_001",
                ParentGobjectId = 1,
                CategoryId = 10,
                TemplateChain = { "$Pump", "$Base" },
            },
            new GalaxyObject
            {
                GobjectId = 3,
                TagName = "Valve_001",
                ContainedName = "Valve",
                BrowseName = "Valve_001",
                ParentGobjectId = 1,
                CategoryId = 11,
                TemplateChain = { "$Valve" },
            },
        ];
    }

    private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
    {
        /// <summary>Gets the fixed cache entry returned to callers.</summary>
        public GalaxyHierarchyCacheEntry Current { get; } = current;

        /// <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;
    }

}
