using Google.Protobuf.WellKnownTypes;
using Shouldly;
using ZB.MOM.WW.OtOpcUa.Commons.Protos;
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry;

/// <summary>
///     Unit tests for <see cref="TelemetryProtoMapCentral"/> — the central-side proto→domain
///     projection. Each kind asserts every field round-trips, including the null-vs-absent presence
///     handling for nullable Timestamps, optional strings, and the tri-state optional bool. A coverage
///     guard proves every <see cref="TelemetryProtoContract.HandledCases"/> value has a converter.
/// </summary>
public sealed class TelemetryProtoMapCentralTests
{
    private static readonly DateTime SampleUtc =
        new(2026, 7, 23, 10, 30, 45, DateTimeKind.Utc);

    private static readonly DateTime OtherUtc =
        new(2026, 7, 23, 9, 15, 0, DateTimeKind.Utc);

    [Fact]
    public void ToAlarm_transcribes_every_field_with_nullables_present()
    {
        var proto = new AlarmTransition
        {
            AlarmId = "Plant/Modbus/dev1/Speed",
            EquipmentPath = "Area/Line/Equip",
            AlarmName = "HighSpeed",
            TransitionKind = "Activated",
            Severity = 700,
            Message = "Speed high",
            User = "operator1",
            TimestampUtc = Timestamp.FromDateTime(SampleUtc),
            AlarmTypeName = "LimitAlarm",
            Comment = "ack comment",
            HistorizeToAveva = false,
        };
        proto.ReferencingEquipmentPaths.AddRange(["Area/Line/E1", "Area/Line/E2"]);

        var e = TelemetryProtoMapCentral.ToAlarm(proto);

        e.AlarmId.ShouldBe("Plant/Modbus/dev1/Speed");
        e.EquipmentPath.ShouldBe("Area/Line/Equip");
        e.AlarmName.ShouldBe("HighSpeed");
        e.TransitionKind.ShouldBe("Activated");
        e.Severity.ShouldBe(700);
        e.Message.ShouldBe("Speed high");
        e.User.ShouldBe("operator1");
        e.TimestampUtc.ShouldBe(SampleUtc);
        e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc);
        e.AlarmTypeName.ShouldBe("LimitAlarm");
        e.Comment.ShouldBe("ack comment");
        e.HistorizeToAveva.ShouldBe(false);
        e.ReferencingEquipmentPaths.ShouldBe(["Area/Line/E1", "Area/Line/E2"]);
    }

    [Fact]
    public void ToAlarm_absent_optionals_map_to_null_and_empty_repeated_to_empty_list()
    {
        var proto = new AlarmTransition
        {
            AlarmId = "a",
            EquipmentPath = "p",
            AlarmName = "n",
            TransitionKind = "Cleared",
            Severity = 1,
            Message = "m",
            User = "system",
            TimestampUtc = Timestamp.FromDateTime(SampleUtc),
            AlarmTypeName = "AlarmCondition",
            // Comment, HistorizeToAveva unset; no referencing paths added.
        };

        var e = TelemetryProtoMapCentral.ToAlarm(proto);

        e.Comment.ShouldBeNull();
        e.HistorizeToAveva.ShouldBeNull();
        e.ReferencingEquipmentPaths.ShouldNotBeNull();
        e.ReferencingEquipmentPaths.ShouldBeEmpty();
    }

    [Fact]
    public void ToAlarm_historize_present_true_round_trips()
    {
        var proto = new AlarmTransition
        {
            AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
            Severity = 1, Message = "m", User = "system",
            TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition",
            HistorizeToAveva = true,
        };

        TelemetryProtoMapCentral.ToAlarm(proto).HistorizeToAveva.ShouldBe(true);
    }

    [Fact]
    public void ToScript_transcribes_every_field_with_optionals_present()
    {
        var proto = new ScriptLog
        {
            ScriptId = "script-1",
            Level = "Information",
            Message = "hello",
            TimestampUtc = Timestamp.FromDateTime(SampleUtc),
            VirtualTagId = "vt-1",
            AlarmId = "al-1",
            EquipmentId = "eq-1",
        };

        var e = TelemetryProtoMapCentral.ToScript(proto);

        e.ScriptId.ShouldBe("script-1");
        e.Level.ShouldBe("Information");
        e.Message.ShouldBe("hello");
        e.TimestampUtc.ShouldBe(SampleUtc);
        e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc);
        e.VirtualTagId.ShouldBe("vt-1");
        e.AlarmId.ShouldBe("al-1");
        e.EquipmentId.ShouldBe("eq-1");
    }

    [Fact]
    public void ToScript_absent_optionals_map_to_null()
    {
        var proto = new ScriptLog
        {
            ScriptId = "script-1",
            Level = "Error",
            Message = "boom",
            TimestampUtc = Timestamp.FromDateTime(SampleUtc),
            // VirtualTagId, AlarmId, EquipmentId unset.
        };

        var e = TelemetryProtoMapCentral.ToScript(proto);

        e.VirtualTagId.ShouldBeNull();
        e.AlarmId.ShouldBeNull();
        e.EquipmentId.ShouldBeNull();
    }

    [Fact]
    public void ToHealth_transcribes_every_field_with_nullables_present()
    {
        var proto = new DriverHealth
        {
            ClusterId = "c1",
            DriverInstanceId = "d1",
            State = "Faulted",
            LastSuccessfulReadUtc = Timestamp.FromDateTime(OtherUtc),
            LastError = "timeout",
            ErrorCount5Min = 3,
            PublishedUtc = Timestamp.FromDateTime(SampleUtc),
        };

        var e = TelemetryProtoMapCentral.ToHealth(proto);

        e.ClusterId.ShouldBe("c1");
        e.DriverInstanceId.ShouldBe("d1");
        e.State.ShouldBe("Faulted");
        e.LastSuccessfulReadUtc.ShouldBe(OtherUtc);
        e.LastSuccessfulReadUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
        e.LastError.ShouldBe("timeout");
        e.ErrorCount5Min.ShouldBe(3);
        e.PublishedUtc.ShouldBe(SampleUtc);
        e.PublishedUtc.Kind.ShouldBe(DateTimeKind.Utc);
    }

    [Fact]
    public void ToHealth_absent_nullable_timestamp_and_optional_string_map_to_null()
    {
        var proto = new DriverHealth
        {
            ClusterId = "c1",
            DriverInstanceId = "d1",
            State = "Healthy",
            ErrorCount5Min = 0,
            PublishedUtc = Timestamp.FromDateTime(SampleUtc),
            // LastSuccessfulReadUtc, LastError unset.
        };

        var e = TelemetryProtoMapCentral.ToHealth(proto);

        e.LastSuccessfulReadUtc.ShouldBeNull();
        e.LastError.ShouldBeNull();
    }

    [Fact]
    public void ToResilience_transcribes_every_field_with_nullable_present()
    {
        var proto = new DriverResilienceStatus
        {
            DriverInstanceId = "d1",
            HostName = "host-a",
            BreakerOpen = true,
            ConsecutiveFailures = 5,
            CurrentInFlight = 2,
            LastBreakerOpenUtc = Timestamp.FromDateTime(OtherUtc),
            LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
            PublishedUtc = Timestamp.FromDateTime(SampleUtc),
        };

        var e = TelemetryProtoMapCentral.ToResilience(proto);

        e.DriverInstanceId.ShouldBe("d1");
        e.HostName.ShouldBe("host-a");
        e.BreakerOpen.ShouldBeTrue();
        e.ConsecutiveFailures.ShouldBe(5);
        e.CurrentInFlight.ShouldBe(2);
        e.LastBreakerOpenUtc.ShouldBe(OtherUtc);
        e.LastBreakerOpenUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
        e.LastSampledUtc.ShouldBe(SampleUtc);
        e.PublishedUtc.ShouldBe(SampleUtc);
    }

    [Fact]
    public void ToResilience_absent_nullable_timestamp_maps_to_null()
    {
        var proto = new DriverResilienceStatus
        {
            DriverInstanceId = "d1",
            HostName = "host-a",
            BreakerOpen = false,
            ConsecutiveFailures = 0,
            CurrentInFlight = 0,
            LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
            PublishedUtc = Timestamp.FromDateTime(SampleUtc),
            // LastBreakerOpenUtc unset.
        };

        TelemetryProtoMapCentral.ToResilience(proto).LastBreakerOpenUtc.ShouldBeNull();
    }

    [Fact]
    public void ToAlarm_missing_required_timestamp_throws_clear_InvalidOperation()
    {
        var proto = new AlarmTransition
        {
            AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
            Severity = 1, Message = "m", User = "system", AlarmTypeName = "AlarmCondition",
            // TimestampUtc deliberately unset — an out-of-contract sender.
        };

        var ex = Should.Throw<InvalidOperationException>(() => TelemetryProtoMapCentral.ToAlarm(proto));
        ex.Message.ShouldContain("AlarmTransition");
        ex.Message.ShouldContain("timestamp_utc");
    }

    [Fact]
    public void ToScript_missing_required_timestamp_throws_clear_InvalidOperation()
    {
        var proto = new ScriptLog { ScriptId = "s", Level = "Information", Message = "m" };

        var ex = Should.Throw<InvalidOperationException>(() => TelemetryProtoMapCentral.ToScript(proto));
        ex.Message.ShouldContain("ScriptLog");
        ex.Message.ShouldContain("timestamp_utc");
    }

    [Fact]
    public void ToHealth_missing_required_timestamp_throws_clear_InvalidOperation()
    {
        var proto = new DriverHealth
        {
            ClusterId = "c", DriverInstanceId = "d", State = "Healthy", ErrorCount5Min = 0,
            // PublishedUtc unset.
        };

        var ex = Should.Throw<InvalidOperationException>(() => TelemetryProtoMapCentral.ToHealth(proto));
        ex.Message.ShouldContain("DriverHealth");
        ex.Message.ShouldContain("published_utc");
    }

    [Fact]
    public void ToResilience_missing_required_timestamp_throws_clear_InvalidOperation()
    {
        var proto = new DriverResilienceStatus
        {
            DriverInstanceId = "d", HostName = "h", BreakerOpen = false,
            ConsecutiveFailures = 0, CurrentInFlight = 0,
            // LastSampledUtc + PublishedUtc unset.
        };

        var ex = Should.Throw<InvalidOperationException>(() => TelemetryProtoMapCentral.ToResilience(proto));
        ex.Message.ShouldContain("DriverResilienceStatus");
    }

    [Fact]
    public void MapEvent_routes_each_case_to_its_record_type()
    {
        var alarm = new TelemetryEvent { AlarmTransition = MinimalAlarm() };
        var script = new TelemetryEvent { ScriptLog = MinimalScript() };
        var health = new TelemetryEvent { DriverHealth = MinimalHealth() };
        var resilience = new TelemetryEvent { DriverResilience = MinimalResilience() };

        TelemetryProtoMapCentral.MapEvent(alarm).ShouldBeOfType<Commons.Messages.Alerts.AlarmTransitionEvent>();
        TelemetryProtoMapCentral.MapEvent(script).ShouldBeOfType<Commons.Messages.Logging.ScriptLogEntry>();
        TelemetryProtoMapCentral.MapEvent(health).ShouldBeOfType<Commons.Messages.Drivers.DriverHealthChanged>();
        TelemetryProtoMapCentral.MapEvent(resilience).ShouldBeOfType<Commons.Messages.Drivers.DriverResilienceStatusChanged>();
    }

    [Fact]
    public void MapEvent_unset_case_throws_NotSupported()
    {
        Should.Throw<NotSupportedException>(() => TelemetryProtoMapCentral.MapEvent(new TelemetryEvent()));
    }

    /// <summary>
    ///     Coverage guard: every <see cref="TelemetryProtoContract.HandledCases"/> value must be routed
    ///     by <see cref="TelemetryProtoMapCentral.MapEvent"/> to a non-null domain record. Adding a
    ///     fifth oneof case (and thus a fifth HandledCases entry) without a converter arm fails here.
    /// </summary>
    [Fact]
    public void MapEvent_covers_every_handled_case()
    {
        foreach (var handled in TelemetryProtoContract.HandledCases)
        {
            var evt = BuildFor(handled);
            evt.EventCase.ShouldBe(handled);
            TelemetryProtoMapCentral.MapEvent(evt).ShouldNotBeNull();
        }
    }

    private static TelemetryEvent BuildFor(TelemetryEvent.EventOneofCase handled) => handled switch
    {
        TelemetryEvent.EventOneofCase.AlarmTransition => new TelemetryEvent { AlarmTransition = MinimalAlarm() },
        TelemetryEvent.EventOneofCase.ScriptLog => new TelemetryEvent { ScriptLog = MinimalScript() },
        TelemetryEvent.EventOneofCase.DriverHealth => new TelemetryEvent { DriverHealth = MinimalHealth() },
        TelemetryEvent.EventOneofCase.DriverResilience => new TelemetryEvent { DriverResilience = MinimalResilience() },
        _ => throw new InvalidOperationException($"Test does not know how to build case {handled}."),
    };

    private static AlarmTransition MinimalAlarm() => new()
    {
        AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated",
        Severity = 1, Message = "m", User = "system",
        TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition",
    };

    private static ScriptLog MinimalScript() => new()
    {
        ScriptId = "s", Level = "Information", Message = "m",
        TimestampUtc = Timestamp.FromDateTime(SampleUtc),
    };

    private static DriverHealth MinimalHealth() => new()
    {
        ClusterId = "c", DriverInstanceId = "d", State = "Healthy",
        ErrorCount5Min = 0, PublishedUtc = Timestamp.FromDateTime(SampleUtc),
    };

    private static DriverResilienceStatus MinimalResilience() => new()
    {
        DriverInstanceId = "d", HostName = "h", BreakerOpen = false,
        ConsecutiveFailures = 0, CurrentInFlight = 0,
        LastSampledUtc = Timestamp.FromDateTime(SampleUtc),
        PublishedUtc = Timestamp.FromDateTime(SampleUtc),
    };
}
