// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Extensions.VSTestBridge.CommandLine;
using Microsoft.Testing.Extensions.VSTestBridge.Resources;
using Microsoft.Testing.Platform;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Requests;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;

namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;

internal abstract class ContextAdapterBase
{
    private const string EmptyUidFilterExpression = "FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__";

    protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter)
        : this(commandLineOptions, runSettings, filter, useFullyQualifiedNameAsUid: false)
    {
    }

    protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter, bool useFullyQualifiedNameAsUid)
    {
        RunSettings = runSettings;

        RoslynDebug.Assert(runSettings.SettingsXml is not null);

        string? filterFromRunsettings = XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("TestCaseFilter")?.Value;
        string? filterFromCommandLineOption = null;
        if (commandLineOptions.TryGetOptionArgumentList(
            TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName,
            out string[]? filterExpressions)
            && filterExpressions is not null
            && filterExpressions.Length == 1)
        {
            filterFromCommandLineOption = filterExpressions[0];
        }

        HandleFilter(filter, filterFromRunsettings, filterFromCommandLineOption, useFullyQualifiedNameAsUid);
    }

    public IRunSettings? RunSettings { get; }

    private FilterExpressionWrapper? FilterExpressionWrapper { get; set; }

    private sealed class BridgeFilterExpression : ITestCaseFilterExpression
    {
        private readonly TestCaseFilterExpression _testCaseFilterExpression;

        public BridgeFilterExpression(TestCaseFilterExpression testCaseFilterExpression)
            => _testCaseFilterExpression = testCaseFilterExpression;

        public string TestCaseFilterValue
            => _testCaseFilterExpression.TestCaseFilterValue;

        public bool MatchTestCase(TestCase testCase, Func<string, object?> propertyValueProvider)
            => _testCaseFilterExpression.MatchTestCase(propertyValueProvider);
    }

    // NOTE: Implementation is borrowed from VSTest
    // MSTest relies on this method existing and access it through reflection: https://github.com/microsoft/testfx/blob/main/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs#L115
#pragma warning disable IDE0060 // Remove unused parameter - used via refl.
    public ITestCaseFilterExpression? GetTestCaseFilter(
        IEnumerable<string>? supportedProperties,
        Func<string, TestProperty?> propertyProvider)
#pragma warning restore IDE0060 // Remove unused parameter
    {
        if (FilterExpressionWrapper is null)
        {
            return null;
        }

        if (!RoslynString.IsNullOrEmpty(FilterExpressionWrapper.ParseError))
        {
            throw new TestPlatformFormatException(FilterExpressionWrapper.ParseError, FilterExpressionWrapper.FilterString);
        }

        // MSTest relies on ITestCaseFilterExpression from VSTest object model.
        // The filter source package doesn't bring object model as very intended design.
        // We create our own BridgeFilterExpression here that just wraps the type from the source-only package
        return new BridgeFilterExpression(new TestCaseFilterExpression(FilterExpressionWrapper));
    }

    private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunsettings, string? filterFromCommandLineOption, bool useFullyQualifiedNameAsUid)
    {
        // No filters at all, we can return immediately as there is nothing to do.
        if (filter is null or NopFilter
            && filterFromRunsettings is null
            && filterFromCommandLineOption is null)
        {
            return;
        }

        var filterBuilder = new StringBuilder();

        AppendFilter(filterFromRunsettings, filterBuilder);
        AppendFilter(filterFromCommandLineOption, filterBuilder);

        if (filter is not null)
        {
            foreach (ITestExecutionFilter leafFilter in TestExecutionFilterComposer.GetLeafFilters(filter))
            {
                switch (leafFilter)
                {
                    case NopFilter:
                        break;

                    case TestNodeUidListFilter testNodeUidListFilter:
                        StartFilter(filterBuilder);
                        if (testNodeUidListFilter.TestNodeUids.Length == 0)
                        {
                            filterBuilder.Append(EmptyUidFilterExpression);
                        }
                        else
                        {
                            BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder, useFullyQualifiedNameAsUid);
                        }

                        EndFilter(filterBuilder);
                        break;

                    default:
                        throw new NotSupportedException(
                            string.Format(
                                CultureInfo.InvariantCulture,
                                ExtensionResources.UnsupportedTestExecutionFilter,
                                leafFilter.GetType().FullName,
                                "VSTestBridge"));
                }
            }
        }

        if (filterBuilder.Length > 0)
        {
            FilterExpressionWrapper = new FilterExpressionWrapper(filterBuilder.ToString());
        }

        // Local functions
        static void AppendFilter(string? filter, StringBuilder builder)
        {
            if (filter is null)
            {
                return;
            }

            StartFilter(builder);
            builder.Append(filter);
            EndFilter(builder);
        }

        static void StartFilter(StringBuilder builder)
        {
            if (builder.Length > 0)
            {
                builder.Append(" & (");
            }
            else
            {
                builder.Append('(');
            }
        }

        static void EndFilter(StringBuilder builder)
            => builder.Append(')');
    }

    // The UID value is produced by ObjectModelConverters.ToTestNode, which sets it to either
    // TestCase.FullyQualifiedName or TestCase.Id depending on useFullyQualifiedNameAsUid. We use that
    // same discriminator here to decide the clause type, rather than guessing from the value with
    // Guid.TryParse. Guessing is wrong for a FullyQualifiedName that happens to be GUID-shaped (e.g. a
    // data-driven test whose display name is exactly a GUID string), which would then be emitted as an
    // Id= clause and select the wrong test (or no test).
    private static void BuildFilter(TestNodeUid[] testNodesUid, StringBuilder filter, bool useFullyQualifiedNameAsUid)
    {
        for (int i = 0; i < testNodesUid.Length; i++)
        {
            if (i > 0)
            {
                filter.Append('|');
            }

            if (!useFullyQualifiedNameAsUid && Guid.TryParse(testNodesUid[i].Value, out Guid guid))
            {
                filter.Append("Id=");
                filter.Append(guid.ToString());
                continue;
            }

            TestNodeUid currentTestNodeUid = testNodesUid[i];
            filter.Append("FullyQualifiedName=");

            // Use VSTest's canonical escaper rather than a hand-rolled loop. It escapes every filter
            // operator ('\', '(', ')', '&', '|', '=', '!', '~') unconditionally, which is exactly what
            // we need for a raw (un-escaped) test-node UID.
            filter.Append(FilterHelper.Escape(currentTestNodeUid.Value));
        }
    }
}
