using Akka.Actor;
using Akka.TestKit.Xunit2;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site.Telemetry;

/// <summary>
/// Tests for <see cref="SiteCommunicationAuditClient"/> — the production
/// <see cref="ISiteStreamAuditClient"/> binding wired by the Host for site
/// roles. The client maps the proto-DTO batches produced by
/// <see cref="SiteAuditTelemetryActor"/> into the Akka
/// <see cref="IngestAuditEventsCommand"/> / <see cref="IngestCachedTelemetryCommand"/>
/// messages, Asks the site's <c>SiteCommunicationActor</c> (which forwards to
/// central over its central transport), and maps the reply back into an
/// <see cref="IngestAck"/>.
/// </summary>
/// <remarks>
/// A <see cref="TestProbe"/> stands in for the <c>SiteCommunicationActor</c>:
/// it lets the tests assert the exact command shape AND drive the reply (or
/// withhold one to exercise the Ask-timeout path).
/// </remarks>
public class SiteCommunicationAuditClientTests : TestKit
{
    /// <summary>Short Ask timeout so the timeout test completes quickly.</summary>
    private static readonly TimeSpan AskTimeout = TimeSpan.FromMilliseconds(500);

    private static AuditEvent NewEvent(Guid? id = null) => ScadaBridgeAuditEventFactory.Create(
        eventId: id ?? Guid.NewGuid(),
        occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
        channel: AuditChannel.ApiOutbound,
        kind: AuditKind.ApiCall,
        status: AuditStatus.Delivered,
        sourceSiteId: "site-1");

    private static AuditEventBatch BatchOf(IEnumerable<AuditEvent> events)
    {
        var batch = new AuditEventBatch();
        foreach (var e in events)
        {
            batch.Events.Add(AuditEventDtoMapper.ToDto(e));
        }
        return batch;
    }

    private static SiteCallOperationalDto NewOperationalDto() => new()
    {
        TrackedOperationId = Guid.NewGuid().ToString(),
        Channel = "ApiOutbound",
        Target = "ext-system-1",
        SourceSite = "site-1",
        Status = "Submitted",
        RetryCount = 0,
        LastError = string.Empty,
        CreatedAtUtc = Timestamp.FromDateTime(new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc)),
        UpdatedAtUtc = Timestamp.FromDateTime(new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc)),
    };

    [Fact]
    public async Task IngestAuditEventsAsync_FullAck_MapsAllAcceptedIdsOntoAck()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var events = Enumerable.Range(0, 3).Select(_ => NewEvent()).ToList();
        var batch = BatchOf(events);

        var task = sut.IngestAuditEventsAsync(batch, CancellationToken.None);

        // The probe receives exactly one IngestAuditEventsCommand carrying the
        // batch's events; it replies with every EventId accepted.
        var cmd = probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
        Assert.Equal(3, cmd.Events.Count);
        Assert.Equal(
            events.Select(e => e.EventId).ToHashSet(),
            cmd.Events.Select(e => e.EventId).ToHashSet());
        probe.Reply(new IngestAuditEventsReply(events.Select(e => e.EventId).ToList()));

        var ack = await task;

        Assert.Equal(
            events.Select(e => e.EventId.ToString()).ToHashSet(),
            ack.AcceptedEventIds.ToHashSet());
    }

    [Fact]
    public async Task IngestAuditEventsAsync_PartialAck_OnlyAcceptedIdsAppearOnAck()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var events = Enumerable.Range(0, 5).Select(_ => NewEvent()).ToList();
        var accepted = events.Take(3).Select(e => e.EventId).ToList();

        var task = sut.IngestAuditEventsAsync(BatchOf(events), CancellationToken.None);

        probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
        probe.Reply(new IngestAuditEventsReply(accepted));

        var ack = await task;

        Assert.Equal(3, ack.AcceptedEventIds.Count);
        Assert.Equal(
            accepted.Select(id => id.ToString()).ToHashSet(),
            ack.AcceptedEventIds.ToHashSet());
    }

    [Fact]
    public async Task IngestAuditEventsAsync_AskTimeout_Throws_SoDrainLoopKeepsRowsPending()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var batch = BatchOf(new[] { NewEvent() });

        // The probe receives the command but never replies — the Ask times out.
        // The contract: a timeout MUST surface as a thrown exception so the
        // SiteAuditTelemetryActor drain loop leaves the rows Pending.
        var task = sut.IngestAuditEventsAsync(batch, CancellationToken.None);
        probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));

        await Assert.ThrowsAnyAsync<Exception>(() => task);
    }

    [Fact]
    public async Task IngestAuditEventsAsync_FaultedReply_Throws()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var task = sut.IngestAuditEventsAsync(BatchOf(new[] { NewEvent() }), CancellationToken.None);
        probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));

        // A Status.Failure from central (Task 1: central does not swallow an
        // ingest fault into an empty ack) must propagate as a thrown exception.
        probe.Reply(new Status.Failure(new InvalidOperationException("central ingest faulted")));

        await Assert.ThrowsAnyAsync<Exception>(() => task);
    }

    [Fact]
    public async Task IngestCachedTelemetryAsync_RoutesCommand_AndMapsReply()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var events = Enumerable.Range(0, 2).Select(_ => NewEvent()).ToList();
        var batch = new CachedTelemetryBatch();
        foreach (var e in events)
        {
            batch.Packets.Add(new CachedTelemetryPacket
            {
                AuditEvent = AuditEventDtoMapper.ToDto(e),
                Operational = NewOperationalDto(),
            });
        }

        var task = sut.IngestCachedTelemetryAsync(batch, CancellationToken.None);

        // The probe receives an IngestCachedTelemetryCommand (NOT an
        // IngestAuditEventsCommand) with one entry per packet.
        var cmd = probe.ExpectMsg<IngestCachedTelemetryCommand>(TimeSpan.FromSeconds(3));
        Assert.Equal(2, cmd.Entries.Count);
        Assert.Equal(
            events.Select(e => e.EventId).ToHashSet(),
            cmd.Entries.Select(en => en.Audit.EventId).ToHashSet());
        probe.Reply(new IngestCachedTelemetryReply(events.Select(e => e.EventId).ToList()));

        var ack = await task;

        Assert.Equal(
            events.Select(e => e.EventId.ToString()).ToHashSet(),
            ack.AcceptedEventIds.ToHashSet());
    }

    [Fact]
    public async Task IngestCachedTelemetryAsync_AskTimeout_Throws()
    {
        var probe = CreateTestProbe();
        var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);

        var batch = new CachedTelemetryBatch();
        batch.Packets.Add(new CachedTelemetryPacket
        {
            AuditEvent = AuditEventDtoMapper.ToDto(NewEvent()),
            Operational = NewOperationalDto(),
        });

        var task = sut.IngestCachedTelemetryAsync(batch, CancellationToken.None);
        probe.ExpectMsg<IngestCachedTelemetryCommand>(TimeSpan.FromSeconds(3));

        await Assert.ThrowsAnyAsync<Exception>(() => task);
    }
}
