using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using IAuditInboundCeilingHitsCounter = ZB.MOM.WW.ScadaBridge.AuditLog.Central.IAuditInboundCeilingHitsCounter;

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

/// <summary>
/// Bundle E (M6-T8) regression coverage for the central-side audit-write
/// failure counter. <see cref="CentralAuditWriter"/> and
/// <see cref="AuditLogIngestActor"/> both swallow repository throws (audit
/// must NEVER abort the user-facing action, alog.md §13) but bump the
/// <see cref="ICentralAuditWriteFailureCounter"/> so the central health
/// surface (<see cref="AuditCentralHealthSnapshot"/>) can flag a sustained
/// outage.
/// </summary>
public class CentralAuditWriteFailuresTests : TestKit
{
    private static AuditEvent NewEvent() => ScadaBridgeAuditEventFactory.Create(
        eventId: Guid.NewGuid(),
        occurredAtUtc: DateTime.UtcNow,
        channel: AuditChannel.ApiOutbound,
        kind: AuditKind.ApiCall,
        status: AuditStatus.Delivered);

    /// <summary>
    /// Repository stub that always throws on insert — exercises the failure
    /// path in both <see cref="CentralAuditWriter"/> and
    /// <see cref="AuditLogIngestActor"/>.
    /// </summary>
    private sealed class ThrowingRepo : IAuditLogRepository
    {
        public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
            throw new InvalidOperationException("simulated repo failure");
        public Task<IReadOnlyList<AuditEvent>> QueryAsync(
            AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
            Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>());
        public Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
            Task.FromResult(0L);
        public Task<long> PurgeChannelOlderThanAsync(
            string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
            Task.FromResult(0L);
        public Task<long> BackfillSourceNodeAsync(
            string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
            Task.FromResult(0L);
        public Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
            DateTime threshold, CancellationToken ct = default) =>
            Task.FromResult<IReadOnlyList<DateTime>>(Array.Empty<DateTime>());
        public Task<ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot> GetKpiSnapshotAsync(
            TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
            Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));

        public Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
            Guid executionId, CancellationToken ct = default) =>
            Task.FromResult<IReadOnlyList<ExecutionTreeNode>>(Array.Empty<ExecutionTreeNode>());

        public Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
            Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
    }

    /// <summary>
    /// In-memory <see cref="ICentralAuditWriteFailureCounter"/> recording
    /// every <see cref="Increment"/> call so tests can assert on the count.
    /// </summary>
    private sealed class RecordingFailureCounter : ICentralAuditWriteFailureCounter
    {
        private int _count;
        public int Count => Volatile.Read(ref _count);
        public void Increment() => Interlocked.Increment(ref _count);
    }

    [Fact]
    public async Task Forced_Failure_Increments_Counter()
    {
        // Direct test: build the writer with a throwing scope and verify the
        // injected counter is bumped on the swallowed insert exception.
        var counter = new RecordingFailureCounter();
        var services = new ServiceCollection();
        services.AddScoped<IAuditLogRepository, ThrowingRepo>();
        var sp = services.BuildServiceProvider();

        var writer = new CentralAuditWriter(
            sp,
            NullLogger<CentralAuditWriter>.Instance,
            redactor: null,
            failureCounter: counter);

        // WriteAsync swallows the exception and increments the counter.
        await writer.WriteAsync(NewEvent());

        Assert.Equal(1, counter.Count);
    }

    [Fact]
    public async Task AuditLogIngestActor_Failure_Increments_Counter()
    {
        // The actor's production ctor resolves both IAuditLogRepository AND
        // ICentralAuditWriteFailureCounter from the scope per-message; we
        // register both and verify the per-row catch bumps the counter for
        // every row in the batch.
        var counter = new RecordingFailureCounter();
        var services = new ServiceCollection();
        services.AddScoped<IAuditLogRepository, ThrowingRepo>();
        // Counter is a singleton — the actor's per-message scope still
        // resolves the same instance via the scope's parent provider.
        services.AddSingleton<ICentralAuditWriteFailureCounter>(counter);
        var sp = services.BuildServiceProvider();

        var actor = Sys.ActorOf(Props.Create(() => new AuditLogIngestActor(
            sp, NullLogger<AuditLogIngestActor>.Instance)));

        var batch = new[] { NewEvent(), NewEvent(), NewEvent() };
        var reply = await actor.Ask<IngestAuditEventsReply>(
            new IngestAuditEventsCommand(batch), TimeSpan.FromSeconds(5));

        // Every row threw → none accepted, counter bumped once per row.
        Assert.Empty(reply.AcceptedEventIds);
        Assert.Equal(batch.Length, counter.Count);
    }

    [Fact]
    public void Snapshot_Aggregates_Counters_And_StalledState()
    {
        // AuditCentralHealthSnapshot implements both writer surfaces; bumping
        // through the writer interfaces is reflected on the read surface, and
        // the per-site stalled state is fed in via ApplyStalled — production
        // wires that to a SiteAuditTelemetryStalledTracker, but the snapshot
        // is testable in isolation against the same Apply surface.
        var snapshot = new AuditCentralHealthSnapshot();

        Assert.Equal(0, snapshot.CentralAuditWriteFailures);
        Assert.Equal(0, snapshot.AuditRedactionFailure);
        Assert.Empty(snapshot.SiteAuditTelemetryStalled);

        ((ICentralAuditWriteFailureCounter)snapshot).Increment();
        ((ICentralAuditWriteFailureCounter)snapshot).Increment();
        ((ZB.MOM.WW.ScadaBridge.AuditLog.Payload.IAuditRedactionFailureCounter)snapshot).Increment();

        // Wire the tracker so an EventStream publish reaches the snapshot.
        // The tracker pushes into the snapshot's ApplyStalled when given
        // the snapshot in its ctor; the tracker also keeps its own latch,
        // but the snapshot read surface is what the central UI reads.
        using var tracker = new SiteAuditTelemetryStalledTracker(Sys, snapshot);
        Sys.EventStream.Publish(new SiteAuditTelemetryStalledChanged("siteA", Stalled: true));
        AwaitAssert(() =>
            {
                var stalledMap = snapshot.SiteAuditTelemetryStalled;
                Assert.True(stalledMap.TryGetValue("siteA", out var s) && s,
                    "expected siteA to be stalled in snapshot");
            },
            duration: TimeSpan.FromSeconds(2),
            interval: TimeSpan.FromMilliseconds(20));

        Assert.Equal(2, snapshot.CentralAuditWriteFailures);
        Assert.Equal(1, snapshot.AuditRedactionFailure);
    }

    [Fact]
    public void Snapshot_Empty_OnConstruction()
    {
        // Sanity: the snapshot's three properties start at their zero values
        // before any writer or stalled-event publication.
        var snapshot = new AuditCentralHealthSnapshot();
        Assert.Equal(0, snapshot.CentralAuditWriteFailures);
        Assert.Equal(0, snapshot.AuditRedactionFailure);
        Assert.Equal(0, snapshot.AuditInboundCeilingHits);
        Assert.Empty(snapshot.SiteAuditTelemetryStalled);
    }

    // ---------------------------------------------------------------------
    // M5.3 (T7) AuditInboundCeilingHits counter
    // AuditCentralHealthSnapshot implements IAuditInboundCeilingHitsCounter.
    // Incrementing through the interface surface is reflected on the snapshot.
    // ---------------------------------------------------------------------

    [Fact]
    public void AuditInboundCeilingHits_StartsAtZero()
    {
        var snapshot = new AuditCentralHealthSnapshot();
        Assert.Equal(0, snapshot.AuditInboundCeilingHits);
    }

    [Fact]
    public void AuditInboundCeilingHits_IncrementedThroughInterface_ReflectedOnSnapshot()
    {
        var snapshot = new AuditCentralHealthSnapshot();
        var counter = (IAuditInboundCeilingHitsCounter)snapshot;

        counter.Increment();
        counter.Increment();
        counter.Increment();

        Assert.Equal(3, snapshot.AuditInboundCeilingHits);
    }

    [Fact]
    public void AuditInboundCeilingHits_IsThreadSafe()
    {
        // Interlocked increment must produce the correct count under concurrent
        // increments — same shape as the existing counter tests.
        var snapshot = new AuditCentralHealthSnapshot();
        var counter = (IAuditInboundCeilingHitsCounter)snapshot;
        const int incrementCount = 1000;

        Parallel.For(0, incrementCount, _ => counter.Increment());

        Assert.Equal(incrementCount, snapshot.AuditInboundCeilingHits);
    }

    [Fact]
    public void AuditInboundCeilingHits_IsIndependentOfOtherCounters()
    {
        // Ceiling-hits increments must not cross-contaminate the other counters
        // and vice versa — each Interlocked field is independent.
        var snapshot = new AuditCentralHealthSnapshot();
        var ceilingCounter = (IAuditInboundCeilingHitsCounter)snapshot;
        var writeCounter = (ICentralAuditWriteFailureCounter)snapshot;
        var redactCounter = (ZB.MOM.WW.ScadaBridge.AuditLog.Payload.IAuditRedactionFailureCounter)snapshot;

        ceilingCounter.Increment();
        ceilingCounter.Increment();
        writeCounter.Increment();
        redactCounter.Increment();
        redactCounter.Increment();
        redactCounter.Increment();

        Assert.Equal(2, snapshot.AuditInboundCeilingHits);
        Assert.Equal(1, snapshot.CentralAuditWriteFailures);
        Assert.Equal(3, snapshot.AuditRedactionFailure);
    }
}
