// 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.AzureDevOpsReport.Resources;
using Microsoft.Testing.Extensions.Reporting;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.OutputDevice;
using Microsoft.Testing.Platform.Extensions.TestHost;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.OutputDevice;
using Microsoft.Testing.Platform.Services;

namespace Microsoft.Testing.Extensions.AzureDevOpsReport;

/// <summary>
/// Wraps each test assembly's output in an Azure DevOps log group (<c>##[group]</c> /
/// <c>##[endgroup]</c>) so the raw pipeline log view collapses the assembly's test output by
/// default. Unlike <c>##vso[task.logdetail]</c> records, the <c>##[group]</c> format commands are
/// rendered by the modern Azure DevOps Pipelines log viewer.
/// </summary>
/// <remarks>
/// This handler also implements <see cref="IDataConsumer"/> (with a no-op
/// <see cref="ConsumeAsync(IDataProducer, IData, CancellationToken)"/>) purely so that, at session
/// end, <c>CommonTestHost.NotifyTestSessionEndAsync</c> runs its
/// <see cref="OnTestSessionFinishingAsync(ITestSessionContext)"/> in the consumer phase — i.e.
/// after the producer-only AzDO handlers. Combined with registering it last, this ensures the
/// closing <c>##[endgroup]</c> is emitted after the other reporters' final <c>##vso[...]</c> lines,
/// so the group truly wraps the whole assembly's output.
/// </remarks>
internal sealed class AzureDevOpsLogGroupReporter : IDataConsumer, ITestSessionLifetimeHandler, IOutputDeviceDataProducer
{
    private readonly ICommandLineOptions _commandLineOptions;
    private readonly IEnvironment _environment;
    private readonly IOutputDevice _outputDevice;
    private readonly ITestApplicationModuleInfo _testApplicationModuleInfo;
    private readonly ILogger _logger;
    private readonly Lazy<string> _targetFrameworkMoniker;

    private bool _groupOpened;

    public AzureDevOpsLogGroupReporter(
        ICommandLineOptions commandLineOptions,
        IEnvironment environment,
        IOutputDevice outputDevice,
        ITestApplicationModuleInfo testApplicationModuleInfo,
        ILoggerFactory loggerFactory)
    {
        _commandLineOptions = commandLineOptions;
        _environment = environment;
        _outputDevice = outputDevice;
        _testApplicationModuleInfo = testApplicationModuleInfo;
        _logger = loggerFactory.CreateLogger<AzureDevOpsLogGroupReporter>();
        _targetFrameworkMoniker = new(TargetFrameworkMonikerHelper.GetTargetFrameworkMonikerIncludingPlatform);
    }

    public string Uid => nameof(AzureDevOpsLogGroupReporter);

    public string Version => ExtensionVersion.DefaultSemVer;

    public string DisplayName => AzureDevOpsResources.DisplayName;

    public string Description => AzureDevOpsResources.Description;

    public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage)];

    public Task<bool> IsEnabledAsync()
        => Task.FromResult(
            _commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName)
            && AzureDevOpsConstants.IsRunningInAzureDevOps(_environment)
            && _commandLineOptions.TryGetOptionArgumentList(
                AzureDevOpsCommandLineOptions.AzureDevOpsGroups,
                out string[]? groupsArguments)
            && groupsArguments is [string groupsValue]
            && string.Equals(groupsValue, AzureDevOpsCommandLineOptions.OptionOn, StringComparison.OrdinalIgnoreCase));

    // No-op: this consumer subscribes to data only to be ordered in the consumer phase at session
    // end (see the type-level remarks). It does not act on individual messages.
    public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
        => Task.CompletedTask;

    public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext)
    {
        try
        {
            testSessionContext.CancellationToken.ThrowIfCancellationRequested();

            string name = $"{_testApplicationModuleInfo.TryGetAssemblyName() ?? "unknown"} ({_targetFrameworkMoniker.Value})";
            string line = $"##[group]{AzDoEscaper.Escape(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.LogGroupHeader, name))}";
            await _outputDevice.DisplayAsync(this, new AzureDevOpsCommandOutputDeviceData(line), testSessionContext.CancellationToken).ConfigureAwait(false);
            _groupOpened = true;
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogUnexpectedException(nameof(OnTestSessionStartingAsync), ex);
        }
    }

    public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext)
    {
        try
        {
            testSessionContext.CancellationToken.ThrowIfCancellationRequested();

            if (!_groupOpened)
            {
                return;
            }

            await _outputDevice.DisplayAsync(this, new AzureDevOpsCommandOutputDeviceData("##[endgroup]"), testSessionContext.CancellationToken).ConfigureAwait(false);
            _groupOpened = false;
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogUnexpectedException(nameof(OnTestSessionFinishingAsync), ex);
        }
    }
}
