// 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;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Configurations;
using Microsoft.Testing.Platform.Extensions.TestFramework;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Services;

namespace Microsoft.Testing.Extensions;

internal readonly record struct ReportEngineContext(
    IFileSystem FileSystem,
    ITestApplicationModuleInfo TestApplicationModuleInfo,
    IEnvironment Environment,
    ICommandLineOptions CommandLineOptions,
    IConfiguration Configuration,
    IClock Clock,
    ITestFramework TestFramework,
    DateTimeOffset TestStartTime,
    int ExitCode,
    CancellationToken CancellationToken);

/// <summary>
/// Shared base class for report engine implementations (CTRF, JUnit, HTML, ...) that all consume
/// the same set of platform services (file system, configuration, clock, ...) and need a common
/// way to extract the user-provided report file name from a parsed command-line argument list.
/// </summary>
internal abstract class ReportEngineBase
{
#pragma warning disable SA1401 // Fields should be private - intentional: derived report engines access these directly through their partial files.
    protected readonly IFileSystem _fileSystem;
    protected readonly ITestApplicationModuleInfo _testApplicationModuleInfo;
    protected readonly IEnvironment _environment;
    protected readonly ICommandLineOptions _commandLineOptions;
    protected readonly IConfiguration _configuration;
    protected readonly IClock _clock;
    protected readonly ITestFramework _testFramework;
    protected readonly DateTimeOffset _testStartTime;
    protected readonly int _exitCode;
    protected readonly CancellationToken _cancellationToken;
#pragma warning restore SA1401

    protected ReportEngineBase(ReportEngineContext context)
    {
        _fileSystem = context.FileSystem;
        _testApplicationModuleInfo = context.TestApplicationModuleInfo;
        _environment = context.Environment;
        _commandLineOptions = context.CommandLineOptions;
        _configuration = context.Configuration;
        _clock = context.Clock;
        _testFramework = context.TestFramework;
        _testStartTime = context.TestStartTime;
        _exitCode = context.ExitCode;
        _cancellationToken = context.CancellationToken;
    }

    internal static string GetProvidedFileName(string[]? providedFileName)
        => providedFileName is { Length: > 0 }
            ? providedFileName[0]
            : throw ApplicationStateGuard.Unreachable();

    protected static string ReplaceInvalidFileNameChars(string fileName)
        => ReportFileNameSanitizer.ReplaceInvalidFileNameChars(fileName);

    protected string ResolveProvidedFileName(string template)
    {
        string processName = Path.GetFileNameWithoutExtension(_testApplicationModuleInfo.GetCurrentTestApplicationFullPath());
        string processId = _environment.ProcessId.ToString(CultureInfo.InvariantCulture);
        return ReportFileNameHelper.ResolveAndSanitize(template, processName, processId, _clock.UtcNow);
    }

    protected string BuildDefaultFileName(string extension)
        => BuildDefaultFileName(_testApplicationModuleInfo.GetCurrentTestApplicationFullPath(), extension);

    internal static string BuildDefaultFileName(string testApplicationModule, string extension)
    {
        // Deterministic <asm>_<tfm>_<arch>.<extension> shape — discoverable across
        // reruns and multi-target/multi-arch matrices. A second run into the same
        // TestResults folder overwrites the previous file (with a warning), matching
        // the behavior of an explicitly-provided file name.
        string moduleName = Path.GetFileNameWithoutExtension(testApplicationModule);
        string targetFrameworkMoniker = TargetFrameworkMonikerHelper.GetTargetFrameworkMonikerIncludingPlatform();
        string architecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
        string raw = $"{moduleName}_{targetFrameworkMoniker}_{architecture}.{extension}";
        return ReplaceInvalidFileNameChars(raw);
    }

    protected (string FinalPath, bool WasExplicit) ResolveOutputPath(string fileNameOptionName, string extension)
        => ResolveOutputPath(fileNameOptionName, () => BuildDefaultFileName(extension));

    protected (string FinalPath, bool WasExplicit) ResolveOutputPath(string fileNameOptionName, Func<string> defaultFileNameFactory)
    {
        _cancellationToken.ThrowIfCancellationRequested();

        bool wasExplicit = _commandLineOptions.TryGetOptionArgumentList(fileNameOptionName, out string[]? providedFileName);
        string fileName = wasExplicit
            ? ResolveProvidedFileName(GetProvidedFileName(providedFileName))
            : defaultFileNameFactory();

        string outputDirectory = _configuration.GetTestResultDirectory();
        // Path.Combine short-circuits when the second argument is rooted, so an absolute
        // user-provided file name overrides the test results directory while validated
        // relative paths stay nested under it.
        string finalPath = Path.Combine(outputDirectory, fileName);
        string? finalDirectory = Path.GetDirectoryName(finalPath);
        if (!RoslynString.IsNullOrEmpty(finalDirectory))
        {
            _fileSystem.CreateDirectory(finalDirectory);
        }

        return (finalPath, wasExplicit);
    }

    protected async Task WriteBytesAsync(string path, FileMode mode, byte[] bytes)
    {
        // Note that we need to dispose the IFileStream, not the inner stream.
        // IFileStream implementations will be responsible to dispose their inner stream.
        using IFileStream stream = _fileSystem.NewFileStream(path, mode);
#if NETCOREAPP
        await stream.Stream.WriteAsync(bytes.AsMemory(), _cancellationToken).ConfigureAwait(false);
#else
        await stream.Stream.WriteAsync(bytes, 0, bytes.Length, _cancellationToken).ConfigureAwait(false);
#endif
    }
}
