using NSubstitute;
using ZB.MOM.WW.ScadaBridge.AuditLog.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Kpi;

/// <summary>
/// M6 "KPI History &amp; Trends" (K8) coverage for <see cref="AuditLogKpiSampleSource"/> —
/// the <see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi.IKpiSampleSource"/> the
/// central recorder enumerates to snapshot Audit Log KPIs. Asserts the source maps the
/// repository's <see cref="AuditLogKpiSnapshot"/> onto exactly three Global-scope samples
/// with the canonical metric names, the recorder-supplied capture instant, and a null
/// <c>ScopeKey</c>.
/// </summary>
public class AuditLogKpiSampleSourceTests
{
    private static readonly DateTime CapturedAt = new(2026, 6, 17, 12, 30, 0, DateTimeKind.Utc);

    [Fact]
    public void Source_is_AuditLog()
    {
        var repo = Substitute.For<IAuditLogRepository>();
        var sut = new AuditLogKpiSampleSource(repo);

        Assert.Equal(KpiSources.AuditLog, sut.Source);
    }

    [Fact]
    public async Task CollectAsync_emits_three_global_samples_mapped_from_snapshot()
    {
        // Distinct values so a swapped mapping (e.g. error ↔ backlog) is caught.
        var snapshot = new AuditLogKpiSnapshot(
            TotalEventsLastHour: 4200,
            ErrorEventsLastHour: 17,
            BacklogTotal: 305,
            AsOfUtc: new DateTime(2026, 6, 17, 12, 30, 5, DateTimeKind.Utc));

        var repo = Substitute.For<IAuditLogRepository>();
        repo.GetKpiSnapshotAsync(Arg.Any<TimeSpan>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
            .Returns(snapshot);

        var sut = new AuditLogKpiSampleSource(repo);

        var samples = await sut.CollectAsync(CapturedAt);

        Assert.Equal(3, samples.Count);

        // Every sample: AuditLog source, Global scope, null ScopeKey, the
        // recorder-stamped capture instant.
        Assert.All(samples, s =>
        {
            Assert.Equal(KpiSources.AuditLog, s.Source);
            Assert.Equal(KpiScopes.Global, s.Scope);
            Assert.Null(s.ScopeKey);
            Assert.Equal(CapturedAt, s.CapturedAtUtc);
        });

        // Exact (Metric, Value) catalog mapping.
        AssertMetric(samples, "totalEventsLastHour", 4200);
        AssertMetric(samples, "errorEventsLastHour", 17);
        AssertMetric(samples, "backlogTotal", 305);
    }

    [Fact]
    public async Task CollectAsync_anchors_the_window_on_capturedAtUtc()
    {
        var repo = Substitute.For<IAuditLogRepository>();
        repo.GetKpiSnapshotAsync(Arg.Any<TimeSpan>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
            .Returns(new AuditLogKpiSnapshot(0, 0, 0, CapturedAt));

        var sut = new AuditLogKpiSampleSource(repo);

        await sut.CollectAsync(CapturedAt);

        // The trailing 1h window is anchored on the recorder's shared instant,
        // not the repository's server-side UtcNow, so the sample is reproducible.
        await repo.Received(1).GetKpiSnapshotAsync(
            TimeSpan.FromHours(1),
            CapturedAt,
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task CollectAsync_returns_empty_when_snapshot_is_null()
    {
        var repo = Substitute.For<IAuditLogRepository>();
        repo.GetKpiSnapshotAsync(Arg.Any<TimeSpan>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
            .Returns((AuditLogKpiSnapshot?)null!);

        var sut = new AuditLogKpiSampleSource(repo);

        var samples = await sut.CollectAsync(CapturedAt);

        Assert.NotNull(samples);
        Assert.Empty(samples);
    }

    [Fact]
    public async Task CollectAsync_uses_backlog_provider_when_present()
    {
        // The repository's snapshot carries the hardwired zero the append-only
        // repo always emits; the provider carries the real cross-site aggregate.
        var snapshot = new AuditLogKpiSnapshot(
            TotalEventsLastHour: 10,
            ErrorEventsLastHour: 1,
            BacklogTotal: 0,
            AsOfUtc: CapturedAt);

        var repo = Substitute.For<IAuditLogRepository>();
        repo.GetKpiSnapshotAsync(Arg.Any<TimeSpan>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
            .Returns(snapshot);

        var backlog = Substitute.For<IAuditBacklogProvider>();
        backlog.GetPendingBacklogTotal().Returns(42L);

        var sut = new AuditLogKpiSampleSource(repo, backlog);

        var samples = await sut.CollectAsync(CapturedAt);

        // Volume + error metrics still come from the snapshot; backlog is the
        // provider's real aggregate, not the snapshot's zero.
        AssertMetric(samples, "totalEventsLastHour", 10);
        AssertMetric(samples, "errorEventsLastHour", 1);
        AssertMetric(samples, "backlogTotal", 42);
    }

    [Fact]
    public async Task CollectAsync_falls_back_to_snapshot_backlog_when_provider_absent()
    {
        // No provider (sites / unit tests): the sample source degrades to the
        // snapshot's own BacklogTotal — unchanged pre-Task-10 behavior.
        var snapshot = new AuditLogKpiSnapshot(
            TotalEventsLastHour: 10,
            ErrorEventsLastHour: 1,
            BacklogTotal: 7,
            AsOfUtc: CapturedAt);

        var repo = Substitute.For<IAuditLogRepository>();
        repo.GetKpiSnapshotAsync(Arg.Any<TimeSpan>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
            .Returns(snapshot);

        var sut = new AuditLogKpiSampleSource(repo, backlogProvider: null);

        var samples = await sut.CollectAsync(CapturedAt);

        AssertMetric(samples, "backlogTotal", 7);
    }

    private static void AssertMetric(
        IReadOnlyList<Commons.Entities.Kpi.KpiSample> samples,
        string metric,
        double expectedValue)
    {
        var sample = Assert.Single(samples, s => s.Metric == metric);
        Assert.Equal(expectedValue, sample.Value);
    }
}
