using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.TestSupport;

namespace ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests;

/// <summary>
/// Task 5 (#22 Retry/Discard relay): tests the site-side execution of a
/// central→site <see cref="RetryParkedOperation"/> / <see cref="DiscardParkedOperation"/>
/// relay command on the <see cref="ParkedMessageHandlerActor"/>. The cached
/// call's S&amp;F buffer message id is the <see cref="TrackedOperationId"/>, so
/// the handler resolves the parked row directly from the tracked id and reuses
/// the existing parked-message Retry/Discard primitive. A non-parked operation
/// must be a safe no-op (<c>Applied=false</c>), never a corruption.
/// </summary>
public class ParkedOperationRelayTests : TestKit, IAsyncLifetime, IDisposable
{
    private readonly TestLocalDb _localDb;
    private readonly StoreAndForwardStorage _storage;
    private readonly StoreAndForwardService _service;

    public ParkedOperationRelayTests()
    {
        _localDb = TestLocalDb.CreateTemp("RelayTests");

        _storage = new StoreAndForwardStorage(_localDb.Db, NullLogger<StoreAndForwardStorage>.Instance);

        var options = new StoreAndForwardOptions
        {
            DefaultRetryInterval = TimeSpan.Zero,
            DefaultMaxRetries = 1,
            RetryTimerInterval = TimeSpan.FromMinutes(10),
        };

        _service = new StoreAndForwardService(
            _storage, options, NullLogger<StoreAndForwardService>.Instance);
    }

    public async Task InitializeAsync() => await _storage.InitializeAsync();

    public Task DisposeAsync() => Task.CompletedTask;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            var path = _localDb.Path;
            _localDb.Dispose();
            TestLocalDb.DeleteFiles(path);
        }

        base.Dispose(disposing);
    }

    /// <summary>
    /// Enqueues a cached-call message whose S&amp;F id is the supplied
    /// <see cref="TrackedOperationId"/> and parks it via the retry sweep.
    /// </summary>
    private async Task ParkCachedCallAsync(TrackedOperationId id)
    {
        _service.RegisterDeliveryHandler(
            StoreAndForwardCategory.ExternalSystem, _ => throw new HttpRequestException("always fails"));
        await _service.EnqueueAsync(
            StoreAndForwardCategory.ExternalSystem, "ERP.GetOrder", """{}""",
            maxRetries: 1, messageId: id.ToString());
        await _service.RetryPendingMessagesAsync();
    }

    [Fact]
    public async Task RetryParkedOperation_ParkedCachedCall_ResetsToPendingAndApplied()
    {
        var id = TrackedOperationId.New();
        await ParkCachedCallAsync(id);

        var actor = Sys.ActorOf(Props.Create(() => new ParkedMessageHandlerActor(_service, "site-1")));
        actor.Tell(new RetryParkedOperation("corr-1", id));

        var ack = ExpectMsg<ParkedOperationActionAck>();
        Assert.True(ack.Applied);
        Assert.Equal("corr-1", ack.CorrelationId);
        Assert.Null(ack.ErrorMessage);

        // The parked row was reset back to Pending so the retry sweep picks it up.
        var msg = await _storage.GetMessageByIdAsync(id.ToString());
        Assert.NotNull(msg);
        Assert.Equal(StoreAndForwardMessageStatus.Pending, msg!.Status);
    }

    [Fact]
    public async Task DiscardParkedOperation_ParkedCachedCall_RemovesRowAndApplied()
    {
        var id = TrackedOperationId.New();
        await ParkCachedCallAsync(id);

        var actor = Sys.ActorOf(Props.Create(() => new ParkedMessageHandlerActor(_service, "site-1")));
        actor.Tell(new DiscardParkedOperation("corr-2", id));

        var ack = ExpectMsg<ParkedOperationActionAck>();
        Assert.True(ack.Applied);
        Assert.Equal("corr-2", ack.CorrelationId);

        var msg = await _storage.GetMessageByIdAsync(id.ToString());
        Assert.Null(msg);
    }

    [Fact]
    public void RetryParkedOperation_UnknownOperation_IsSafeNoOp()
    {
        var actor = Sys.ActorOf(Props.Create(() => new ParkedMessageHandlerActor(_service, "site-1")));
        actor.Tell(new RetryParkedOperation("corr-3", TrackedOperationId.New()));

        var ack = ExpectMsg<ParkedOperationActionAck>();
        // No parked row matched — definitive "nothing to do", not an error.
        Assert.False(ack.Applied);
        Assert.Equal("corr-3", ack.CorrelationId);
        Assert.Null(ack.ErrorMessage);
    }

    [Fact]
    public async Task RetryParkedOperation_NonParkedOperation_IsSafeNoOpAndDoesNotCorrupt()
    {
        // Enqueue a cached call but DO NOT park it — it stays Pending.
        var id = TrackedOperationId.New();
        _service.RegisterDeliveryHandler(
            StoreAndForwardCategory.ExternalSystem, _ => throw new HttpRequestException("fails"));
        await _service.EnqueueAsync(
            StoreAndForwardCategory.ExternalSystem, "ERP.GetOrder", """{}""",
            maxRetries: 5, messageId: id.ToString());

        var before = await _storage.GetMessageByIdAsync(id.ToString());
        Assert.Equal(StoreAndForwardMessageStatus.Pending, before!.Status);

        var actor = Sys.ActorOf(Props.Create(() => new ParkedMessageHandlerActor(_service, "site-1")));
        actor.Tell(new RetryParkedOperation("corr-4", id));

        var ack = ExpectMsg<ParkedOperationActionAck>();
        // The row is Pending, not Parked — Retry must be a no-op, not a mutation.
        Assert.False(ack.Applied);

        var after = await _storage.GetMessageByIdAsync(id.ToString());
        Assert.NotNull(after);
        Assert.Equal(StoreAndForwardMessageStatus.Pending, after!.Status);
        // retry_count untouched — a Parked-only Retry must not reset a live row.
        Assert.Equal(before.RetryCount, after.RetryCount);
    }

    [Fact]
    public async Task DiscardParkedOperation_NonParkedOperation_IsSafeNoOp()
    {
        var id = TrackedOperationId.New();
        _service.RegisterDeliveryHandler(
            StoreAndForwardCategory.ExternalSystem, _ => throw new HttpRequestException("fails"));
        await _service.EnqueueAsync(
            StoreAndForwardCategory.ExternalSystem, "ERP.GetOrder", """{}""",
            maxRetries: 5, messageId: id.ToString());

        var actor = Sys.ActorOf(Props.Create(() => new ParkedMessageHandlerActor(_service, "site-1")));
        actor.Tell(new DiscardParkedOperation("corr-5", id));

        var ack = ExpectMsg<ParkedOperationActionAck>();
        Assert.False(ack.Applied);

        // The Pending row must NOT have been deleted by a Parked-only Discard.
        var after = await _storage.GetMessageByIdAsync(id.ToString());
        Assert.NotNull(after);
    }
}
