using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Metrics;
using ZB.MOM.WW.MxGateway.Server.Workers;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Workers;

/// <summary>
///     Per <c>gateway.md</c> the gateway must terminate
///     orphaned worker processes on startup. These tests pin that the terminator
///     kills leftover workers (matched by executable path, or by image name when
///     the path is unreadable) without touching unrelated processes or itself.
/// </summary>
public sealed class OrphanWorkerTerminatorTests
{
    // An OS-appropriate, already-normalized absolute path so Path.GetFullPath (used by the
    // terminator to resolve the configured path) is idempotent and the exact-match compare holds
    // on every platform. A hard-coded Windows literal (C:\...) is not rooted on Linux, so
    // Path.GetFullPath there prepends the cwd and the match fails — the terminator is a
    // Windows-only feature in production, but its matching logic must still be testable on the
    // Linux CI runner.
    private static readonly string WorkerExecutablePath = Path.GetFullPath(Path.Combine(
        Path.GetTempPath(),
        "MxGatewayOrphanTests",
        "bin",
        "x86",
        "Release",
        "ZB.MOM.WW.MxGateway.Worker.exe"));

    /// <summary>Verifies that orphan worker processes matching the configured executable path are killed.</summary>
    [Fact]
    public void TerminateOrphans_KillsWorkerProcessesMatchingConfiguredExecutablePath()
    {
        FakeProcessInspector inspector = new(
        [
            new RunningProcessInfo(101, WorkerExecutablePath),
            new RunningProcessInfo(102, WorkerExecutablePath),
        ]);
        OrphanWorkerTerminator terminator = CreateTerminator(inspector);

        int killed = terminator.TerminateOrphans();

        Assert.Equal(2, killed);
        Assert.Equal([101, 102], inspector.KilledProcessIds.Order());
    }

    /// <summary>Verifies that orphan workers are killed when executable path is unreadable but image name matches.</summary>
    [Fact]
    public void TerminateOrphans_KillsImageNameMatchWhenExecutablePathUnreadable()
    {
        // The x64 gateway cannot introspect the x86 worker's main module, so the
        // path comes back null. Image-name match is the only signal — and it is
        // exactly the orphan worker case, so the process must still be killed.
        FakeProcessInspector inspector = new(
        [
            new RunningProcessInfo(201, ExecutablePath: null),
        ]);
        OrphanWorkerTerminator terminator = CreateTerminator(inspector);

        int killed = terminator.TerminateOrphans();

        Assert.Equal(1, killed);
        Assert.Equal([201], inspector.KilledProcessIds);
    }

    /// <summary>Verifies that unrelated processes with the same image name are not killed.</summary>
    [Fact]
    public void TerminateOrphans_DoesNotKillUnrelatedProcessSharingImageName()
    {
        // A process with the same image name but a different executable path is
        // not our worker and must be left alone.
        FakeProcessInspector inspector = new(
        [
            new RunningProcessInfo(301, Path.GetFullPath(Path.Combine(
                Path.GetTempPath(),
                "MxGatewayOrphanTests-other",
                "ZB.MOM.WW.MxGateway.Worker.exe"))),
        ]);
        OrphanWorkerTerminator terminator = CreateTerminator(inspector);

        int killed = terminator.TerminateOrphans();

        Assert.Equal(0, killed);
        Assert.Empty(inspector.KilledProcessIds);
    }

    /// <summary>Verifies that the current process is not killed even if path matches.</summary>
    [Fact]
    public void TerminateOrphans_DoesNotKillCurrentProcess()
    {
        FakeProcessInspector inspector = new(
        [
            new RunningProcessInfo(Environment.ProcessId, WorkerExecutablePath),
        ]);
        OrphanWorkerTerminator terminator = CreateTerminator(inspector);

        int killed = terminator.TerminateOrphans();

        Assert.Equal(0, killed);
        Assert.Empty(inspector.KilledProcessIds);
    }

    /// <summary>Verifies that termination continues when one process kill fails.</summary>
    [Fact]
    public void TerminateOrphans_ContinuesWhenOneKillThrows()
    {
        FakeProcessInspector inspector = new(
        [
            new RunningProcessInfo(401, WorkerExecutablePath),
            new RunningProcessInfo(402, WorkerExecutablePath),
        ])
        {
            ThrowOnKillProcessId = 401,
        };
        OrphanWorkerTerminator terminator = CreateTerminator(inspector);

        int killed = terminator.TerminateOrphans();

        Assert.Equal(1, killed);
        Assert.Contains(402, inspector.KilledProcessIds);
    }

    private static OrphanWorkerTerminator CreateTerminator(IRunningProcessInspector inspector)
    {
        GatewayOptions options = new()
        {
            Worker = new WorkerOptions
            {
                ExecutablePath = WorkerExecutablePath,
            },
        };
        return new OrphanWorkerTerminator(
            Options.Create(options),
            inspector,
            new GatewayMetrics());
    }

    private sealed class FakeProcessInspector(IReadOnlyList<RunningProcessInfo> processes)
        : IRunningProcessInspector
    {
        /// <summary>Gets the list of killed process IDs.</summary>
        public List<int> KilledProcessIds { get; } = [];

        /// <summary>Gets or sets the process ID that should throw when killed.</summary>
        public int? ThrowOnKillProcessId { get; init; }

        /// <inheritdoc />
        public IReadOnlyList<RunningProcessInfo> GetProcessesByName(string processName) => processes;

        /// <inheritdoc />
        public void Kill(int processId)
        {
            if (ThrowOnKillProcessId == processId)
            {
                throw new InvalidOperationException("Process has already exited.");
            }

            KilledProcessIds.Add(processId);
        }
    }
}
