using System;
using System.IO.Pipes;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Serilog;
using Serilog.Core;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Ipc;

/// <summary>
///     Driver.Historian.Wonderware-007 regression. The two other rejection paths
///     (shared-secret-mismatch and major-version-mismatch) both write a <see cref="HelloAck"/>
///     with <c>Accepted=false</c> before disconnecting; the caller-SID-mismatch path used to
///     just disconnect abruptly, leaving the client to time out instead of learning why.
///     The fix sends a symmetric <c>caller-sid-mismatch</c> ack before disconnecting.
///
///     The test uses the internal test-seam constructor so the verifier rejects without
///     needing to actually relax the pipe ACL (which would block the test client itself).
/// </summary>
public sealed class PipeServerSidRejectTests
{
    private static readonly ILogger Quiet = Logger.None;

    /// <summary>Verifies that a caller SID mismatch sends HelloAck with reject reason before disconnect.</summary>
    [Fact]
    public async Task Caller_SID_mismatch_sends_HelloAck_with_reject_reason_before_disconnect()
    {
        // The pipe ACL must allow the current process to connect — so wire up the pipe
        // with the current user's SID. Then have the verifier seam simulate the SID
        // mismatch by returning false. This isolates the "what does the server do on a
        // rejected caller" question from the (separate) "is the ACL correct" question.
        var current = WindowsIdentity.GetCurrent().User
                      ?? throw new InvalidOperationException("WindowsIdentity.GetCurrent().User was null — cannot run test");

        var pipeName = $"otopcua-hist-sidreject-test-{Guid.NewGuid():N}";

        PipeServer.CallerVerifier rejecting = (NamedPipeServerStream _, SecurityIdentifier _, out string reason) =>
        {
            reason = "synthetic-mismatch";
            return false;
        };
        using var server = new PipeServer(pipeName, current, "secret", Quiet, rejecting);

        var serverTask = Task.Run(() => server.RunOneConnectionAsync(new NoopHandler(), CancellationToken.None));

        using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
        await client.ConnectAsync(5_000);

        using var writer = new FrameWriter(client, leaveOpen: true);
        using var reader = new FrameReader(client, leaveOpen: true);

        var hello = new Hello { ProtocolMajor = Hello.CurrentMajor, PeerName = "test", SharedSecret = "secret" };
        await writer.WriteAsync(MessageKind.Hello, hello, CancellationToken.None);

        // Read the rejecting HelloAck the server is expected to send before disconnecting.
        var frame = await reader.ReadFrameAsync(CancellationToken.None);
        frame.ShouldNotBeNull("server must send a HelloAck on caller-SID rejection, not just disconnect");
        frame!.Value.Kind.ShouldBe(MessageKind.HelloAck);

        var ack = MessagePackSerializer.Deserialize<HelloAck>(frame.Value.Body);
        ack.Accepted.ShouldBeFalse();
        ack.RejectReason.ShouldNotBeNullOrEmpty();
        ack.RejectReason!.ShouldContain("caller-sid-mismatch",
            Case.Insensitive,
            "reject reason must match the documented caller-sid-mismatch tag so clients can diagnose");

        await serverTask;
    }

    /// <summary>Handler that asserts it is never called — the connection must be rejected at Hello.</summary>
    private sealed class NoopHandler : IFrameHandler
    {
        /// <summary>Throws if called, as the connection should be rejected before reaching this handler.</summary>
        /// <param name="kind">The message kind (unused).</param>
        /// <param name="body">The message body (unused).</param>
        /// <param name="writer">The frame writer (unused).</param>
        /// <param name="ct">Cancellation token (unused).</param>
        /// <returns>Never returns; always throws.</returns>
        public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
        {
            throw new InvalidOperationException(
                $"Handler must not be reached on a rejected caller; got frame {kind}");
        }
    }
}
