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

using System.Collections.Immutable;

using DiscoveryAndExecutionTests.Utilities;

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;

using ITestResultRecorder = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestResultRecorder;
using TestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult;

namespace Microsoft.MSTestV2.CLIAutomation;

public abstract partial class CLITestBase
{
    private static readonly SemaphoreSlim ExecutionLock = new(1, 1);

    internal static ImmutableArray<TestCase> DiscoverTests(string assemblyPath, string? testCaseFilter = null)
    {
        ExecutionLock.Wait();
        string currentDirectory = Environment.CurrentDirectory;
        try
        {
            var unitTestDiscoverer = new UnitTestDiscoverer(new TestSourceHandler());
            var logger = new InternalLogger();
            var sink = new InternalSink();

            string runSettingsXml = GetRunSettingsXml(string.Empty);
            var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter);

            unitTestDiscoverer.DiscoverTestsInSourceAsync(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), runSettingsXml, new TestElementFilterProvider(context), false).GetAwaiter().GetResult();

            return sink.DiscoveredTests;
        }
        finally
        {
            Environment.CurrentDirectory = currentDirectory;
            ExecutionLock.Release();
        }
    }

    internal static async Task<ImmutableArray<TestResult>> RunTestsAsync(IEnumerable<TestCase> testCases)
    {
        await ExecutionLock.WaitAsync();
        string currentDirectory = Environment.CurrentDirectory;
        try
        {
            var testExecutionManager = new TestExecutionManager();
            var frameworkHandle = new InternalFrameworkHandle();

            ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings);
            await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(null, null), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, filterProvider: null, false);
            return frameworkHandle.GetFlattenedTestResults();
        }
        finally
        {
            Environment.CurrentDirectory = currentDirectory;
            ExecutionLock.Release();
        }
    }

    internal static async Task<ImmutableArray<TestResult>> RunTestsAsync(IEnumerable<TestCase> testCases, string? testCaseFilter)
    {
        await ExecutionLock.WaitAsync();
        string currentDirectory = Environment.CurrentDirectory;
        try
        {
            var testExecutionManager = new TestExecutionManager();
            var frameworkHandle = new InternalFrameworkHandle();

            string runSettingsXml = GetRunSettingsXml(string.Empty);
            var runContext = new InternalRunContext(runSettingsXml, testCaseFilter);

            ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings);
            await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(runContext.TestRunDirectory, runContext.RunSettings?.SettingsXml), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), false);
            return frameworkHandle.GetFlattenedTestResults();
        }
        finally
        {
            Environment.CurrentDirectory = currentDirectory;
            ExecutionLock.Release();
        }
    }

    // Mirrors the adapter's execution boundary (see MSTestExecutor): each host test case becomes a neutral
    // UnitTestElement carrying its execution-context (TCM) properties and the originating test case as an
    // opaque recording handle, so results are recorded against the exact same TestCase instances.
    private static IEnumerable<UnitTestElement> ToUnitTestElements(IEnumerable<TestCase> testCases)
        => testCases.Select(static testCase =>
        {
            UnitTestElement element = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);
            element.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase);
            element.HostRecordingHandle = testCase;
            return element;
        });

    #region Helper classes
    private class InternalLogger : IMessageLogger
    {
        public void SendMessage(TestMessageLevel testMessageLevel, string message) => Debug.WriteLine($"{testMessageLevel}: {message}");
    }

    private sealed class InternalSink : ITestCaseDiscoverySink
    {
        private readonly List<TestCase> _testCases = [];

        public ImmutableArray<TestCase> DiscoveredTests => [.. _testCases];

        public void SendTestCase(TestCase discoveredTest) => _testCases.Add(discoveredTest);
    }

    private class InternalDiscoveryContext : IDiscoveryContext
    {
        private readonly ITestCaseFilterExpression? _filter;

        public InternalDiscoveryContext(string runSettings, string? testCaseFilter)
        {
            RunSettings = new InternalRunSettings(runSettings);

            if (testCaseFilter != null)
            {
                _filter = TestCaseFilterFactory.ParseTestFilter(testCaseFilter);
            }
        }

        public IRunSettings? RunSettings { get; }

        public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable<string> supportedProperties, Func<string, TestProperty> propertyProvider) => _filter;
    }

    private sealed class InternalRunContext : IRunContext
    {
        private readonly ITestCaseFilterExpression? _filter;

        public InternalRunContext(string runSettings, string? testCaseFilter)
        {
            RunSettings = new InternalRunSettings(runSettings);

            if (testCaseFilter != null)
            {
                _filter = TestCaseFilterFactory.ParseTestFilter(testCaseFilter);
            }
        }

        public IRunSettings? RunSettings { get; }

        public bool KeepAlive => false;

        public bool InIsolation => false;

        public bool IsDataCollectionEnabled => false;

        public bool IsBeingDebugged => false;

        public string? TestRunDirectory => null;

        public string? SolutionDirectory => null;

        public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable<string>? supportedProperties, Func<string, TestProperty?> propertyProvider) => _filter;
    }

    private sealed class InternalRunSettings : IRunSettings
    {
        public InternalRunSettings(string runSettings) => SettingsXml = runSettings;

        public string SettingsXml { get; }

        public ISettingsProvider? GetSettings(string? settingsName) => throw new NotImplementedException();
    }

    private sealed class InternalFrameworkHandle : IFrameworkHandle
    {
        private readonly List<string> _messageList = [];
        private readonly ConcurrentDictionary<TestCase, ConcurrentBag<TestResult>> _testResults = new();

        public bool EnableShutdownAfterTestRun { get; set; }

        public void RecordStart(TestCase testCase)
        {
        }

        public void RecordEnd(TestCase testCase, TestOutcome outcome)
        {
        }

        public void RecordResult(TestResult testResult)
        {
            Assert.IsNotNull(testResult, "Test result should not be null.");
            _testResults.GetOrAdd(testResult.TestCase, _ => []).Add(testResult);
        }

        public ImmutableArray<TestResult> GetFlattenedTestResults()
        {
            var allTestResults = _testResults.SelectMany(i => i.Value).ToImmutableArray();
            CollectionAssert.AllItemsAreNotNull(allTestResults, "All test results should be non-null.");

            return allTestResults;
        }

        public void RecordAttachments(IList<AttachmentSet> attachmentSets)
            => throw new NotImplementedException();

        public void SendMessage(TestMessageLevel testMessageLevel, string message) => _messageList.Add($"{testMessageLevel}:{message}");

        public int LaunchProcessWithDebuggerAttached(string filePath, string? workingDirectory, string? arguments, IDictionary<string, string?>? environmentVariables)
            => throw new NotImplementedException();
    }
    #endregion
}
