// 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.Platform.Builder;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Configurations;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.CommandLine;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.OutputDevice;
using Microsoft.Testing.Platform.Resources;
using Microsoft.Testing.Platform.Services;
using Microsoft.Testing.Platform.Telemetry;
using Microsoft.Testing.Platform.TestHostControllers;
using Microsoft.Testing.Platform.Tools;

namespace Microsoft.Testing.Platform.Hosts;

internal sealed partial class TestHostBuilder
{
    private async Task<BuildContext> SetupCommonServicesAsync(
        ApplicationLoggingState loggingState,
        TestApplicationOptions testApplicationOptions,
        IUnhandledExceptionsHandler unhandledExceptionsHandler,
        DateTimeOffset createBuilderStart)
    {
        // ============= SETUP COMMON SERVICE USED IN ALL MODES ===============//
        SystemClock systemClock = new();
        DateTimeOffset buildBuilderStart = systemClock.UtcNow;
        Dictionary<string, object> builderMetrics = new()
        {
            [TelemetryProperties.HostProperties.CreateBuilderStart] = createBuilderStart,
            [TelemetryProperties.HostProperties.BuildBuilderStart] = buildBuilderStart,
        };
        SystemEnvironment systemEnvironment = new();
        ServiceProvider serviceProvider = new();
        TestHostControllerInfo testHostControllerInfo = new(loggingState.CommandLineParseResult);
        BuildContext context = new(
            loggingState,
            testApplicationOptions,
            unhandledExceptionsHandler,
            createBuilderStart,
            systemClock,
            buildBuilderStart,
            builderMetrics,
            systemEnvironment,
            serviceProvider,
            testHostControllerInfo);

        if (loggingState.FileLoggerProvider is not null)
        {
            serviceProvider.TryAddService(loggingState.FileLoggerProvider);
        }

        serviceProvider.TryAddService(loggingState);
        serviceProvider.TryAddService(systemEnvironment);
        SystemConsole systemConsole = new();
        serviceProvider.TryAddService(systemConsole);
        SystemTask systemTask = new();
        serviceProvider.TryAddService(systemTask);
        serviceProvider.TryAddService(_runtimeFeature);
        serviceProvider.TryAddService(_processHandler);
        serviceProvider.TryAddService(_fileSystem);
        serviceProvider.TryAddService(_testApplicationModuleInfo);
        serviceProvider.TryAddService(testHostControllerInfo);
        serviceProvider.TryAddService(systemClock);
        serviceProvider.TryAddService(new ArtifactNamingService(_testApplicationModuleInfo, systemEnvironment, systemClock));

        SystemMonitor systemMonitor = new();
        serviceProvider.TryAddService(systemMonitor);
        SystemMonitorAsyncFactory systemMonitorAsyncFactory = new();
        serviceProvider.TryAddService(systemMonitorAsyncFactory);
        context.SystemMonitorAsyncFactory = systemMonitorAsyncFactory;

        serviceProvider.AddService(new PlatformInformation());

        ILogger? logger = null;
        if (loggingState.FileLoggerProvider is not null)
        {
            logger = loggingState.FileLoggerProvider.CreateLogger(GetType().ToString());
            FileLoggerInformation fileLoggerInformation = new(loggingState.FileLoggerProvider.SyncFlush, new(loggingState.FileLoggerProvider.FileLogger.FileName), loggingState.LogLevel);
            serviceProvider.TryAddService(fileLoggerInformation);
        }

        if (logger is not null)
        {
            await logger.LogInformationAsync($"Setting RegisterEnvironmentVariablesConfigurationSource: '{testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource}'").ConfigureAwait(false);
        }

        if (testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource)
        {
            Configuration.AddConfigurationSource(() => new EnvironmentVariablesConfigurationSource(systemEnvironment));
        }

        // Issue #6349: expose the parsed CLI options through IConfiguration under the
        // "commandLineOptions" section so that the same lookup helpers consult both the CLI and
        // testconfig.json. Highest priority (Order=0) so the CLI always wins.
        Configuration.AddConfigurationSource(() => new CommandLineConfigurationSource());

        Configuration.AddConfigurationSource(() => new JsonConfigurationSource(_testApplicationModuleInfo, _fileSystem, loggingState.FileLoggerProvider));

        context.Configuration = (AggregatedConfiguration)await ((ConfigurationManager)Configuration).BuildAsync(loggingState.FileLoggerProvider, loggingState.CommandLineParseResult).ConfigureAwait(false);
        serviceProvider.TryAddService(context.Configuration);

#pragma warning disable CA1416 // Browser does not support the test host controller process model. The provider itself is marked [UnsupportedOSPlatform("browser")].
        if (!OperatingSystem.IsBrowser())
        {
            // Insert the built-in provider at the front so later user-supplied providers -
            // including the VSTest bridge runsettings provider - can still override these
            // unlocked values when both sources are present.
            if (TestHostControllers is TestHostControllersManager testHostControllersManager)
            {
                testHostControllersManager.AddEnvironmentVariableProviderFirst(sp => new TestConfigurationEnvironmentVariableProvider(sp.GetConfiguration()));
            }
            else
            {
                throw ApplicationStateGuard.Unreachable();
            }
        }
#pragma warning restore CA1416

        if (((TelemetryManager)Telemetry).BuildOTelProvider(serviceProvider) is { } otelService)
        {
            serviceProvider.AddService(otelService);

            // Nest the whole run under the trace context of whoever started this process (a CI pipeline step,
            // `dotnet test`, an IDE, or the test host controller) so the run is not an orphan trace.
            IPlatformOpenTelemetryService? platformOTelService = serviceProvider.GetServiceInternal<IPlatformOpenTelemetryService>();
            if (platformOTelService is not null)
            {
                // Set before creating any span so every span picks it up, including the ones created with an
                // explicit parent id (which do not inherit tracestate).
                platformOTelService.RootTraceState = EnvironmentTraceContext.TryGetTraceState(systemEnvironment);
                context.BuilderActivity = platformOTelService.StartActivity(
                    TestingPlatformSemanticConventions.Activities.TestHostBuilder,
                    parentId: EnvironmentTraceContext.TryGetParentId(systemEnvironment),
                    startTime: buildBuilderStart);
            }
        }

        _ = bool.TryParse(context.Configuration[PlatformConfigurationConstants.PlatformExitProcessOnUnhandledException], out bool isFileConfiguredToFailFast);

        string? environmentSetting = _environment.GetEnvironmentVariable(EnvironmentVariableConstants.TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION);
        bool? isEnvConfiguredToFailFast = null;
        if (environmentSetting is "1" or "0")
        {
            isEnvConfiguredToFailFast = environmentSetting == "1";
        }

        bool exitProcessOnUnhandledException = isEnvConfiguredToFailFast is not null ? environmentSetting == "1" : isFileConfiguredToFailFast;

        if (logger is not null)
        {
            await logger.LogInformationAsync($"Setting PlatformExitProcessOnUnhandledException: '{exitProcessOnUnhandledException}', config file: {isFileConfiguredToFailFast} environment variable: {isEnvConfiguredToFailFast}").ConfigureAwait(false);
        }

        if (exitProcessOnUnhandledException)
        {
            unhandledExceptionsHandler.Subscribe();
        }

        serviceProvider.AddService(new UnhandledExceptionsPolicy(fastFailOnFailure: exitProcessOnUnhandledException));

        context.TestApplicationCancellationTokenSource = new CTRLPlusCCancellationTokenSource(
            systemConsole,
            loggingState.FileLoggerProvider?.CreateLogger(nameof(CTRLPlusCCancellationTokenSource)));
        serviceProvider.AddService(context.TestApplicationCancellationTokenSource, throwIfSameInstanceExit: true);

        CommandLineOptionsProxy commandLineOptionsProxy = new();
        serviceProvider.TryAddService(commandLineOptionsProxy);

        LoggerFactoryProxy loggerFactoryProxy = new();
        serviceProvider.TryAddService(loggerFactoryProxy);

        context.CommandLineHandler = await ((CommandLineManager)CommandLine).BuildAsync(loggingState.CommandLineParseResult, serviceProvider, context.Configuration).ConfigureAwait(false);
        commandLineOptionsProxy.SetCommandLineOptions(context.CommandLineHandler);

        context.PoliciesService = new StopPoliciesService(context.TestApplicationCancellationTokenSource);
        serviceProvider.AddService(context.PoliciesService);

        bool hasServerFlag = context.CommandLineHandler.TryGetOptionArgumentList(PlatformCommandLineProvider.ServerOptionKey, out string[]? protocolName);
        context.IsJsonRpcProtocol = hasServerFlag && (protocolName is null
            || protocolName.Length == 0
            || protocolName[0].Equals(PlatformCommandLineProvider.JsonRpcProtocolName, StringComparison.OrdinalIgnoreCase));

        // Reuse the shared helper so the pipe-protocol detection stays in one place.
        bool isPipeProtocol = context.CommandLineHandler.HasDotnetTestServerOption();

        // Register the single coverage accumulator before the output device is built so the terminal
        // output device can read from it (rather than buffering its own copy of the coverage messages).
        serviceProvider.AddService(new TestCoverageCapabilities());
        serviceProvider.AddService(new TestCoverageResult(loggerFactoryProxy));

        context.ProxyOutputDevice = await _outputDisplay.BuildAsync(serviceProvider, context.IsJsonRpcProtocol, isPipeProtocol).ConfigureAwait(false);

        if (loggingState.FileLoggerProvider is not null)
        {
            Logging.AddProvider((_, _) => loggingState.FileLoggerProvider);
        }

        ICommandLineOptions commandLineOptions = serviceProvider.GetCommandLineOptions();
        context.LoggerFactory = await ((LoggingManager)Logging).BuildAsync(serviceProvider, loggingState.LogLevel, systemMonitor).ConfigureAwait(false);
        loggerFactoryProxy.SetLoggerFactory(context.LoggerFactory);

        if (await context.ProxyOutputDevice.OriginalOutputDevice.IsEnabledAsync().ConfigureAwait(false))
        {
            await context.ProxyOutputDevice.OriginalOutputDevice.TryInitializeAsync().ConfigureAwait(false);
        }

        serviceProvider.TryAddService(context.ProxyOutputDevice);
        serviceProvider.TryAddService(context.ProxyOutputDevice.OriginalOutputDevice);

        // Reports extensions/consumers that have not yet completed once the test-application
        // cancellation token is signalled. Registered after the output device so it can use it.
        ShutdownProgressReporter shutdownProgressReporter = new(
            context.TestApplicationCancellationTokenSource,
            context.ProxyOutputDevice,
            context.LoggerFactory,
            systemClock);
        serviceProvider.AddService(shutdownProgressReporter);

        context.TestFrameworkCapabilities = TestFramework!.TestFrameworkCapabilitiesFactory(serviceProvider);
        if (context.TestFrameworkCapabilities is IAsyncInitializableExtension testFrameworkCapabilitiesAsyncInitializable)
        {
            await testFrameworkCapabilitiesAsyncInitializable.InitializeAsync().ConfigureAwait(false);
        }

        serviceProvider.AddService(context.TestFrameworkCapabilities);

        IReadOnlyList<JsonCommandLineOptionEntry> jsonCommandLineOptions;
        try
        {
            // Normalize JSON-sourced scalar option entries to the indexed shape for arg-bearing
            // options before any consumer reads them. This makes "foo": "value" in testconfig.json
            // behave identically to "foo": ["value"], removing the foot-gun where a scalar like
            // "timeout": "true" was misinterpreted as a presence marker. See #6349/#8830.
            //
            // Defensive: option providers may register duplicate names; CommandLineOptionsValidator
            // catches that later (ValidateOptionsAreNotDuplicated) and reports a clear error. Skip
            // duplicates here so the normalization step itself doesn't crash for that malformed setup.
            var optionByName = new Dictionary<string, CommandLineOption>(StringComparer.OrdinalIgnoreCase);
            IEnumerable<ICommandLineOptionsProvider> extensionOptionsProviders =
                loggingState.CommandLineParseResult.ToolName is string toolName
                    ? context.CommandLineHandler.ExtensionsCommandLineOptionsProviders
                        .OfType<IToolCommandLineOptionsProvider>()
                        .Where(provider => provider.ToolName == toolName)
                    : context.CommandLineHandler.ExtensionsCommandLineOptionsProviders
                        .Where(provider => provider is not IToolCommandLineOptionsProvider);
            foreach (ICommandLineOptionsProvider optionsProvider in context.CommandLineHandler.SystemCommandLineOptionsProviders
                .Concat(extensionOptionsProviders))
            {
                foreach (CommandLineOption option in optionsProvider.GetCommandLineOptions())
                {
                    if (!optionByName.ContainsKey(option.Name))
                    {
                        optionByName.Add(option.Name, option);
                    }
                }
            }

            context.Configuration.NormalizeJsonCommandLineOptionScalars(optionByName);

            jsonCommandLineOptions = context.Configuration.EnumerateJsonCommandLineOptions();
        }
        catch (FormatException ex) when (!loggingState.CommandLineParseResult.HasTool)
        {
            await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);
            StringBuilder sb = new();
            sb.AppendLine(PlatformResources.InvalidCommandLineArguments);
            sb.AppendLine(CultureInfo.InvariantCulture, $"\t- {ex.Message}");
            await context.ProxyOutputDevice.DisplayAsync(context.CommandLineHandler, new ErrorMessageOutputDeviceData(sb.ToString().TrimEnd()), context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);
            await context.CommandLineHandler.PrintHelpAsync(context.ProxyOutputDevice, null, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);

            CompleteBuilderActivity(context.BuilderActivity, nameof(InformativeCommandLineHost));
            context.EarlyHost = new InformativeCommandLineHost((int)ExitCode.InvalidCommandLine, serviceProvider);
            return context;
        }
        catch (FormatException)
        {
            // A tool such as --info or --version is being invoked. Degrade gracefully by treating
            // the malformed testconfig.json as empty so the tool can still complete its job.
            jsonCommandLineOptions = [];
        }

        ValidationResult commandLineValidationResult = await CommandLineOptionsValidator.ValidateAsync(
            loggingState.CommandLineParseResult,
            context.CommandLineHandler.SystemCommandLineOptionsProviders,
            context.CommandLineHandler.ExtensionsCommandLineOptionsProviders,
            context.CommandLineHandler,
            jsonCommandLineOptions).ConfigureAwait(false);

        if (!commandLineValidationResult.IsValid)
        {
            await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);
            await context.ProxyOutputDevice.DisplayAsync(context.CommandLineHandler, new ErrorMessageOutputDeviceData(commandLineValidationResult.ErrorMessage), context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);
            await context.CommandLineHandler.PrintHelpAsync(context.ProxyOutputDevice, null, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);

            CompleteBuilderActivity(context.BuilderActivity, nameof(InformativeCommandLineHost));
            context.EarlyHost = new InformativeCommandLineHost((int)ExitCode.InvalidCommandLine, serviceProvider);
            return context;
        }

        serviceProvider.TryAddService(context.CommandLineHandler);

        if (commandLineOptions.IsOptionSet(PlatformCommandLineProvider.TimeoutOptionKey)
            && commandLineOptions.TryGetOptionArgumentList(PlatformCommandLineProvider.TimeoutOptionKey, out string[]? args))
        {
            if (!TimeSpanParser.TryParseRequireSuffix(args[0], out TimeSpan timeout))
            {
                throw ApplicationStateGuard.Unreachable();
            }

            context.TestApplicationCancellationTokenSource.CancelAfter(timeout);
        }

        context.IsHelpCommand = context.CommandLineHandler.IsHelpInvoked();
        context.IsInfoCommand = context.CommandLineHandler.IsInfoInvoked();

        if (!context.IsHelpCommand && !context.IsInfoCommand)
        {
            await context.Configuration.CheckTestResultsDirectoryOverrideAndCreateItAsync(loggingState.FileLoggerProvider).ConfigureAwait(false);
        }

        await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);

        ITelemetryCollector telemetryService = await ((TelemetryManager)Telemetry).BuildTelemetryAsync(serviceProvider, context.LoggerFactory, testApplicationOptions).ConfigureAwait(false);
        serviceProvider.TryAddService(telemetryService);

        AddApplicationTelemetryMetadata(serviceProvider, builderMetrics);

        if (commandLineOptions.IsOptionSet(PlatformCommandLineProvider.ExitOnProcessExitOptionKey))
        {
            if (OperatingSystem.IsBrowser())
            {
                throw new PlatformNotSupportedException(PlatformResources.PlatformCommandLineExitOnProcessExitNotSupportedInBrowser);
            }

            serviceProvider.AddService(new NonCooperativeParentProcessListener(commandLineOptions, _environment));
        }

        serviceProvider.AddService(new TestApplicationResult(
            context.ProxyOutputDevice,
            serviceProvider.GetCommandLineOptions(),
            serviceProvider.GetEnvironment(),
            context.PoliciesService,
            serviceProvider.GetPlatformOTelService(),
            serviceProvider.GetRequiredService<ITestCoverageResult>()));

        ChatClientManager.BuildChatClients(serviceProvider);

        return context;
    }
}
