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

namespace ScadaLink.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)
        });

        var siteRepo = Substitute.For<ISiteRepository>();
        _service = new DeploymentService(
            _repo, siteRepo, _pipeline, _comms, _lockManager, _audit,
            new DiffService(), _notifier, options,
            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 Pending,
        // InProgress and Failed writes — not be silent (the pre-fix behaviour).
        var result = await _service.DeployInstanceAsync(7, "admin");

        Assert.True(result.IsFailure);
        Assert.NotEmpty(changes);
        Assert.All(changes, c => Assert.Equal(7, c.InstanceId));
        Assert.Contains(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");
    }
}
