using System.Diagnostics;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;

/// <summary>
///     Live end-to-end proof of the STAB-1 reconnect path against the real python-snap7
///     S7-1500 profile: a running <see cref="S7Driver"/> survives a full PLC bounce (container
///     stop/start) and resumes reading <b>without a redeploy</b> — the exact failure the
///     arch-review flagged (a transient drop permanently killed the driver until redeploy).
/// </summary>
/// <remarks>
///     <para>
///         Double-gated so it never runs destructively by accident:
///         <list type="number">
///             <item><see cref="Snap7ServerFixture.SkipReason"/> — the sim must be reachable; and</item>
///             <item><c>S7_RECONNECT_BOUNCE_CMD</c> — the shell command that bounces the PLC/container
///                 (e.g. <c>ssh dohertj2@10.100.0.35 docker restart otopcua-python-snap7-s7_1500</c>).
///                 Absent ⇒ the test skips, because bouncing the shared sim would disrupt other suites.</item>
///         </list>
///         There is no per-test container hook in the harness, so the bounce is delegated to this
///         operator-supplied command — the automation the plan called a "manual recipe", captured in
///         code so it is reproducible.
///     </para>
/// </remarks>
[Collection(Snap7ServerCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Category", "Reconnect")]
[Trait("Device", "S7_1500")]
public sealed class S7_1500ReconnectTests(Snap7ServerFixture sim)
{
    private const string BounceCmdEnvVar = "S7_RECONNECT_BOUNCE_CMD";

    /// <summary>
    ///     Reads a seeded tag Good, bounces the PLC out-of-band, observes the read fail (Degraded),
    ///     then asserts the driver reopens on its own and the tag returns to Good — no redeploy.
    /// </summary>
    [Fact]
    public async Task Driver_recovers_reads_after_a_live_PLC_bounce_without_redeploy()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
        var bounceCmd = Environment.GetEnvironmentVariable(BounceCmdEnvVar);
        if (string.IsNullOrWhiteSpace(bounceCmd))
            Assert.Skip($"Set {BounceCmdEnvVar} to the command that bounces the S7 sim to run this destructive test.");

        var ct = TestContext.Current.CancellationToken;
        var options = S7_1500Profile.BuildOptions(sim.Host, sim.Port);
        await using var drv = new S7Driver(options, driverInstanceId: "s7-reconnect-live");
        await drv.InitializeAsync("{}", ct);

        // Baseline — a Good read against the live sim.
        (await drv.ReadAsync([S7_1500Profile.ProbeTag], ct))[0].StatusCode.ShouldBe(0u);
        drv.GetHealth().State.ShouldBe(DriverState.Healthy);

        // Bounce the PLC out-of-band (stop → start), then confirm the driver both NOTICES the drop
        // (at least one non-Good read) and RECOVERS to Good on its own within a bounded window.
        await RunBounceCommandAsync(bounceCmd!, ct);

        var sawOutage = false;
        var recovered = false;
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60);
        while (DateTime.UtcNow < deadline)
        {
            var status = (await drv.ReadAsync([S7_1500Profile.ProbeTag], ct))[0].StatusCode;
            if (status != 0u) sawOutage = true;
            else if (sawOutage) { recovered = true; break; }
            await Task.Delay(TimeSpan.FromMilliseconds(500), ct);
        }

        sawOutage.ShouldBeTrue("the driver should have observed the PLC outage as a failed read");
        recovered.ShouldBeTrue("the driver should have reopened the connection and resumed Good reads without a redeploy");
        drv.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    private static async Task RunBounceCommandAsync(string command, CancellationToken ct)
    {
        // Run through the login shell so an SSH/docker one-liner in the env var works verbatim.
        using var proc = new Process
        {
            StartInfo = new ProcessStartInfo("/bin/sh", $"-c \"{command.Replace("\"", "\\\"")}\"")
            {
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
            },
        };
        proc.Start();
        await proc.WaitForExitAsync(ct);
        proc.ExitCode.ShouldBe(0, $"bounce command failed: {await proc.StandardError.ReadToEndAsync(ct)}");
        // Give the container a moment to release the port before the first post-bounce read.
        await Task.Delay(TimeSpan.FromSeconds(1), ct);
    }
}
