using MessagePack;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.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.Tests;

/// <summary>
///     End-to-end IPC round-trips over an in-memory loopback: <c>IpcFocasClient</c> talks
///     to a test fake that plays the Host's role by reading frames, dispatching on kind,
///     and responding with canned DTOs. Validates that every <see cref="IFocasClient"/>
///     method translates to the right wire frame + decodes the response correctly.
/// </summary>
[Trait("Category", "Unit")]
public sealed class IpcFocasClientTests
{
    private const string Secret = "test-secret";

    private static async Task ServerLoopAsync(Stream serverSide, Func<FocasMessageKind, byte[], FrameWriter, Task> dispatch, CancellationToken ct)
    {
        using var reader = new FrameReader(serverSide, leaveOpen: true);
        using var writer = new FrameWriter(serverSide, leaveOpen: true);

        // Hello handshake.
        var first = await reader.ReadFrameAsync(ct);
        if (first is null) return;
        var hello = MessagePackSerializer.Deserialize<Hello>(first.Value.Body);
        var accepted = hello.SharedSecret == Secret;
        await writer.WriteAsync(FocasMessageKind.HelloAck,
            new HelloAck { Accepted = accepted, RejectReason = accepted ? null : "wrong-secret" }, ct);
        if (!accepted) return;

        while (!ct.IsCancellationRequested)
        {
            var frame = await reader.ReadFrameAsync(ct);
            if (frame is null) return;
            await dispatch(frame.Value.Kind, frame.Value.Body, writer);
        }
    }

    [Fact]
    public async Task Connect_sends_OpenSessionRequest_and_caches_session_id()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        OpenSessionRequest? received = null;
        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            if (kind == FocasMessageKind.OpenSessionRequest)
            {
                received = MessagePackSerializer.Deserialize<OpenSessionRequest>(body);
                await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                    new OpenSessionResponse { Success = true, SessionId = 42 }, cts.Token);
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc, FocasCncSeries.Thirty_i);
        await client.ConnectAsync(new FocasHostAddress("192.168.1.50", 8193), TimeSpan.FromSeconds(2), cts.Token);

        client.IsConnected.ShouldBeTrue();
        received.ShouldNotBeNull();
        received!.HostAddress.ShouldBe("192.168.1.50:8193");
        received.CncSeries.ShouldBe((int)FocasCncSeries.Thirty_i);

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

    [Fact]
    public async Task Connect_throws_when_host_rejects()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            if (kind == FocasMessageKind.OpenSessionRequest)
            {
                await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                    new OpenSessionResponse { Success = false, Error = "unreachable", ErrorCode = "EW_SOCKET" }, cts.Token);
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        await Should.ThrowAsync<InvalidOperationException>(async () =>
            await client.ConnectAsync(new FocasHostAddress("10.0.0.1", 8193), TimeSpan.FromSeconds(1), cts.Token));

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

    [Fact]
    public async Task Read_sends_ReadRequest_and_decodes_response()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        ReadRequest? received = null;
        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            switch (kind)
            {
                case FocasMessageKind.OpenSessionRequest:
                    await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                        new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
                    break;
                case FocasMessageKind.ReadRequest:
                    received = MessagePackSerializer.Deserialize<ReadRequest>(body);
                    await writer.WriteAsync(FocasMessageKind.ReadResponse,
                        new ReadResponse
                        {
                            Success = true,
                            StatusCode = 0,
                            ValueBytes = MessagePackSerializer.Serialize((int)12345),
                            ValueTypeCode = FocasDataTypeCode.Int32,
                        }, cts.Token);
                    break;
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);

        var addr = new FocasAddress(FocasAreaKind.Parameter, null, 1815, null);
        var (value, status) = await client.ReadAsync(addr, FocasDataType.Int32, cts.Token);
        status.ShouldBe(0u);
        value.ShouldBe(12345);
        received!.Address.Number.ShouldBe(1815);

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

    [Fact]
    public async Task Write_sends_WriteRequest_and_returns_status()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            switch (kind)
            {
                case FocasMessageKind.OpenSessionRequest:
                    await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                        new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
                    break;
                case FocasMessageKind.WriteRequest:
                    var req = MessagePackSerializer.Deserialize<WriteRequest>(body);
                    MessagePackSerializer.Deserialize<double>(req.ValueBytes!).ShouldBe(3.14);
                    await writer.WriteAsync(FocasMessageKind.WriteResponse,
                        new WriteResponse { Success = true, StatusCode = 0 }, cts.Token);
                    break;
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);

        var status = await client.WriteAsync(new FocasAddress(FocasAreaKind.Macro, null, 500, null),
            FocasDataType.Float64, 3.14, cts.Token);
        status.ShouldBe(0u);

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

    [Fact]
    public async Task Write_pmc_bit_sends_first_class_RMW_frame()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        PmcBitWriteRequest? received = null;
        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            switch (kind)
            {
                case FocasMessageKind.OpenSessionRequest:
                    await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                        new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
                    break;
                case FocasMessageKind.PmcBitWriteRequest:
                    received = MessagePackSerializer.Deserialize<PmcBitWriteRequest>(body);
                    await writer.WriteAsync(FocasMessageKind.PmcBitWriteResponse,
                        new PmcBitWriteResponse { Success = true, StatusCode = 0 }, cts.Token);
                    break;
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);

        var addr = new FocasAddress(FocasAreaKind.Pmc, "R", 100, BitIndex: 5);
        var status = await client.WriteAsync(addr, FocasDataType.Bit, true, cts.Token);
        status.ShouldBe(0u);
        received.ShouldNotBeNull();
        received!.BitIndex.ShouldBe(5);
        received.Value.ShouldBeTrue();
        received.Address.PmcLetter.ShouldBe("R");

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

    [Fact]
    public async Task Probe_round_trips_health_from_host()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            switch (kind)
            {
                case FocasMessageKind.OpenSessionRequest:
                    await writer.WriteAsync(FocasMessageKind.OpenSessionResponse,
                        new OpenSessionResponse { Success = true, SessionId = 1 }, cts.Token);
                    break;
                case FocasMessageKind.ProbeRequest:
                    await writer.WriteAsync(FocasMessageKind.ProbeResponse,
                        new ProbeResponse { Healthy = true, ObservedAtUtcUnixMs = 1_700_000_000_000 }, cts.Token);
                    break;
            }
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token);
        (await client.ProbeAsync(cts.Token)).ShouldBeTrue();

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

    [Fact]
    public async Task Error_response_from_host_surfaces_as_FocasIpcException()
    {
        await using var loop = new IpcLoopback();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var server = Task.Run(() => ServerLoopAsync(loop.ServerSide, async (kind, body, writer) =>
        {
            await writer.WriteAsync(FocasMessageKind.ErrorResponse,
                new ErrorResponse { Code = "backend-exception", Message = "simulated" }, cts.Token);
        }, cts.Token));

        var ipc = await FocasIpcClient.ConnectAsync(loop.ClientSide, Secret, cts.Token);
        var client = new IpcFocasClient(ipc);
        var ex = await Should.ThrowAsync<FocasIpcException>(async () =>
            await client.ConnectAsync(new FocasHostAddress("h", 8193), TimeSpan.FromSeconds(1), cts.Token));
        ex.Code.ShouldBe("backend-exception");

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