using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests;

[Trait("Category", "Unit")]
public sealed class ScriptedAlarmSourceTests
{
    private static async Task<(ScriptedAlarmEngine e, ScriptedAlarmSource s, FakeUpstream u)> BuildAsync()
    {
        var up = new FakeUpstream();
        up.Set("Temp", 50);
        var logger = new LoggerConfiguration().CreateLogger();
        var engine = new ScriptedAlarmEngine(up, new InMemoryAlarmStateStore(),
            new ScriptLoggerFactory(logger), logger);
        await engine.LoadAsync([
            new ScriptedAlarmDefinition(
                "Plant/Line1::HighTemp",
                "Plant/Line1",
                "HighTemp",
                AlarmKind.LimitAlarm,
                AlarmSeverity.High,
                "Temp {Temp}C",
                """return (int)ctx.GetTag("Temp").Value > 100;"""),
            new ScriptedAlarmDefinition(
                "Plant/Line2::OtherAlarm",
                "Plant/Line2",
                "OtherAlarm",
                AlarmKind.AlarmCondition,
                AlarmSeverity.Low,
                "other",
                """return false;"""),
        ], CancellationToken.None);

        var source = new ScriptedAlarmSource(engine);
        return (engine, source, up);
    }

    /// <summary>Verifies that subscribing with an empty filter receives every alarm emission.</summary>
    [Fact]
    public async Task Subscribe_with_empty_filter_receives_every_alarm_emission()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        var events = new List<AlarmEventArgs>();
        source.OnAlarmEvent += (_, e) => events.Add(e);
        var handle = await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken);

        up.Push("Temp", 150);
        await Task.Delay(200);

        events.Count.ShouldBe(1);
        events[0].ConditionId.ShouldBe("Plant/Line1::HighTemp");
        events[0].SourceNodeId.ShouldBe("Plant/Line1");
        events[0].Severity.ShouldBe(AlarmSeverity.High);
        events[0].AlarmType.ShouldBe("LimitAlarm");
        events[0].Message.ShouldBe("Temp 150C");

        await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);
    }

    /// <summary>Verifies that subscribing with an equipment prefix filters alarms by that prefix.</summary>
    [Fact]
    public async Task Subscribe_with_equipment_prefix_filters_by_that_prefix()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        var events = new List<AlarmEventArgs>();
        source.OnAlarmEvent += (_, e) => events.Add(e);

        // Subscribe only to Line1 alarms.
        var handle = await source.SubscribeAlarmsAsync(["Plant/Line1"], TestContext.Current.CancellationToken);

        up.Push("Temp", 150);
        await Task.Delay(200);

        events.Count.ShouldBe(1);
        events[0].SourceNodeId.ShouldBe("Plant/Line1");

        await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);
    }

    /// <summary>Verifies that unsubscribing stops further alarm events.</summary>
    [Fact]
    public async Task Unsubscribe_stops_further_events()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        var events = new List<AlarmEventArgs>();
        source.OnAlarmEvent += (_, e) => events.Add(e);
        var handle = await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken);
        await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);

        up.Push("Temp", 150);
        await Task.Delay(200);

        events.Count.ShouldBe(0);
    }

    /// <summary>#478 — a QualityChanged engine emission (source-quality bucket change, no state transition)
    /// must NOT surface through the IAlarmSource fan-out: quality is delivered out of band via the
    /// dedicated node path, never as an AlarmEventArgs (which would materialize / historize a condition).</summary>
    [Fact]
    public async Task QualityChanged_emission_raises_no_alarm_event()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        var events = new List<AlarmEventArgs>();
        source.OnAlarmEvent += (_, e) => events.Add(e);
        await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken);

        // Drive HighTemp's input Bad: the predicate freezes (no transition), but the worst-quality bucket
        // moves Good→Bad → the engine emits QualityChanged. The source must swallow it.
        up.Push("Temp", 50, 0x80000000u);
        await Task.Delay(200);

        events.ShouldBeEmpty();
    }

    /// <summary>Verifies that AcknowledgeAsync routes to the engine with a default user.</summary>
    [Fact]
    public async Task AcknowledgeAsync_routes_to_engine_with_default_user()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        up.Push("Temp", 150);
        await Task.Delay(200);
        engine.GetState("Plant/Line1::HighTemp")!.Acked.ShouldBe(AlarmAckedState.Unacknowledged);

        await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
            "Plant/Line1", "Plant/Line1::HighTemp", "ack via opcua")],
            TestContext.Current.CancellationToken);

        var state = engine.GetState("Plant/Line1::HighTemp")!;
        state.Acked.ShouldBe(AlarmAckedState.Acknowledged);
        state.LastAckUser.ShouldBe("opcua-client");
        state.LastAckComment.ShouldBe("ack via opcua");
    }

    /// <summary>
    ///     Verifies that AcknowledgeAsync honors a supplied authenticated principal
    ///     (<see cref="AlarmAcknowledgeRequest.OperatorUser"/>) and falls back to
    ///     <c>"opcua-client"</c> only when none is supplied.
    /// </summary>
    [Fact]
    public async Task ScriptedAlarmSource_uses_supplied_principal_else_default()
    {
        var (engine, source, up) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        // Supplied principal -> honored.
        up.Push("Temp", 150);
        await Task.Delay(200);
        await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
            "Plant/Line1", "Plant/Line1::HighTemp", "ack as bob", "bob")],
            TestContext.Current.CancellationToken);
        engine.GetState("Plant/Line1::HighTemp")!.LastAckUser.ShouldBe("bob");

        // No principal -> default audit identity.
        up.Push("Temp", 50);
        await Task.Delay(200);
        up.Push("Temp", 150);
        await Task.Delay(200);
        await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
            "Plant/Line1", "Plant/Line1::HighTemp", "ack anon", null)],
            TestContext.Current.CancellationToken);
        engine.GetState("Plant/Line1::HighTemp")!.LastAckUser.ShouldBe("opcua-client");
    }

    /// <summary>Verifies that null arguments are rejected.</summary>
    [Fact]
    public async Task Null_arguments_rejected()
    {
        var (engine, source, _) = await BuildAsync();
        using var _e = engine;
        using var _s = source;

        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.SubscribeAlarmsAsync(null!, TestContext.Current.CancellationToken));
        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.UnsubscribeAlarmsAsync(null!, TestContext.Current.CancellationToken));
        await Should.ThrowAsync<ArgumentNullException>(async () =>
            await source.AcknowledgeAsync(null!, TestContext.Current.CancellationToken));
    }
}
