using Akka.Actor;
using Akka.TestKit.Xunit2;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using Microsoft.Extensions.DependencyInjection;

namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc;

/// <summary>
/// Audit Log (#23) site→central ingest routing on <see cref="CentralControlGrpcService"/>.
/// <para>
/// The service Asks the <c>audit-log-ingest</c> singleton proxy DIRECTLY. It used to relay
/// through <see cref="CentralCommunicationActor"/>, which re-Asked the same proxy with the same
/// 30 s <see cref="SiteStreamGrpcServer.AuditIngestAskTimeout"/> — a second hop whose inner Ask
/// expired at the same instant as the outer one, so it could only add latency. These tests pin
/// the direct dispatch, the wiring-race reply, and the fact that the relay is gone.
/// </para>
/// </summary>
public class CentralControlGrpcServiceAuditIngestTests : TestKit
{
    private static ServerCallContext NewContext(CancellationToken ct = default)
    {
        var context = Substitute.For<ServerCallContext>();
        context.CancellationToken.Returns(ct);
        return context;
    }

    private static CentralControlGrpcService CreateService() => new(
        NullLogger<CentralControlGrpcService>.Instance,
        Options.Create(new CommunicationOptions()));

    [Fact]
    public async Task IngestAuditEvents_AsksTheIngestProxy_AndNeverTheCommunicationActor()
    {
        var ingest = CreateTestProbe();
        var control = CreateTestProbe();
        var service = CreateService();
        service.SetReady(control.Ref);
        service.SetAuditIngestActor(ingest.Ref);

        var evt = SampleAuditEvent();
        var batch = new AuditEventBatch();
        batch.Events.Add(AuditEventDtoMapper.ToDto(evt));

        var call = service.IngestAuditEvents(batch, NewContext());

        var received = ingest.ExpectMsg<IngestAuditEventsCommand>();
        Assert.Equal(evt.EventId, Assert.Single(received.Events).EventId);
        ingest.Reply(new IngestAuditEventsReply(new[] { evt.EventId }));

        var ack = await call;
        Assert.Equal(evt.EventId.ToString(), Assert.Single(ack.AcceptedEventIds));

        // The old relay hop is gone: the control-plane actor sees nothing at all.
        control.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    [Fact]
    public async Task IngestCachedTelemetry_AsksTheIngestProxy_AndNeverTheCommunicationActor()
    {
        var ingest = CreateTestProbe();
        var control = CreateTestProbe();
        var service = CreateService();
        service.SetReady(control.Ref);
        service.SetAuditIngestActor(ingest.Ref);

        var evt = SampleAuditEvent();
        var batch = new CachedTelemetryBatch();
        batch.Packets.Add(new CachedTelemetryPacket
        {
            AuditEvent = AuditEventDtoMapper.ToDto(evt),
            Operational = SiteCallDtoMapper.ToDto(SampleSiteCall()),
        });

        var call = service.IngestCachedTelemetry(batch, NewContext());

        var received = ingest.ExpectMsg<IngestCachedTelemetryCommand>();
        Assert.Single(received.Entries);
        ingest.Reply(new IngestCachedTelemetryReply(new[] { evt.EventId }));

        var ack = await call;
        Assert.Equal(evt.EventId.ToString(), Assert.Single(ack.AcceptedEventIds));
        control.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
    }

    [Fact]
    public async Task IngestAuditEvents_BeforeTheIngestProxyIsWired_ReturnsAnEmptyAck()
    {
        // The singleton starts moments after SetReady, so this window is real. An empty ack
        // (NOT a fault, NOT Unavailable) leaves the site's rows Pending for the next drain —
        // byte-for-byte what the removed relay replied when its proxy was still null.
        var service = CreateService();
        service.SetReady(CreateTestProbe().Ref);

        var batch = new AuditEventBatch();
        batch.Events.Add(AuditEventDtoMapper.ToDto(SampleAuditEvent()));

        var ack = await service.IngestAuditEvents(batch, NewContext());

        Assert.Empty(ack.AcceptedEventIds);
    }

    [Fact]
    public async Task IngestCachedTelemetry_BeforeTheIngestProxyIsWired_ReturnsAnEmptyAck()
    {
        var service = CreateService();
        service.SetReady(CreateTestProbe().Ref);

        var batch = new CachedTelemetryBatch();
        batch.Packets.Add(new CachedTelemetryPacket
        {
            AuditEvent = AuditEventDtoMapper.ToDto(SampleAuditEvent()),
            Operational = SiteCallDtoMapper.ToDto(SampleSiteCall()),
        });

        var ack = await service.IngestCachedTelemetry(batch, NewContext());

        Assert.Empty(ack.AcceptedEventIds);
    }

    [Fact]
    public void SetAuditIngestActor_IsIndependentOfSetReady()
    {
        var service = CreateService();
        Assert.False(service.IsAuditIngestBound);

        service.SetAuditIngestActor(CreateTestProbe().Ref);

        Assert.True(service.IsAuditIngestBound);
        Assert.False(service.IsReady);
    }

    [Fact]
    public void CentralCommunicationActor_NoLongerRelaysIngestCommands()
    {
        // Regression pin for the removed hop: the actor has no ingest receive at all, so an
        // ingest command reaching it is an unhandled message with no reply — not a silent
        // second path that could drift from the direct one.
        var actor = CreateCentralCommunicationActor();

        actor.Tell(new IngestAuditEventsCommand(new[] { SampleAuditEvent() }), TestActor);
        actor.Tell(
            new IngestCachedTelemetryCommand(
                new[] { new CachedTelemetryEntry(SampleAuditEvent(), SampleSiteCall()) }),
            TestActor);

        ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    private IActorRef CreateCentralCommunicationActor()
    {
        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(new List<Commons.Entities.Sites.Site>());

        var services = new ServiceCollection();
        services.AddScoped(_ => siteRepo);
        var sp = services.BuildServiceProvider();

        var transport = Substitute.For<ISiteCommandTransport>();
        return Sys.ActorOf(Props.Create(() => new CentralCommunicationActor(sp, transport)));
    }

    private static AuditEvent SampleAuditEvent() =>
        ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: AuditKind.ApiCall,
            status: AuditStatus.Delivered,
            sourceSiteId: "site-a");

    private static SiteCall SampleSiteCall() => new()
    {
        TrackedOperationId = TrackedOperationId.New(),
        Channel = "OutboundApi",
        Target = "ExternalSystemA",
        SourceSite = "site-a",
        Status = "Delivered",
        RetryCount = 0,
        CreatedAtUtc = DateTime.UtcNow,
        UpdatedAtUtc = DateTime.UtcNow,
        IngestedAtUtc = DateTime.UtcNow,
    };
}
