using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring.Kpi;

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

/// <summary>
/// M6 "KPI History &amp; Trends" (K9) coverage for
/// <see cref="SiteHealthKpiSampleSource"/>. The source reads the in-memory
/// <see cref="ICentralHealthAggregator"/> and emits per-site
/// (<see cref="KpiScopes.Site"/>) <see cref="KpiSample"/> rows for each site that
/// has reported a <see cref="SiteHealthReport"/>; sites known only via heartbeats
/// (null <see cref="SiteHealthState.LatestReport"/>) contribute nothing.
/// </summary>
public class SiteHealthKpiSampleSourceTests
{
    private static readonly DateTime CapturedAt =
        new(2026, 6, 17, 12, 0, 0, DateTimeKind.Utc);

    [Fact]
    public void Source_Is_SiteHealth()
    {
        var source = new SiteHealthKpiSampleSource(new StubAggregator());
        Assert.Equal(KpiSources.SiteHealth, source.Source);
    }

    [Fact]
    public async Task CollectAsync_PopulatedSite_EmitsAllMetrics_NullReportSite_EmitsNothing()
    {
        // site-a: fully-populated report. site-b: heartbeat-only (null report).
        var report = new SiteHealthReport(
            SiteId: "site-a",
            SequenceNumber: 5,
            ReportTimestamp: CapturedAt,
            DataConnectionStatuses: new Dictionary<string, ConnectionHealth>
            {
                ["conn-1"] = ConnectionHealth.Connected,
                ["conn-2"] = ConnectionHealth.Connected,
                ["conn-3"] = ConnectionHealth.Disconnected,
                ["conn-4"] = ConnectionHealth.Connecting,
                ["conn-5"] = ConnectionHealth.Error,
            },
            TagResolutionCounts: new Dictionary<string, TagResolutionStatus>(),
            ScriptErrorCount: 3,
            AlarmEvaluationErrorCount: 4,
            StoreAndForwardBufferDepths: new Dictionary<string, int>
            {
                ["buf-1"] = 10,
                ["buf-2"] = 15,
            },
            DeadLetterCount: 6,
            DeployedInstanceCount: 20,
            EnabledInstanceCount: 18,
            DisabledInstanceCount: 2,
            ParkedMessageCount: 7,
            SiteAuditBacklog: new SiteAuditBacklogSnapshot(
                PendingCount: 9, OldestPendingUtc: null, OnDiskBytes: 0),
            SiteEventLogWriteFailures: 11);

        var aggregator = new StubAggregator
        {
            States =
            {
                ["site-a"] = new SiteHealthState { SiteId = "site-a", LatestReport = report },
                ["site-b"] = new SiteHealthState { SiteId = "site-b", LatestReport = null },
            },
        };

        var source = new SiteHealthKpiSampleSource(aggregator);

        var samples = await source.CollectAsync(CapturedAt);

        // Every sample is for site-a only — the null-report site yields nothing.
        Assert.All(samples, s =>
        {
            Assert.Equal(KpiSources.SiteHealth, s.Source);
            Assert.Equal(KpiScopes.Site, s.Scope);
            Assert.Equal("site-a", s.ScopeKey);
            Assert.Equal(CapturedAt, s.CapturedAtUtc);
        });
        Assert.DoesNotContain(samples, s => s.ScopeKey == "site-b");

        // Exact (Metric, Value) tuples for the populated site.
        var byMetric = samples.ToDictionary(s => s.Metric, s => s.Value);

        Assert.Equal(2, byMetric["connectionsUp"]);    // 2 Connected
        Assert.Equal(3, byMetric["connectionsDown"]);  // Disconnected + Connecting + Error
        Assert.Equal(3, byMetric["scriptErrors"]);
        Assert.Equal(4, byMetric["alarmEvalErrors"]);
        Assert.Equal(25, byMetric["sfBufferDepth"]);   // 10 + 15
        Assert.Equal(6, byMetric["deadLetters"]);
        Assert.Equal(7, byMetric["parkedMessages"]);
        Assert.Equal(20, byMetric["deployedInstances"]);
        Assert.Equal(18, byMetric["enabledInstances"]);
        Assert.Equal(2, byMetric["disabledInstances"]);
        Assert.Equal(9, byMetric["auditBacklogPending"]);
        Assert.Equal(11, byMetric["eventLogWriteFailures"]);

        // All 12 metrics emitted, exactly once each, for the one populated site.
        Assert.Equal(12, byMetric.Count);
        Assert.Equal(12, samples.Count);
    }

    [Fact]
    public async Task CollectAsync_NullAuditBacklog_EmitsZeroAuditBacklogPending()
    {
        var report = MinimalReport("site-a") with { SiteAuditBacklog = null };
        var aggregator = new StubAggregator
        {
            States = { ["site-a"] = new SiteHealthState { SiteId = "site-a", LatestReport = report } },
        };

        var samples = await new SiteHealthKpiSampleSource(aggregator).CollectAsync(CapturedAt);

        Assert.Equal(0, samples.Single(s => s.Metric == "auditBacklogPending").Value);
    }

    [Fact]
    public async Task CollectAsync_NoSitesWithReports_ReturnsEmptyList()
    {
        var aggregator = new StubAggregator
        {
            States = { ["site-b"] = new SiteHealthState { SiteId = "site-b", LatestReport = null } },
        };

        var samples = await new SiteHealthKpiSampleSource(aggregator).CollectAsync(CapturedAt);

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

    private static SiteHealthReport MinimalReport(string siteId) =>
        new(
            SiteId: siteId,
            SequenceNumber: 1,
            ReportTimestamp: CapturedAt,
            DataConnectionStatuses: new Dictionary<string, ConnectionHealth>(),
            TagResolutionCounts: new Dictionary<string, TagResolutionStatus>(),
            ScriptErrorCount: 0,
            AlarmEvaluationErrorCount: 0,
            StoreAndForwardBufferDepths: new Dictionary<string, int>(),
            DeadLetterCount: 0,
            DeployedInstanceCount: 0,
            EnabledInstanceCount: 0,
            DisabledInstanceCount: 0);

    /// <summary>
    /// Hand-rolled <see cref="ICentralHealthAggregator"/> stub — the
    /// HealthMonitoring.Tests project has no mocking library. Only
    /// <see cref="GetAllSiteStates"/> is exercised by the source under test.
    /// </summary>
    private sealed class StubAggregator : ICentralHealthAggregator
    {
        public Dictionary<string, SiteHealthState> States { get; } = new();

        public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() => States;

        public SiteHealthState? GetSiteState(string siteId) =>
            States.TryGetValue(siteId, out var state) ? state : null;

        public void ProcessReport(SiteHealthReport report) =>
            throw new NotSupportedException();

        public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) =>
            throw new NotSupportedException();

        public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) =>
            throw new NotSupportedException();
    }
}
