using ZB.MOM.WW.MxGateway.Contracts;

namespace ZB.MOM.WW.MxGateway.IntegrationTests;

public static class IntegrationTestEnvironment
{
    /// <summary>
    /// Sourced from <see cref="GatewayContractInfo.LiveMxAccessOptInVariableName"/>
    /// so the env-var literal is shared with
    /// <c>ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute</c>.
    /// </summary>
    public const string LiveMxAccessVariableName = GatewayContractInfo.LiveMxAccessOptInVariableName;
    public const string LiveMxAccessWorkerExecutableVariableName = "MXGATEWAY_LIVE_MXACCESS_WORKER_EXE";
    public const string LiveMxAccessItemVariableName = "MXGATEWAY_LIVE_MXACCESS_ITEM";
    public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
    public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";

    /// <summary>Gets whether live MXAccess tests are enabled.</summary>
    public static bool LiveMxAccessTestsEnabled => IsEnabled(LiveMxAccessVariableName);

    /// <summary>
    /// Gets whether an opt-in live-test suite is enabled, by comparing the named
    /// environment variable to <c>1</c>. Shared by every <c>Live*FactAttribute</c>
    /// so the opt-in check has a single implementation.
    /// </summary>
    /// <param name="variableName">The environment variable that gates the suite.</param>
    /// <returns><see langword="true"/> when the variable is exactly <c>1</c>.</returns>
    public static bool IsEnabled(string variableName) =>
        string.Equals(
            Environment.GetEnvironmentVariable(variableName),
            "1",
            StringComparison.Ordinal);

    /// <summary>Gets the MXAccess item name for live tests.</summary>
    public static string LiveMxAccessItem =>
        GetOptionalEnvironmentVariable(
            LiveMxAccessItemVariableName,
            "TestChildObject.TestInt");

    /// <summary>Gets the client name for live tests.</summary>
    public static string LiveMxAccessClientName =>
        GetOptionalEnvironmentVariable(
            LiveMxAccessClientNameVariableName,
            "ZB.MOM.WW.MxGateway.IntegrationTests");

    /// <summary>Gets the timeout for waiting on events in live tests.</summary>
    public static TimeSpan LiveMxAccessEventTimeout =>
        TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
            LiveMxAccessEventTimeoutSecondsVariableName,
            defaultValue: 15));

    /// <summary>Resolves the path to the worker executable for live tests.</summary>
    /// <returns>Path to ZB.MOM.WW.MxGateway.Worker.exe.</returns>
    public static string ResolveLiveMxAccessWorkerExecutablePath()
    {
        string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
        if (!string.IsNullOrWhiteSpace(configuredPath))
        {
            return Path.GetFullPath(configuredPath);
        }

        string repositoryRoot = ResolveRepositoryRoot(AppContext.BaseDirectory);
        string[] candidatePaths =
        [
            Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Debug", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
            Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "Debug", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
            Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Release", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
            Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "Release", "net48", "ZB.MOM.WW.MxGateway.Worker.exe"),
            Path.Combine(repositoryRoot, "src", "ZB.MOM.WW.MxGateway.Worker", "bin", "x86", "Release", "ZB.MOM.WW.MxGateway.Worker.exe"),
        ];

        return candidatePaths.FirstOrDefault(File.Exists)
            ?? candidatePaths[0];
    }

    private static string GetOptionalEnvironmentVariable(
        string name,
        string defaultValue)
    {
        string? value = Environment.GetEnvironmentVariable(name);
        return string.IsNullOrWhiteSpace(value)
            ? defaultValue
            : value;
    }

    private static int GetPositiveIntegerEnvironmentVariable(
        string name,
        int defaultValue)
    {
        string? value = Environment.GetEnvironmentVariable(name);
        if (int.TryParse(value, out int parsed) && parsed > 0)
        {
            return parsed;
        }

        return defaultValue;
    }

    /// <summary>
    /// Resolves the root directory of the repository by walking parents for a
    /// <c>src/</c> directory next to either a <c>.git</c> marker or a
    /// <c>.sln</c>/<c>.slnx</c> file. Throws <see cref="InvalidOperationException"/>
    /// when no root is found so a misconfigured run fails fast with an actionable
    /// message rather than silently falling back to the current working directory
    /// (which previously produced a misleading "worker exe not found" pointing at
    /// a fabricated path). The
    /// <see cref="LiveMxAccessWorkerExecutableVariableName"/> environment variable
    /// remains the escape hatch for unusual deployments.
    /// </summary>
    /// <param name="startDirectory">Starting directory to search from.</param>
    /// <param name="stopBoundary">
    /// Optional upper bound for the parent walk. When supplied, the walker checks
    /// <paramref name="stopBoundary"/> for markers and then stops, ignoring all
    /// ancestors above it. Tests pass an isolated boundary so the walker cannot
    /// leak into ambient ancestors (a redirected <c>TMP</c>, a co-located checkout
    /// at <c>C:\src</c>, an enclosing CI workspace, etc.) that would silently
    /// satisfy <see cref="IsRepositoryRoot"/>.
    /// Production callers pass <see langword="null"/> so the walk continues to the
    /// drive root as before.
    /// </param>
    /// <returns>The repository root path.</returns>
    /// <exception cref="InvalidOperationException">
    /// Thrown when the parent walk exhausts without finding a repository root.
    /// </exception>
    internal static string ResolveRepositoryRoot(string startDirectory, string? stopBoundary = null)
    {
        string? normalizedBoundary = stopBoundary is null
            ? null
            : Path.TrimEndingDirectorySeparator(Path.GetFullPath(stopBoundary));

        DirectoryInfo? directory = new(startDirectory);
        while (directory is not null)
        {
            if (IsRepositoryRoot(directory))
            {
                return directory.FullName;
            }

            if (normalizedBoundary is not null
                && string.Equals(
                    Path.TrimEndingDirectorySeparator(directory.FullName),
                    normalizedBoundary,
                    StringComparison.OrdinalIgnoreCase))
            {
                break;
            }

            directory = directory.Parent;
        }

        throw new InvalidOperationException(
            $"Could not resolve repository root by walking parents of '{startDirectory}'. "
            + "Expected to find a directory containing a 'src' subdirectory next to either a '.git' marker "
            + "or a '*.sln' / '*.slnx' file in 'src'. "
            + $"Set the '{LiveMxAccessWorkerExecutableVariableName}' environment variable to bypass repository-root resolution.");
    }

    private static bool IsRepositoryRoot(DirectoryInfo directory)
    {
        string srcPath = Path.Combine(directory.FullName, "src");
        if (!Directory.Exists(srcPath))
        {
            return false;
        }

        // Accept a checked-out git repo OR an unpacked working tree that ships a
        // .sln/.slnx alongside src/. The .sln/.slnx fallback lets the integration
        // tests run in copies that have no .git folder (e.g. an extracted zip).
        if (Directory.Exists(Path.Combine(directory.FullName, ".git"))
            || File.Exists(Path.Combine(directory.FullName, ".git")))
        {
            return true;
        }

        return Directory.EnumerateFiles(srcPath, "*.slnx").Any()
            || Directory.EnumerateFiles(srcPath, "*.sln").Any();
    }
}
