using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
using CommonsTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
using ProtoConditionState = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState;
using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;

/// <summary>Task-12: pure MxGateway alarm-feed proto → NativeAlarmTransition mapping.</summary>
public class MxGatewayAlarmMapperTests
{
    [Fact]
    public void MapTransition_AckTransition_IsAcknowledgedWithOperator()
    {
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            AlarmTypeName = "AnalogLimitAlarm.HiHi",
            TransitionKind = ProtoTransitionKind.Acknowledge,
            Severity = 600,
            OperatorUser = "operator1",
            OperatorComment = "ack",
            Category = "Process",
            Description = "hi"
        };

        var t = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.Equal(CommonsTransitionKind.Acknowledge, t.Kind);
        Assert.True(t.Condition.Active);
        Assert.True(t.Condition.Acknowledged);
        Assert.Equal(600, t.Condition.Severity);
        Assert.Equal("operator1", t.OperatorUser);
        Assert.Equal("Tank01", t.SourceObjectReference);
    }

    [Fact]
    public void MapConditionState_ActiveAcked_To_ActiveTrue_AckTrue()
    {
        var c = MxGatewayAlarmMapper.MapConditionState(ProtoConditionState.ActiveAcked, severity: 600);
        Assert.True(c.Active);
        Assert.True(c.Acknowledged);
        Assert.Equal(600, c.Severity);
    }

    [Fact]
    public void MapSnapshot_ActiveUnacked_IsSnapshotKind()
    {
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Tank01.Level.Hi",
            SourceObjectReference = "Tank01",
            AlarmTypeName = "AnalogLimitAlarm.Hi",
            CurrentState = ProtoConditionState.Active,
            Severity = 1500 // out of range — must clamp
        };

        var t = MxGatewayAlarmMapper.MapSnapshot(snap);

        Assert.Equal(CommonsTransitionKind.Snapshot, t.Kind);
        Assert.True(t.Condition.Active);
        Assert.False(t.Condition.Acknowledged);
        Assert.Equal(1000, t.Condition.Severity);
    }

    [Fact]
    public void SourceReference_IsObjectRelative_NotFullProviderReference()
    {
        // The condition identity surfaced upward is the object-relative reference
        // (e.g. "Z28061.HeartbeatTimeoutAlarm"), not the gateway's full provider
        // reference ("Galaxy!<area>.<object>.<alarm>"). Area lives in Category.
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
            SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
            AlarmTypeName = "Syst",
            Category = "CVDAisle_1",
            CurrentState = ProtoConditionState.Active,
            Severity = 400
        };
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
            SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
            AlarmTypeName = "Syst",
            TransitionKind = ProtoTransitionKind.Raise,
            Severity = 400
        };

        var snapT = MxGatewayAlarmMapper.MapSnapshot(snap);
        var liveT = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.Equal("Z28061.HeartbeatTimeoutAlarm", snapT.SourceReference);
        Assert.Equal("Z28061.HeartbeatTimeoutAlarm", liveT.SourceReference);
        Assert.Equal("CVDAisle_1", snapT.Category);
    }

    [Fact]
    public void SourceReference_FallsBackToFullReference_WhenObjectReferenceEmpty()
    {
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Galaxy!Area.Obj.Alarm",
            SourceObjectReference = "",
            CurrentState = ProtoConditionState.Active,
            Severity = 100
        };

        var t = MxGatewayAlarmMapper.MapSnapshot(snap);

        Assert.Equal("Galaxy!Area.Obj.Alarm", t.SourceReference);
    }

    // ── CurrentValue / LimitValue (M2.13 / #27) ──────────────────────────────

    [Fact]
    public void MapTransition_CurrentAndLimitValue_PopulatedFromProto()
    {
        // The gateway proto OnAlarmTransitionEvent carries current_value and
        // limit_value as MxValue union fields.  Verify both are mapped through
        // MxValueToString into the neutral NativeAlarmTransition strings.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            AlarmTypeName = "AnalogLimitAlarm.HiHi",
            TransitionKind = ProtoTransitionKind.Raise,
            Severity = 800,
            CurrentValue = 95.3.ToMxValue(),
            LimitValue = 90.0.ToMxValue()
        };

        var t = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.Equal("95.3", t.CurrentValue);
        Assert.Equal("90", t.LimitValue);
    }

    [Fact]
    public void MapTransition_AbsentCurrentAndLimitValue_YieldsEmpty()
    {
        // When the gateway sends events without current/limit value fields (optional),
        // the resulting transition must have empty strings — never null.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.Hi",
            SourceObjectReference = "Tank01",
            AlarmTypeName = "AnalogLimitAlarm.Hi",
            TransitionKind = ProtoTransitionKind.Raise,
            Severity = 600
            // CurrentValue and LimitValue not set → proto default (null reference)
        };

        var t = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.Equal("", t.CurrentValue);
        Assert.Equal("", t.LimitValue);
    }

    [Fact]
    public void MapSnapshot_CurrentAndLimitValue_PopulatedFromProto()
    {
        // ActiveAlarmSnapshot also carries current_value and limit_value.
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Pump01.Vibration.HiHi",
            SourceObjectReference = "Pump01",
            AlarmTypeName = "AnalogLimitAlarm.HiHi",
            CurrentState = ProtoConditionState.Active,
            Severity = 900,
            CurrentValue = 12.7.ToMxValue(),
            LimitValue = 10.0.ToMxValue()
        };

        var t = MxGatewayAlarmMapper.MapSnapshot(snap);

        Assert.Equal("12.7", t.CurrentValue);
        Assert.Equal("10", t.LimitValue);
    }

    [Fact]
    public void MapSnapshot_StringMxValue_ProducesStringCurrentValue()
    {
        // MxValue can carry string values (e.g. for discrete/string-type tags).
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Mode.Alarm",
            SourceObjectReference = "Mode",
            AlarmTypeName = "DiscreteAlarm",
            CurrentState = ProtoConditionState.Active,
            Severity = 500,
            CurrentValue = "FAULT".ToMxValue()
        };

        var t = MxGatewayAlarmMapper.MapSnapshot(snap);

        Assert.Equal("FAULT", t.CurrentValue);
        Assert.Equal("", t.LimitValue); // not set
    }

    // ── MES alarm-status API §6.4: AckTime stamping ──

    private static readonly DateTimeOffset GatewayTime =
        new(2026, 8, 1, 9, 30, 0, TimeSpan.Zero);

    [Fact]
    public void MapTransition_Acknowledge_StampsTheObservedTransitionTimeAsAckTime()
    {
        // The gateway feed carries NO dedicated ack timestamp, so the ack transition's own
        // time is what gets stamped — accurate to when the system saw the ack.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            AlarmTypeName = "AnalogLimitAlarm.HiHi",
            TransitionKind = ProtoTransitionKind.Acknowledge,
            Severity = 600,
            TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        var t = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.Equal(GatewayTime, t.AckTime);
        Assert.Equal(t.TransitionTime, t.AckTime);
    }

    [Fact]
    public void MapTransition_Raise_HasNoAckTime()
    {
        // Null while unacknowledged.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            TransitionKind = ProtoTransitionKind.Raise,
            Severity = 600,
            TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        Assert.Null(MxGatewayAlarmMapper.MapTransition(ev).AckTime);
    }

    [Fact]
    public void MapTransition_Retrigger_ClearsTheAckTime()
    {
        // "Cleared on re-raise": a Retrigger arrives unacknowledged, so no ack time is
        // reported even though the condition was acked a moment earlier.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            TransitionKind = ProtoTransitionKind.Retrigger,
            Severity = 600,
            TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        Assert.Null(MxGatewayAlarmMapper.MapTransition(ev).AckTime);
    }

    [Fact]
    public void MapTransition_Clear_HasNoAckTime_DespiteInactiveMappingToAcked()
    {
        // The gateway maps INACTIVE to acked = true. Without the active check every
        // return-to-normal would claim an ack the system never observed.
        var ev = new OnAlarmTransitionEvent
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            TransitionKind = ProtoTransitionKind.Clear,
            Severity = 600,
            TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        var t = MxGatewayAlarmMapper.MapTransition(ev);

        Assert.True(t.Condition.Acknowledged);   // the gateway's own mapping
        Assert.Null(t.AckTime);                  // …but no ack was observed
    }

    [Fact]
    public void MapSnapshot_ActiveAcked_RestoresAnAckTimeFromTheLastTransition()
    {
        // A (re)subscribe snapshot must not silently drop the ack instant of an already
        // acknowledged alarm — the last transition time is the best the feed supplies.
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            CurrentState = ProtoConditionState.ActiveAcked,
            Severity = 600,
            LastTransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        Assert.Equal(GatewayTime, MxGatewayAlarmMapper.MapSnapshot(snap).AckTime);
    }

    [Fact]
    public void MapSnapshot_ActiveUnacked_HasNoAckTime()
    {
        var snap = new ActiveAlarmSnapshot
        {
            AlarmFullReference = "Tank01.Level.HiHi",
            SourceObjectReference = "Tank01",
            CurrentState = ProtoConditionState.Active,
            Severity = 600,
            LastTransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
        };

        Assert.Null(MxGatewayAlarmMapper.MapSnapshot(snap).AckTime);
    }

    [Fact]
    public void SnapshotComplete_Sentinel_CarriesNoAckTime()
    {
        // The end-of-snapshot sentinel has no condition payload at all.
        Assert.Null(MxGatewayAlarmMapper.SnapshotComplete().AckTime);
    }
}
