using System;
using System.IO;
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.FOCAS.Host.Ipc;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests
{
    /// <summary>
    ///     Direct FOCAS Host IPC handshake test. Drives <see cref="PipeServer"/> through a
    ///     hand-rolled pipe client built on <see cref="FrameReader"/> / <see cref="FrameWriter"/>
    ///     from FOCAS.Shared. Skipped on Administrator shells because <c>PipeAcl</c> denies
    ///     the BuiltinAdministrators group.
    /// </summary>
    [Trait("Category", "Integration")]
    public sealed class IpcHandshakeIntegrationTests
    {
        private static bool IsAdministrator()
        {
            using var identity = WindowsIdentity.GetCurrent();
            return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
        }

        private static async Task<(NamedPipeClientStream Stream, FrameReader Reader, FrameWriter Writer)>
            ConnectAndHelloAsync(string pipeName, string secret, CancellationToken ct)
        {
            var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
            await stream.ConnectAsync(5_000, ct);

            var reader = new FrameReader(stream, leaveOpen: true);
            var writer = new FrameWriter(stream, leaveOpen: true);
            await writer.WriteAsync(FocasMessageKind.Hello,
                new Hello { PeerName = "test-client", SharedSecret = secret }, ct);

            var ack = await reader.ReadFrameAsync(ct);
            if (ack is null) throw new EndOfStreamException("no HelloAck");
            if (ack.Value.Kind != FocasMessageKind.HelloAck)
                throw new InvalidOperationException("unexpected first frame kind " + ack.Value.Kind);
            var ackMsg = MessagePackSerializer.Deserialize<HelloAck>(ack.Value.Body);
            if (!ackMsg.Accepted) throw new UnauthorizedAccessException(ackMsg.RejectReason);

            return (stream, reader, writer);
        }

        [Fact]
        public async Task Handshake_with_correct_secret_succeeds_and_heartbeat_round_trips()
        {
            if (IsAdministrator()) return;

            using var identity = WindowsIdentity.GetCurrent();
            var sid = identity.User!;
            var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
            const string secret = "test-secret-2026";
            Logger log = new LoggerConfiguration().CreateLogger();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

            var server = new PipeServer(pipe, sid, secret, log);
            var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));

            var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
            using (stream)
            using (reader)
            using (writer)
            {
                await writer.WriteAsync(FocasMessageKind.Heartbeat,
                    new Heartbeat { MonotonicTicks = 42 }, cts.Token);

                var hbAck = await reader.ReadFrameAsync(cts.Token);
                hbAck.HasValue.ShouldBeTrue();
                hbAck!.Value.Kind.ShouldBe(FocasMessageKind.HeartbeatAck);
                MessagePackSerializer.Deserialize<HeartbeatAck>(hbAck.Value.Body).MonotonicTicks.ShouldBe(42L);
            }

            cts.Cancel();
            try { await serverTask; } catch { }
            server.Dispose();
        }

        [Fact]
        public async Task Handshake_with_wrong_secret_is_rejected()
        {
            if (IsAdministrator()) return;

            using var identity = WindowsIdentity.GetCurrent();
            var sid = identity.User!;
            var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
            Logger log = new LoggerConfiguration().CreateLogger();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

            var server = new PipeServer(pipe, sid, "real-secret", log);
            var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));

            await Should.ThrowAsync<UnauthorizedAccessException>(async () =>
            {
                var (s, r, w) = await ConnectAndHelloAsync(pipe, "wrong-secret", cts.Token);
                s.Dispose();
                r.Dispose();
                w.Dispose();
            });

            cts.Cancel();
            try { await serverTask; } catch { }
            server.Dispose();
        }

        [Fact]
        public async Task Stub_handler_returns_not_implemented_for_data_plane_request()
        {
            if (IsAdministrator()) return;

            using var identity = WindowsIdentity.GetCurrent();
            var sid = identity.User!;
            var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
            const string secret = "stub-test";
            Logger log = new LoggerConfiguration().CreateLogger();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

            var server = new PipeServer(pipe, sid, secret, log);
            var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));

            var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
            using (stream)
            using (reader)
            using (writer)
            {
                await writer.WriteAsync(FocasMessageKind.ReadRequest,
                    new ReadRequest
                    {
                        SessionId = 1,
                        Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 },
                        DataType = FocasDataTypeCode.Int32,
                    },
                    cts.Token);

                var resp = await reader.ReadFrameAsync(cts.Token);
                resp.HasValue.ShouldBeTrue();
                resp!.Value.Kind.ShouldBe(FocasMessageKind.ErrorResponse);
                var err = MessagePackSerializer.Deserialize<ErrorResponse>(resp.Value.Body);
                err.Code.ShouldBe("not-implemented");
                err.Message.ShouldContain("PR C");
            }

            cts.Cancel();
            try { await serverTask; } catch { }
            server.Dispose();
        }
    }
}
