using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;

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

/// <summary>
/// CentralUI-006: regression tests proving <see cref="DeploymentService"/>
/// raises <see cref="IDeploymentStatusNotifier.StatusChanged"/> whenever it
/// writes a deployment record's status. This is the event source the Central
/// UI deployment-status page subscribes to instead of polling.
/// </summary>
public class DeploymentStatusNotifierTests : TestKit
{
    private readonly IDeploymentManagerRepository _repo;
    private readonly IFlatteningPipeline _pipeline;
    private readonly CommunicationService _comms;
    private readonly OperationLockManager _lockManager;
    private readonly IAuditService _audit;
    private readonly DeploymentStatusNotifier _notifier;
    private readonly DeploymentService _service;

    public DeploymentStatusNotifierTests()
    {
        _repo = Substitute.For<IDeploymentManagerRepository>();
        _pipeline = Substitute.For<IFlatteningPipeline>();
        _comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);
        _lockManager = new OperationLockManager();
        _audit = Substitute.For<IAuditService>();
        _notifier = new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance);

        var options = Options.Create(new DeploymentManagerOptions
        {
            OperationLockTimeout = TimeSpan.FromSeconds(5)
        });

        // DeploymentManager-021: the resolver now throws when the site row
        // is missing, so seed the substitute to return a real-shaped Site for
        // any id these tests touch.
        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetSiteByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(callInfo =>
            {
                var id = callInfo.ArgAt<int>(0);
                return new Site($"Test Site {id}", $"site-{id}") { Id = id };
            });

        _service = new DeploymentService(
            _repo, siteRepo, _pipeline, _comms, _lockManager, _audit,
            new DiffService(), new RevisionHashService(), _notifier, options,
            // Notify-and-fetch (Task 6): a non-empty CentralFetchBaseUrl lets the
            // deploy path reach the send (and, here, the catch block) rather than
            // tripping the fail-fast configuration guard.
            Options.Create(new CommunicationOptions { CentralFetchBaseUrl = "https://central.test:9000" }),
            NullLogger<DeploymentService>.Instance);
    }

    [Fact]
    public async Task DeployInstanceAsync_RaisesStatusChange_ForEveryRecordStatusWrite()
    {
        var instance = new Instance("TestInst") { Id = 7, SiteId = 1, State = InstanceState.NotDeployed };
        _repo.GetInstanceByIdAsync(7).Returns(instance);

        var config = new FlattenedConfiguration { InstanceUniqueName = "TestInst" };
        _pipeline.FlattenAndValidateAsync(7, Arg.Any<CancellationToken>())
            .Returns(Result<FlatteningPipelineResult>.Success(
                new FlatteningPipelineResult(config, "sha256:abc", ValidationResult.Success())));

        var changes = new List<DeploymentStatusChange>();
        _notifier.StatusChanged += c => changes.Add(c);

        // _comms has no actor set, so the deploy reaches the catch block and
        // the record ends Failed. The notifier must fire for the InProgress
        // and Failed writes — not be silent (the pre-fix behaviour).
        //
        // DeploymentManager-022: the transient Pending write was dropped from
        // the deploy path (the record is now created directly in InProgress),
        // so there is no Pending notification any more. The remaining two
        // writes — the initial InProgress insert and the catch-block Failed
        // update — must each raise a status-change.
        var result = await _service.DeployInstanceAsync(7, "admin");

        Assert.True(result.IsFailure);
        Assert.NotEmpty(changes);
        Assert.All(changes, c => Assert.Equal(7, c.InstanceId));
        Assert.DoesNotContain(changes, c => c.Status == DeploymentStatus.Pending);
        Assert.Contains(changes, c => c.Status == DeploymentStatus.InProgress);
        Assert.Contains(changes, c => c.Status == DeploymentStatus.Failed);

        // All notifications carry the same deployment id (the one created here).
        var deploymentId = changes[0].DeploymentId;
        Assert.False(string.IsNullOrEmpty(deploymentId));
        Assert.All(changes, c => Assert.Equal(deploymentId, c.DeploymentId));
    }

    [Fact]
    public void NotifyStatusChanged_WithNoSubscribers_DoesNotThrow()
    {
        var notifier = new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance);

        var ex = Record.Exception(() =>
            notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-1", 1, DeploymentStatus.Success)));

        Assert.Null(ex);
    }

    [Fact]
    public void NotifyStatusChanged_ThrowingSubscriber_DoesNotBreakOtherSubscribers()
    {
        var notifier = new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance);
        var reached = false;

        notifier.StatusChanged += _ => throw new InvalidOperationException("boom");
        notifier.StatusChanged += _ => reached = true;

        var ex = Record.Exception(() =>
            notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-2", 2, DeploymentStatus.InProgress)));

        Assert.Null(ex);
        Assert.True(reached, "a faulting subscriber must not stop later subscribers from being notified");
    }
}
