using System.Collections.Concurrent;
using System.Text.Json;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ScadaLink.Commons.Entities.Deployment;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Artifacts;
using ScadaLink.Communication;

namespace ScadaLink.DeploymentManager.Tests;

/// <summary>
/// WP-7: Tests for system-wide artifact deployment.
/// </summary>
public class ArtifactDeploymentServiceTests : TestKit
{
    private readonly ISiteRepository _siteRepo;
    private readonly IDeploymentManagerRepository _deploymentRepo;
    private readonly ITemplateEngineRepository _templateRepo;
    private readonly IExternalSystemRepository _externalSystemRepo;
    private readonly INotificationRepository _notificationRepo;
    private readonly IAuditService _audit;

    public ArtifactDeploymentServiceTests()
    {
        _siteRepo = Substitute.For<ISiteRepository>();
        _deploymentRepo = Substitute.For<IDeploymentManagerRepository>();
        _templateRepo = Substitute.For<ITemplateEngineRepository>();
        _externalSystemRepo = Substitute.For<IExternalSystemRepository>();
        _notificationRepo = Substitute.For<INotificationRepository>();
        _audit = Substitute.For<IAuditService>();
    }

    [Fact]
    public async Task DeployToAllSitesAsync_NoSites_ReturnsFailure()
    {
        _siteRepo.GetAllSitesAsync().Returns(new List<Site>());

        var service = CreateService();

        var result = await service.DeployToAllSitesAsync("admin");

        Assert.True(result.IsFailure);
        Assert.Contains("No sites", result.Error);
    }

    [Fact]
    public void SiteArtifactResult_ContainsSiteInfo()
    {
        var result = new SiteArtifactResult("site1", "Site One", true, null);
        Assert.Equal("site1", result.SiteId);
        Assert.Equal("Site One", result.SiteName);
        Assert.True(result.Success);
        Assert.Null(result.ErrorMessage);
    }

    [Fact]
    public void ArtifactDeploymentSummary_CountsCorrectly()
    {
        var results = new List<SiteArtifactResult>
        {
            new("s1", "Site1", true, null),
            new("s2", "Site2", false, "error"),
            new("s3", "Site3", true, null)
        };
        var summary = new ArtifactDeploymentSummary("dep1", results, 2, 1);

        Assert.Equal(2, summary.SuccessCount);
        Assert.Equal(1, summary.FailureCount);
        Assert.Equal(3, summary.SiteResults.Count);
    }

    // ── DeploymentManager-010: one logical deployment id across all per-site commands ──

    [Fact]
    public async Task DeployToAllSitesAsync_AllPerSiteCommandsShareTheSummaryDeploymentId()
    {
        // DeploymentManager-010: previously each per-site DeployArtifactsCommand
        // minted its own GUID, so one logical deployment produced N+1 unrelated
        // ids. Every per-site command must now carry the SAME id, equal to the
        // id reported in the summary and audit log.
        var sites = new List<Site>
        {
            new("Site One", "site-1") { Id = 1 },
            new("Site Two", "site-2") { Id = 2 }
        };
        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
        var service = CreateServiceWithCommActor(probe);

        var result = await service.DeployToAllSitesAsync("admin");

        Assert.True(result.IsSuccess);
        var commands = ArtifactProbeActor.Received;
        Assert.Equal(2, commands.Count);

        // All per-site commands carry one shared id, equal to the summary id.
        var distinctIds = commands.Select(c => c.DeploymentId).Distinct().ToList();
        Assert.Single(distinctIds);
        Assert.Equal(result.Value.DeploymentId, distinctIds[0]);

        // The persisted record embeds the same logical deployment id.
        await _deploymentRepo.Received().AddSystemArtifactDeploymentAsync(
            Arg.Do<SystemArtifactDeploymentRecord>(r =>
            {
                using var doc = JsonDocument.Parse(r.PerSiteStatus!);
                Assert.Equal(result.Value.DeploymentId,
                    doc.RootElement.GetProperty("DeploymentId").GetString());
            }),
            Arg.Any<CancellationToken>());
    }

    // ── DeploymentManager-014: real per-site success/failure coverage ──

    [Fact]
    public async Task DeployToAllSitesAsync_PartialFailure_ReportsPerSiteMatrix()
    {
        // Site one succeeds, site two fails -> the summary counts must reflect
        // the per-site matrix.
        var sites = new List<Site>
        {
            new("Site One", "ok-site") { Id = 1 },
            new("Site Two", "fail-site") { Id = 2 }
        };
        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor("fail-site")));
        var service = CreateServiceWithCommActor(probe);

        var result = await service.DeployToAllSitesAsync("admin");

        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Value.SuccessCount);
        Assert.Equal(1, result.Value.FailureCount);
        Assert.Contains(result.Value.SiteResults, r => r.SiteId == "ok-site" && r.Success);
        Assert.Contains(result.Value.SiteResults, r => r.SiteId == "fail-site" && !r.Success);
    }

    [Fact]
    public async Task RetryForSiteAsync_SiteSucceeds_ReturnsSuccessAndAudits()
    {
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
        var service = CreateServiceWithCommActor(probe);

        var result = await service.RetryForSiteAsync(1, "retry-site", "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("retry-site", result.Value.SiteId);
        await _audit.Received().LogAsync(
            "admin", "RetryArtifactDeployment", "SystemArtifact",
            Arg.Any<string>(), "retry-site", Arg.Any<object>(), Arg.Any<CancellationToken>());
    }

    private ArtifactDeploymentService CreateService()
    {
        var comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);

        return new ArtifactDeploymentService(
            _siteRepo, _deploymentRepo, _templateRepo, _externalSystemRepo, _notificationRepo,
            comms, _audit,
            Options.Create(new DeploymentManagerOptions()),
            NullLogger<ArtifactDeploymentService>.Instance);
    }

    private ArtifactDeploymentService CreateServiceWithCommActor(IActorRef commActor)
    {
        var comms = new CommunicationService(
            Options.Create(new CommunicationOptions
            {
                ArtifactDeploymentTimeout = TimeSpan.FromSeconds(5)
            }),
            NullLogger<CommunicationService>.Instance);
        comms.SetCommunicationActor(commActor);

        return new ArtifactDeploymentService(
            _siteRepo, _deploymentRepo, _templateRepo, _externalSystemRepo, _notificationRepo,
            comms, _audit,
            Options.Create(new DeploymentManagerOptions
            {
                ArtifactDeploymentTimeoutPerSite = TimeSpan.FromSeconds(5)
            }),
            NullLogger<ArtifactDeploymentService>.Instance);
    }

    /// <summary>
    /// Stand-in CentralCommunicationActor for artifact deployment. Records every
    /// <see cref="DeployArtifactsCommand"/> it receives and replies success
    /// unless the target site id is in the configured failure set.
    /// </summary>
    private class ArtifactProbeActor : ReceiveActor
    {
        public static readonly ConcurrentBag<DeployArtifactsCommand> Received = new();

        public ArtifactProbeActor(params string[] failingSites)
        {
            Received.Clear();
            var failSet = new HashSet<string>(failingSites);

            Receive<SiteEnvelope>(env =>
            {
                if (env.Message is DeployArtifactsCommand cmd)
                {
                    Received.Add(cmd);
                    var success = !failSet.Contains(env.SiteId);
                    Sender.Tell(new ArtifactDeploymentResponse(
                        cmd.DeploymentId, env.SiteId, success,
                        success ? null : "site rejected artifacts", DateTimeOffset.UtcNow));
                }
            });
        }
    }
}
