using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Backend;
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>
    ///     Validates that <see cref="FwlibFrameHandler"/> correctly dispatches each
    ///     <see cref="FocasMessageKind"/> to the corresponding <see cref="IFocasBackend"/>
    ///     method and serializes the response into the expected response kind. Uses
    ///     <see cref="FakeFocasBackend"/> so no hardware is needed.
    /// </summary>
    [Trait("Category", "Unit")]
    public sealed class FwlibFrameHandlerTests
    {
        private static async Task RoundTripAsync<TReq, TResp>(
            IFrameHandler handler, FocasMessageKind reqKind, TReq req, FocasMessageKind expectedRespKind,
            Action<TResp> assertResponse)
        {
            using var buffer = new MemoryStream();
            using var writer = new FrameWriter(buffer, leaveOpen: true);
            await handler.HandleAsync(reqKind, MessagePackSerializer.Serialize(req), writer, CancellationToken.None);

            buffer.Position = 0;
            using var reader = new FrameReader(buffer, leaveOpen: true);
            var frame = await reader.ReadFrameAsync(CancellationToken.None);
            frame.HasValue.ShouldBeTrue();
            frame!.Value.Kind.ShouldBe(expectedRespKind);
            assertResponse(MessagePackSerializer.Deserialize<TResp>(frame.Value.Body));
        }

        private static FwlibFrameHandler BuildHandler() =>
            new(new FakeFocasBackend(), new LoggerConfiguration().CreateLogger());

        [Fact]
        public async Task OpenSession_returns_a_new_session_id()
        {
            long sessionId = 0;
            await RoundTripAsync<OpenSessionRequest, OpenSessionResponse>(
                BuildHandler(),
                FocasMessageKind.OpenSessionRequest,
                new OpenSessionRequest { HostAddress = "h:8193" },
                FocasMessageKind.OpenSessionResponse,
                resp => { resp.Success.ShouldBeTrue(); resp.SessionId.ShouldBeGreaterThan(0L); sessionId = resp.SessionId; });
            sessionId.ShouldBeGreaterThan(0L);
        }

        [Fact]
        public async Task Read_without_open_session_returns_internal_error()
        {
            await RoundTripAsync<ReadRequest, ReadResponse>(
                BuildHandler(),
                FocasMessageKind.ReadRequest,
                new ReadRequest
                {
                    SessionId = 999,
                    Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 },
                    DataType = FocasDataTypeCode.Int32,
                },
                FocasMessageKind.ReadResponse,
                resp => { resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("session-not-open"); });
        }

        [Fact]
        public async Task Full_open_write_read_round_trip_preserves_value()
        {
            var handler = BuildHandler();

            // Open.
            using var buffer = new MemoryStream();
            using var writer = new FrameWriter(buffer, leaveOpen: true);
            await handler.HandleAsync(FocasMessageKind.OpenSessionRequest,
                MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None);

            buffer.Position = 0;
            using var reader = new FrameReader(buffer, leaveOpen: true);
            var openFrame = await reader.ReadFrameAsync(CancellationToken.None);
            var openResp = MessagePackSerializer.Deserialize<OpenSessionResponse>(openFrame!.Value.Body);
            var sessionId = openResp.SessionId;

            // Write 42 at MACRO:500 as Int32.
            buffer.Position = 0;
            buffer.SetLength(0);
            await handler.HandleAsync(FocasMessageKind.WriteRequest,
                MessagePackSerializer.Serialize(new WriteRequest
                {
                    SessionId = sessionId,
                    Address = new FocasAddressDto { Kind = 2, Number = 500 },
                    DataType = FocasDataTypeCode.Int32,
                    ValueTypeCode = FocasDataTypeCode.Int32,
                    ValueBytes = MessagePackSerializer.Serialize((int)42),
                }), writer, CancellationToken.None);

            // Read back.
            buffer.Position = 0;
            buffer.SetLength(0);
            await handler.HandleAsync(FocasMessageKind.ReadRequest,
                MessagePackSerializer.Serialize(new ReadRequest
                {
                    SessionId = sessionId,
                    Address = new FocasAddressDto { Kind = 2, Number = 500 },
                    DataType = FocasDataTypeCode.Int32,
                }), writer, CancellationToken.None);

            buffer.Position = 0;
            var readFrame = await reader.ReadFrameAsync(CancellationToken.None);
            readFrame.HasValue.ShouldBeTrue();
            readFrame!.Value.Kind.ShouldBe(FocasMessageKind.ReadResponse);
            // With buffer reuse there may be multiple queued frames; we want the last one.
            var lastResp = MessagePackSerializer.Deserialize<ReadResponse>(readFrame.Value.Body);
            // If the Write frame is first, drain it.
            if (lastResp.ValueBytes is null)
            {
                var next = await reader.ReadFrameAsync(CancellationToken.None);
                lastResp = MessagePackSerializer.Deserialize<ReadResponse>(next!.Value.Body);
            }
            lastResp.Success.ShouldBeTrue();
            MessagePackSerializer.Deserialize<int>(lastResp.ValueBytes!).ShouldBe(42);
        }

        [Fact]
        public async Task PmcBitWrite_sets_specified_bit()
        {
            var handler = BuildHandler();
            using var buffer = new MemoryStream();
            using var writer = new FrameWriter(buffer, leaveOpen: true);

            await handler.HandleAsync(FocasMessageKind.OpenSessionRequest,
                MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None);
            buffer.Position = 0;
            using var reader = new FrameReader(buffer, leaveOpen: true);
            var openFrame = await reader.ReadFrameAsync(CancellationToken.None);
            var sessionId = MessagePackSerializer.Deserialize<OpenSessionResponse>(openFrame!.Value.Body).SessionId;

            buffer.Position = 0; buffer.SetLength(0);
            await handler.HandleAsync(FocasMessageKind.PmcBitWriteRequest,
                MessagePackSerializer.Serialize(new PmcBitWriteRequest
                {
                    SessionId = sessionId,
                    Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 },
                    BitIndex = 3,
                    Value = true,
                }), writer, CancellationToken.None);

            buffer.Position = 0;
            var resp = MessagePackSerializer.Deserialize<PmcBitWriteResponse>(
                (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body);
            resp.Success.ShouldBeTrue();
            resp.StatusCode.ShouldBe(0u);
        }

        [Fact]
        public async Task Probe_reports_healthy_when_session_open()
        {
            var handler = BuildHandler();
            using var buffer = new MemoryStream();
            using var writer = new FrameWriter(buffer, leaveOpen: true);
            await handler.HandleAsync(FocasMessageKind.OpenSessionRequest,
                MessagePackSerializer.Serialize(new OpenSessionRequest { HostAddress = "h:8193" }), writer, CancellationToken.None);
            buffer.Position = 0;
            using var reader = new FrameReader(buffer, leaveOpen: true);
            var sessionId = MessagePackSerializer.Deserialize<OpenSessionResponse>(
                (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body).SessionId;

            buffer.Position = 0; buffer.SetLength(0);
            await handler.HandleAsync(FocasMessageKind.ProbeRequest,
                MessagePackSerializer.Serialize(new ProbeRequest { SessionId = sessionId }), writer, CancellationToken.None);
            buffer.Position = 0;
            var resp = MessagePackSerializer.Deserialize<ProbeResponse>(
                (await reader.ReadFrameAsync(CancellationToken.None))!.Value.Body);
            resp.Healthy.ShouldBeTrue();
        }

        [Fact]
        public async Task Unconfigured_backend_returns_pointed_error_message()
        {
            var handler = new FwlibFrameHandler(new UnconfiguredFocasBackend(), new LoggerConfiguration().CreateLogger());
            await RoundTripAsync<OpenSessionRequest, OpenSessionResponse>(
                handler,
                FocasMessageKind.OpenSessionRequest,
                new OpenSessionRequest { HostAddress = "h:8193" },
                FocasMessageKind.OpenSessionResponse,
                resp =>
                {
                    resp.Success.ShouldBeFalse();
                    resp.Error.ShouldContain("Fwlib32");
                    resp.ErrorCode.ShouldBe("NoFwlibBackend");
                });
        }
    }
}
