using System;
using System.Collections.Generic;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
using ZB.MOM.WW.MxGateway.Worker.Sta;
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;

/// <summary>
///     Verifies that the four new alarm <see cref="MxCommandKind"/> values
///     route through <see cref="MxAccessCommandExecutor"/> to a fake
///     <see cref="IAlarmCommandHandler"/> and that the resulting
///     <see cref="MxCommandReply"/> carries the expected payload.
///
///     The data-side <see cref="MxAccessSession"/> is constructed via a
///     no-op factory because the executor only touches it for non-alarm
///     command kinds — alarm dispatch never reaches the data session.
/// </summary>
public sealed class AlarmCommandExecutorTests
{
    private const string SessionId = "S";
    private const string CorrelationId = "C";

    /// <summary>Verifies that the handler routes alarm subscriptions and returns ok.</summary>
    [Fact]
    public void SubscribeAlarms_WithHandler_RoutesToHandlerAndReturnsOk()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler();
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.SubscribeAlarms,
                SubscribeAlarms = new SubscribeAlarmsCommand
                {
                    SubscriptionExpression = @"\\HOST\Galaxy!Area",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Equal(@"\\HOST\Galaxy!Area", handler.LastSubscription);
        Assert.Equal(SessionId, handler.LastSessionId);
    }

    /// <summary>Verifies that subscription without handler returns invalid request.</summary>
    [Fact]
    public void SubscribeAlarms_WithoutHandler_ReturnsInvalidRequest()
    {
        MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.SubscribeAlarms,
                SubscribeAlarms = new SubscribeAlarmsCommand
                {
                    SubscriptionExpression = @"\\HOST\Galaxy!Area",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that empty subscription expression returns invalid request.</summary>
    [Fact]
    public void SubscribeAlarms_WithEmptyExpression_ReturnsInvalidRequest()
    {
        MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler());

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.SubscribeAlarms,
                SubscribeAlarms = new SubscribeAlarmsCommand
                {
                    SubscriptionExpression = "  ",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that acknowledge routes native status into hresult and payload.</summary>
    [Fact]
    public void AcknowledgeAlarm_WithHandler_RoutesNativeStatusIntoHresultAndPayload()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 };
        MxAccessCommandExecutor executor = NewExecutor(handler);
        Guid g = Guid.NewGuid();

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarm,
                AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
                {
                    AlarmGuid = g.ToString(),
                    Comment = "ack",
                    OperatorUser = "alice",
                    OperatorNode = "WS",
                    OperatorDomain = "CORP",
                    OperatorFullName = "Alice S",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Equal(0, reply.Hresult);
        Assert.NotNull(reply.AcknowledgeAlarm);
        Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus);
        Assert.Equal(g, handler.LastAckGuid);
        Assert.Equal("alice", handler.LastAckOperatorName);
    }

    /// <summary>Verifies that invalid alarm GUID returns invalid request.</summary>
    [Fact]
    public void AcknowledgeAlarm_WithInvalidGuid_ReturnsInvalidRequest()
    {
        MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler());

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarm,
                AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
                {
                    AlarmGuid = "not-a-guid",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that nonzero native status carries a diagnostic message.</summary>
    [Fact]
    public void AcknowledgeAlarm_WithNonzeroNativeStatus_CarriesDiagnostic()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = -123 };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarm,
                AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
                {
                    AlarmGuid = Guid.NewGuid().ToString(),
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(-123, reply.Hresult);
        Assert.Contains("-123", reply.DiagnosticMessage);
    }

    /// <summary>Verifies that acknowledge by name routes tuple to handler.</summary>
    [Fact]
    public void AcknowledgeAlarmByName_WithHandler_RoutesTupleToHandler()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarmByName,
                AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
                {
                    AlarmName = "TestMachine_001.TestAlarm001",
                    ProviderName = "Galaxy",
                    GroupName = "TestArea",
                    Comment = "ack",
                    OperatorUser = "alice",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.NotNull(reply.AcknowledgeAlarm);
        Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus);
        Assert.NotNull(handler.LastAckByNameTuple);
        Assert.Equal("TestMachine_001.TestAlarm001", handler.LastAckByNameTuple!.Value.Name);
        Assert.Equal("Galaxy", handler.LastAckByNameTuple!.Value.Provider);
        Assert.Equal("TestArea", handler.LastAckByNameTuple!.Value.Group);
        Assert.Equal("alice", handler.LastAckOperatorName);
    }

    /// <summary>Verifies that empty alarm name returns invalid request.</summary>
    [Fact]
    public void AcknowledgeAlarmByName_WithEmptyName_ReturnsInvalidRequest()
    {
        MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler());

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarmByName,
                AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
                {
                    AlarmName = " ",
                    ProviderName = "Galaxy",
                    GroupName = "TestArea",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that query active alarms returns payload with snapshots.</summary>
    [Fact]
    public void QueryActiveAlarms_WithHandler_ReturnsPayloadWithSnapshots()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler
        {
            QueryResult = new[]
            {
                new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T1" },
                new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T2" },
            },
        };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.QueryActiveAlarms,
                QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand
                {
                    AlarmFilterPrefix = "Galaxy!A",
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.NotNull(reply.QueryActiveAlarms);
        Assert.Equal(2, reply.QueryActiveAlarms.Snapshots.Count);
        Assert.Equal("Galaxy!A", handler.LastFilterPrefix);
    }

    /// <summary>
    ///     The reply payload's <c>SnapshotTruncated</c> comes from the handler's
    ///     verdict. This is the last hop before the IPC frame; if the executor
    ///     dropped it, a filtered query returning no records would carry no
    ///     completeness caveat at all.
    /// </summary>
    /// <param name="handlerReportsTruncated">The verdict the fake handler reports.</param>
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void QueryActiveAlarms_StampsSnapshotTruncatedFromHandler(bool handlerReportsTruncated)
    {
        FakeAlarmHandler handler = new FakeAlarmHandler
        {
            SnapshotTruncated = handlerReportsTruncated,
            QueryResult = new[]
            {
                new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T1" },
            },
        };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.QueryActiveAlarms,
                QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand(),
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.NotNull(reply.QueryActiveAlarms);
        Assert.Equal(handlerReportsTruncated, reply.QueryActiveAlarms.SnapshotTruncated);
    }

    /// <summary>
    ///     A truncated fetch whose records all filtered out still reports the
    ///     caveat on the payload — the case the per-record flag cannot cover.
    /// </summary>
    [Fact]
    public void QueryActiveAlarms_WithTruncatedFetchAndNoRecords_StillReportsTruncation()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler
        {
            SnapshotTruncated = true,
            QueryResult = Array.Empty<ActiveAlarmSnapshot>(),
        };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.QueryActiveAlarms,
                QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand(),
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.Empty(reply.QueryActiveAlarms.Snapshots);
        Assert.True(reply.QueryActiveAlarms.SnapshotTruncated);
    }

    /// <summary>Verifies that unsubscribe routes to handler.</summary>
    [Fact]
    public void UnsubscribeAlarms_WithHandler_RoutesToHandler()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler();
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.UnsubscribeAlarms,
                UnsubscribeAlarms = new UnsubscribeAlarmsCommand(),
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
        Assert.True(handler.UnsubscribeCalled);
    }

    /// <summary>Verifies that unsubscribe without handler is an ok noop.</summary>
    [Fact]
    public void UnsubscribeAlarms_WithoutHandler_IsOkNoop()
    {
        MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.UnsubscribeAlarms,
                UnsubscribeAlarms = new UnsubscribeAlarmsCommand(),
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that handler exception returns MXAccess failure status.</summary>
    [Fact]
    public void AcknowledgeAlarm_WhenHandlerThrows_ReturnsMxaccessFailure()
    {
        FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeThrow = true };
        MxAccessCommandExecutor executor = NewExecutor(handler);

        StaCommand command = new StaCommand(
            SessionId, CorrelationId,
            new MxCommand
            {
                Kind = MxCommandKind.AcknowledgeAlarm,
                AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand
                {
                    AlarmGuid = Guid.NewGuid().ToString(),
                },
            });

        MxCommandReply reply = executor.Execute(command);

        Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code);
        Assert.Contains("simulated", reply.DiagnosticMessage);
    }

    private static MxAccessCommandExecutor NewExecutor(IAlarmCommandHandler? alarmHandler)
    {
        // Construct an executor with a no-op data session — we only exercise
        // the alarm switch arms, which never touch the data session. The
        // session is built through the internal MxAccessSession.CreateForTesting
        // factory (exposed via [assembly: InternalsVisibleTo("ZB.MOM.WW.MxGateway.Worker.Tests")]
        // on ZB.MOM.WW.MxGateway.Worker), so no reflection is needed.
        return new MxAccessCommandExecutor(
            session: MxAccessSession.CreateForTesting(
                mxAccessServer: new NoopMxAccessServer(),
                eventSink: new NoopEventSink()),
            variantConverter: new ZB.MOM.WW.MxGateway.Worker.Conversion.VariantConverter(),
            alarmCommandHandler: alarmHandler);
    }

    private sealed class FakeAlarmHandler : IAlarmCommandHandler
    {
        /// <summary>Gets the last subscription expression.</summary>
        public string? LastSubscription { get; private set; }

        /// <summary>Gets the last session ID.</summary>
        public string? LastSessionId { get; private set; }

        /// <summary>Gets a value indicating whether unsubscribe was called.</summary>
        public bool UnsubscribeCalled { get; private set; }

        /// <summary>Gets the last acknowledge alarm GUID.</summary>
        public Guid LastAckGuid { get; private set; }

        /// <summary>Gets the last acknowledge operator name.</summary>
        public string? LastAckOperatorName { get; private set; }

        /// <summary>Gets or sets the value returned by acknowledge.</summary>
        public int AcknowledgeReturn { get; set; }

        /// <summary>Gets or sets a value indicating whether acknowledge throws.</summary>
        public bool AcknowledgeThrow { get; set; }

        /// <summary>Gets or sets the query result snapshots.</summary>
        public IReadOnlyList<ActiveAlarmSnapshot> QueryResult { get; set; } =
            Array.Empty<ActiveAlarmSnapshot>();

        /// <summary>Gets the last alarm filter prefix.</summary>
        public string? LastFilterPrefix { get; private set; }

        /// <summary>Gets or sets the truncation verdict this handler reports with its query result.</summary>
        public bool SnapshotTruncated { get; set; }

        /// <inheritdoc />
        public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
        {
            LastSubscription = command.SubscriptionExpression;
            LastSessionId = sessionId;
        }

        /// <inheritdoc />
        public void Unsubscribe()
        {
            UnsubscribeCalled = true;
        }

        /// <inheritdoc />
        public int Acknowledge(
            Guid alarmGuid, string comment, string operatorUser,
            string operatorNode, string operatorDomain, string operatorFullName)
        {
            LastAckGuid = alarmGuid;
            LastAckOperatorName = operatorUser;
            if (AcknowledgeThrow)
            {
                throw new InvalidOperationException("simulated alarm-handler failure");
            }
            return AcknowledgeReturn;
        }

        /// <inheritdoc />
        public int AcknowledgeByName(
            string alarmName, string providerName, string groupName,
            string comment, string operatorUser, string operatorNode,
            string operatorDomain, string operatorFullName)
        {
            LastAckByNameTuple = (alarmName, providerName, groupName);
            LastAckOperatorName = operatorUser;
            return AcknowledgeReturn;
        }

        /// <summary>Gets the last acknowledge by name tuple.</summary>
        public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }

        /// <inheritdoc />
        public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(
            string? alarmFilterPrefix,
            out bool snapshotTruncated)
        {
            LastFilterPrefix = alarmFilterPrefix;
            snapshotTruncated = SnapshotTruncated;
            return QueryResult;
        }

        /// <summary>Gets the number of poll calls.</summary>
        public int PollCount { get; private set; }

        /// <inheritdoc />
        public void PollOnce()
        {
            PollCount++;
        }

        /// <inheritdoc />
        public void Dispose() { }
    }
}
