using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;

/// <summary>
/// Unit tests for <see cref="AlarmSummaryService"/> (M7 T13 — Operator Alarm
/// Summary). The snapshot client and instance repository are substituted so the
/// fan-out + best-effort degradation + roll-up math are exercised without any
/// cluster traffic.
/// </summary>
public class AlarmSummaryServiceTests
{
    private const int SiteId = 7;
    private const string SiteIdentifier = "plant-a";

    private static readonly DateTimeOffset Now =
        new(2026, 6, 18, 12, 0, 0, TimeSpan.Zero);

    private readonly ITemplateEngineRepository _instanceRepo = Substitute.For<ITemplateEngineRepository>();
    private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
    private readonly IInstanceSnapshotClient _snapshotClient = Substitute.For<IInstanceSnapshotClient>();

    private AlarmSummaryService CreateSut() =>
        new(_instanceRepo, _siteRepo, _snapshotClient);

    private static Instance Instance(int id, string uniqueName, InstanceState state) =>
        new(uniqueName) { Id = id, SiteId = SiteId, State = state };

    private static Site Site() =>
        new("Plant A", SiteIdentifier) { Id = SiteId };

    /// <summary>Builds a native-style alarm with explicit condition flags.</summary>
    private static AlarmStateChanged NativeAlarm(
        string instance, string name, AlarmState state, int severity,
        bool active, bool acked, AlarmKind kind = AlarmKind.NativeOpcUa) =>
        new(instance, name, state, severity, Now)
        {
            Kind = kind,
            Condition = new AlarmConditionState(
                Active: active, Acknowledged: acked, Confirmed: null,
                Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: severity),
        };

    private static AlarmStateChanged ComputedAlarm(
        string instance, string name, AlarmState state, int priority) =>
        new(instance, name, state, priority, Now) { Kind = AlarmKind.Computed };

    private static DebugViewSnapshot Snapshot(string instance, params AlarmStateChanged[] alarms) =>
        new(instance, Array.Empty<AttributeValueChanged>(), alarms, Now);

    [Fact]
    public async Task GetSiteAlarmsAsync_AggregatesReportingInstances_AndRecordsTheFailedOne()
    {
        _siteRepo.GetSiteByIdAsync(SiteId, Arg.Any<CancellationToken>()).Returns(Site());

        var enabledA = Instance(1, "inst-a", InstanceState.Enabled);
        var enabledB = Instance(2, "inst-b", InstanceState.Enabled);
        var enabledC = Instance(3, "inst-c", InstanceState.Enabled);
        var disabled = Instance(4, "inst-d", InstanceState.Disabled);
        var notDeployed = Instance(5, "inst-e", InstanceState.NotDeployed);

        _instanceRepo.GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>())
            .Returns(new List<Instance> { enabledA, enabledB, enabledC, disabled, notDeployed });

        // inst-a: 2 alarms (one active native sev 800 unacked, one computed normal)
        _snapshotClient.GetSnapshotAsync(SiteIdentifier, "inst-a", Arg.Any<CancellationToken>())
            .Returns(Snapshot("inst-a",
                NativeAlarm("inst-a", "TankHi", AlarmState.Active, 800, active: true, acked: false),
                ComputedAlarm("inst-a", "PumpFault", AlarmState.Normal, 200)));

        // inst-b: 1 alarm (active native sev 500 acked)
        _snapshotClient.GetSnapshotAsync(SiteIdentifier, "inst-b", Arg.Any<CancellationToken>())
            .Returns(Snapshot("inst-b",
                NativeAlarm("inst-b", "ValveStuck", AlarmState.Active, 500, active: true, acked: true)));

        // inst-c: snapshot fetch throws → not-reporting
        _snapshotClient.GetSnapshotAsync(SiteIdentifier, "inst-c", Arg.Any<CancellationToken>())
            .Returns<DebugViewSnapshot>(_ => throw new TimeoutException("site silent"));

        var sut = CreateSut();

        var result = await sut.GetSiteAlarmsAsync(SiteId);

        // Aggregated alarm count = sum of the two reporting instances (2 + 1).
        Assert.Equal(3, result.Alarms.Count);
        Assert.Equal(2, result.Alarms.Count(r => r.InstanceUniqueName == "inst-a"));
        Assert.Single(result.Alarms, r => r.InstanceUniqueName == "inst-b");

        // The throwing instance is recorded, and only it.
        Assert.Equal(new[] { "inst-c" }, result.NotReportingInstances);

        // Disabled / not-deployed instances are never fetched.
        await _snapshotClient.DidNotReceive()
            .GetSnapshotAsync(SiteIdentifier, "inst-d", Arg.Any<CancellationToken>());
        await _snapshotClient.DidNotReceive()
            .GetSnapshotAsync(SiteIdentifier, "inst-e", Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetSiteAlarmsAsync_InstanceNotFoundSnapshot_GoesToNotReporting()
    {
        _siteRepo.GetSiteByIdAsync(SiteId, Arg.Any<CancellationToken>()).Returns(Site());
        _instanceRepo.GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>())
            .Returns(new List<Instance> { Instance(1, "ghost", InstanceState.Enabled) });

        _snapshotClient.GetSnapshotAsync(SiteIdentifier, "ghost", Arg.Any<CancellationToken>())
            .Returns(new DebugViewSnapshot(
                "ghost", Array.Empty<AttributeValueChanged>(),
                Array.Empty<AlarmStateChanged>(), Now, InstanceNotFound: true));

        var result = await CreateSut().GetSiteAlarmsAsync(SiteId);

        Assert.Empty(result.Alarms);
        Assert.Equal(new[] { "ghost" }, result.NotReportingInstances);
    }

    [Fact]
    public void ComputeRollup_ComputesWorstSeverityActiveAndUnackedAndKindCounts()
    {
        var rows = new List<AlarmSummaryRow>
        {
            // active native, sev 800, unacked → counts toward unacked
            new("inst-a", NativeAlarm("inst-a", "TankHi", AlarmState.Active, 800, active: true, acked: false)),
            // active native, sev 500, acked → not unacked
            new("inst-b", NativeAlarm("inst-b", "ValveStuck", AlarmState.Active, 500, active: true, acked: true)),
            // active mxaccess, sev 300, unacked → unacked
            new("inst-b", NativeAlarm("inst-b", "MtrTrip", AlarmState.Active, 300, active: true, acked: false, kind: AlarmKind.NativeMxAccess)),
            // computed normal (not active) → ignored by active/unacked, severity 200 not counted
            new("inst-a", ComputedAlarm("inst-a", "PumpFault", AlarmState.Normal, 200)),
            // computed active sev 999 — active, but computed so NOT unacked
            new("inst-c", ComputedAlarm("inst-c", "HiHi", AlarmState.Active, 999)),
        };

        var rollup = CreateSut().ComputeRollup(rows);

        // Active = the 3 native actives + the active computed = 4
        Assert.Equal(4, rollup.TotalActive);
        // Worst severity among active = 999 (the active computed)
        Assert.Equal(999, rollup.WorstSeverity);
        // Unacked = active && !acked && kind != Computed = TankHi + MtrTrip = 2
        Assert.Equal(2, rollup.UnackedCount);
        // Kind counts: NativeOpcUa 2, NativeMxAccess 1, Computed 2
        Assert.Equal(2, rollup.CountsByKind[AlarmKind.NativeOpcUa]);
        Assert.Equal(1, rollup.CountsByKind[AlarmKind.NativeMxAccess]);
        Assert.Equal(2, rollup.CountsByKind[AlarmKind.Computed]);
    }

    [Fact]
    public void ComputeRollup_NoActiveRows_WorstSeverityZero()
    {
        var rows = new List<AlarmSummaryRow>
        {
            new("inst-a", NativeAlarm("inst-a", "Cleared", AlarmState.Normal, 700, active: false, acked: true)),
        };

        var rollup = CreateSut().ComputeRollup(rows);

        Assert.Equal(0, rollup.TotalActive);
        Assert.Equal(0, rollup.WorstSeverity);
        Assert.Equal(0, rollup.UnackedCount);
    }

    // ── BuildFromLiveAlarms (plan #10, Task 5) ────────────────────────────────

    [Fact]
    public void BuildFromLiveAlarms_FlattensAndSorts_SameAsPollPath_WithEmptyNotReporting()
    {
        // Intentionally out-of-order input to prove the deterministic
        // instance-then-alarm-name sort matches GetSiteAlarmsAsync.
        var alarms = new List<AlarmStateChanged>
        {
            NativeAlarm("zeta", "Zulu", AlarmState.Active, 900, active: true, acked: false),
            NativeAlarm("alpha", "Bravo", AlarmState.Active, 500, active: true, acked: true),
            NativeAlarm("alpha", "Alpha", AlarmState.Active, 700, active: true, acked: false),
        };

        var result = CreateSut().BuildFromLiveAlarms(alarms);

        // One row per alarm, instance taken from the alarm itself.
        Assert.Equal(3, result.Alarms.Count);
        Assert.Collection(result.Alarms,
            r => Assert.Equal(("alpha", "Alpha"), (r.InstanceUniqueName, r.Alarm.AlarmName)),
            r => Assert.Equal(("alpha", "Bravo"), (r.InstanceUniqueName, r.Alarm.AlarmName)),
            r => Assert.Equal(("zeta", "Zulu"), (r.InstanceUniqueName, r.Alarm.AlarmName)));

        // The live path can't compute "not reporting" — always empty.
        Assert.Empty(result.NotReportingInstances);
    }

    [Fact]
    public void BuildFromLiveAlarms_EmptySnapshot_YieldsNoRows()
    {
        var result = CreateSut().BuildFromLiveAlarms(Array.Empty<AlarmStateChanged>());

        Assert.Empty(result.Alarms);
        Assert.Empty(result.NotReportingInstances);
    }
}
