using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using MxGateway.Contracts; using MxGateway.Contracts.Proto; namespace MxGateway.Tests.Contracts; public sealed class ProtobufContractRoundTripTests { /// Verifies that gateway descriptor contains expected public service methods. [Fact] public void GatewayDescriptor_ContainsInitialPublicServiceMethods() { var service = Assert.Single( MxaccessGatewayReflection.Descriptor.Services, descriptor => descriptor.Name == "MxAccessGateway"); Assert.Contains(service.Methods, method => method.Name == "OpenSession"); Assert.Contains(service.Methods, method => method.Name == "CloseSession"); Assert.Contains(service.Methods, method => method.Name == "Invoke"); Assert.Contains(service.Methods, method => method.Name == "StreamEvents"); Assert.Contains(service.Methods, method => method.Name == "AcknowledgeAlarm"); Assert.Contains(service.Methods, method => method.Name == "QueryActiveAlarms"); } /// Verifies that worker envelope descriptor contains required correlation fields. [Fact] public void WorkerEnvelopeDescriptor_ContainsRequiredCorrelationFields() { var fields = WorkerEnvelope.Descriptor.Fields.InDeclarationOrder(); Assert.Contains(fields, field => field.Name == "protocol_version"); Assert.Contains(fields, field => field.Name == "session_id"); Assert.Contains(fields, field => field.Name == "sequence"); Assert.Contains(fields, field => field.Name == "correlation_id"); } /// Verifies that command request round-trips through serialization. [Fact] public void CommandRequest_RoundTripsMethodSpecificPayload() { var original = new MxCommandRequest { SessionId = "session-1", ClientCorrelationId = "client-correlation-1", Command = new MxCommand { Kind = MxCommandKind.Register, Register = new RegisterCommand { ClientName = "mxaccessgw-test-client", }, }, }; var parsed = MxCommandRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxCommand.PayloadOneofCase.Register, parsed.Command.PayloadCase); } /// Verifies that command reply round-trips with return values and statuses. [Fact] public void CommandReply_RoundTripsHResultReturnValueOutParamsAndStatuses() { var original = new MxCommandReply { SessionId = "session-1", CorrelationId = "gateway-correlation-1", Kind = MxCommandKind.AddItem, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, }, Hresult = 0, ReturnValue = new MxValue { DataType = MxDataType.Integer, Int32Value = 1234, VariantType = "VT_I4", }, AddItem = new AddItemReply { ItemHandle = 1234, }, }; original.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingLmx, Detail = 0, }); var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.HasHresult); Assert.Equal(MxCommandReply.PayloadOneofCase.AddItem, parsed.PayloadCase); Assert.Single(parsed.Statuses); } /// Verifies that event round-trips with value, status, and sequence. [Fact] public void Event_RoundTripsValueStatusSequenceAndBufferedBody() { var timestamp = Timestamp.FromDateTime(new DateTime(2026, 4, 26, 20, 0, 0, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnBufferedDataChange, SessionId = "session-1", ServerHandle = 10, ItemHandle = 20, Value = new MxValue { DataType = MxDataType.Float, ArrayValue = new MxArray { ElementDataType = MxDataType.Float, FloatValues = new FloatArray { Values = { 1.5f, 2.5f }, }, Dimensions = { 2 }, VariantType = "VT_ARRAY|VT_R4", }, }, Quality = 192, SourceTimestamp = timestamp, WorkerSequence = 42, WorkerTimestamp = timestamp, GatewayReceiveTimestamp = timestamp, OnBufferedDataChange = new OnBufferedDataChangeEvent { DataType = MxDataType.Float, QualityValues = new MxArray { ElementDataType = MxDataType.Integer, Int32Values = new Int32Array { Values = { 192, 192 }, }, Dimensions = { 2 }, }, TimestampValues = new MxArray { ElementDataType = MxDataType.Time, TimestampValues = new TimestampArray { Values = { timestamp, timestamp }, }, Dimensions = { 2 }, }, }, }; original.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingNmx, Detail = 0, }); var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, parsed.BodyCase); Assert.Single(parsed.Statuses); } /// Verifies that worker envelope round-trips through serialization preserving protocol and command fields. [Fact] public void WorkerEnvelope_RoundTripsProtocolFieldsAndCommandBody() { var original = new WorkerEnvelope { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, SessionId = "session-1", Sequence = 7, CorrelationId = "gateway-correlation-1", WorkerCommand = new WorkerCommand { EnqueueTimestamp = Timestamp.FromDateTime( new DateTime(2026, 4, 26, 20, 5, 0, DateTimeKind.Utc)), Command = new MxCommand { Kind = MxCommandKind.Advise, Advise = new AdviseCommand { ServerHandle = 10, ItemHandle = 20, }, }, }, }; var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, parsed.BodyCase); Assert.Equal(MxCommand.PayloadOneofCase.Advise, parsed.WorkerCommand.Command.PayloadCase); } /// Verifies that an OnAlarmTransition event round-trips with full payload. [Fact] public void Event_RoundTripsOnAlarmTransitionWithFullPayload() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnAlarmTransition, SessionId = "session-1", WorkerSequence = 99, WorkerTimestamp = ack, GatewayReceiveTimestamp = ack, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Acknowledge, Severity = 750, OriginalRaiseTimestamp = raise, TransitionTimestamp = ack, OperatorUser = "operator1", OperatorComment = "investigating", Category = "Process", Description = "Tank 01 high-high level", CurrentValue = new MxValue { DataType = MxDataType.Float, FloatValue = 95.4f, VariantType = "VT_R4", }, LimitValue = new MxValue { DataType = MxDataType.Float, FloatValue = 90.0f, VariantType = "VT_R4", }, }, }; var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, parsed.BodyCase); Assert.Equal(AlarmTransitionKind.Acknowledge, parsed.OnAlarmTransition.TransitionKind); Assert.Equal(raise, parsed.OnAlarmTransition.OriginalRaiseTimestamp); Assert.Equal("operator1", parsed.OnAlarmTransition.OperatorUser); } /// Verifies that an OnAlarmTransition event round-trips with only the required fields populated. [Fact] public void Event_RoundTripsOnAlarmTransitionWithOptionalFieldsEmpty() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var original = new MxEvent { Family = MxEventFamily.OnAlarmTransition, SessionId = "session-1", WorkerSequence = 100, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Raise, Severity = 750, TransitionTimestamp = raise, }, }; var parsed = MxEvent.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorUser); Assert.Equal(string.Empty, parsed.OnAlarmTransition.OperatorComment); Assert.Null(parsed.OnAlarmTransition.OriginalRaiseTimestamp); Assert.Null(parsed.OnAlarmTransition.CurrentValue); } /// Verifies that an MxEvent body oneof rejects multiple bodies — last write wins per proto3 semantics. [Fact] public void Event_OneofGuard_LastBodyWins() { var ev = new MxEvent { Family = MxEventFamily.OnAlarmTransition, OnDataChange = new OnDataChangeEvent(), OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "X", TransitionKind = AlarmTransitionKind.Raise, }, }; Assert.Equal(MxEvent.BodyOneofCase.OnAlarmTransition, ev.BodyCase); Assert.Null(ev.OnDataChange); } /// Verifies that AcknowledgeAlarmRequest round-trips through serialization. [Fact] public void AcknowledgeAlarmRequest_RoundTripsAllFields() { var original = new AcknowledgeAlarmRequest { SessionId = "session-1", ClientCorrelationId = "client-correlation-7", AlarmFullReference = "Tank01.Level.HiHi", Comment = "shift handover", OperatorUser = "operator2", }; var parsed = AcknowledgeAlarmRequest.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); } /// Verifies that AcknowledgeAlarmReply round-trips with status, hresult, and diagnostics. [Fact] public void AcknowledgeAlarmReply_RoundTripsStatusAndHresult() { var original = new AcknowledgeAlarmReply { SessionId = "session-1", CorrelationId = "gateway-correlation-7", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok, DetectedBy = MxStatusSource.RespondingLmx, }, DiagnosticMessage = "ack accepted", }; var parsed = AcknowledgeAlarmReply.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.True(parsed.HasHresult); } /// Verifies that ActiveAlarmSnapshot round-trips with current state and operator metadata. [Fact] public void ActiveAlarmSnapshot_RoundTripsAllFields() { var raise = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)); var ack = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)); var original = new ActiveAlarmSnapshot { AlarmFullReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", Severity = 750, OriginalRaiseTimestamp = raise, CurrentState = AlarmConditionState.ActiveAcked, Category = "Process", Description = "Tank 01 high-high level", LastTransitionTimestamp = ack, OperatorUser = "operator2", OperatorComment = "investigating", }; var parsed = ActiveAlarmSnapshot.Parser.ParseFrom(original.ToByteArray()); Assert.Equal(original, parsed); Assert.Equal(AlarmConditionState.ActiveAcked, parsed.CurrentState); } /// Verifies that QueryActiveAlarmsRequest round-trips empty filter prefix. [Fact] public void QueryActiveAlarmsRequest_RoundTripsWithAndWithoutFilter() { var withoutFilter = new QueryActiveAlarmsRequest { SessionId = "session-1", ClientCorrelationId = "client-correlation-8", }; var withFilter = new QueryActiveAlarmsRequest { SessionId = "session-1", ClientCorrelationId = "client-correlation-9", AlarmFilterPrefix = "Tank01.", }; Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray())); Assert.Equal(withFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray())); } }