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 ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
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.Messages.Artifacts;
using ZB.MOM.WW.ScadaBridge.Communication;

namespace ZB.MOM.WW.ScadaBridge.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>();
        // DeploymentManager-025/-027: the notification repo is retained only so the
        // tests can assert the artifact path NEVER touches it (it is no longer a
        // constructor dependency of ArtifactDeploymentService).
        _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 recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
        var service = CreateServiceWithCommActor(probe);

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

        Assert.True(result.IsSuccess);
        var commands = recorder.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 recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder, "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);
    }

    // ── DeploymentManager-023: global artifact queries hoisted out of the per-site loop ──

    [Fact]
    public async Task DeployToAllSitesAsync_HoistsGlobalArtifactQueriesOutOfPerSiteLoop()
    {
        // DeploymentManager-023: previously each per-site iteration of the deploy-many
        // loop re-issued the global artifact queries (shared scripts, external systems,
        // DB connections) — a textbook N+1 over the global sets. With three sites the
        // queries must now be issued ONCE in total, regardless of site count.
        var sites = new List<Site>
        {
            new("Site One", "site-1") { Id = 1 },
            new("Site Two", "site-2") { Id = 2 },
            new("Site Three", "site-3") { Id = 3 },
        };
        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
        var recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
        var service = CreateServiceWithCommActor(probe);

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

        Assert.True(result.IsSuccess);
        // Each global query must be called EXACTLY ONCE for the whole multi-site sweep.
        await _templateRepo.Received(1).GetAllSharedScriptsAsync(Arg.Any<CancellationToken>());
        await _externalSystemRepo.Received(1).GetAllExternalSystemsAsync(Arg.Any<CancellationToken>());
        await _externalSystemRepo.Received(1).GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>());
        // DeploymentManager-025/-027: notification lists and SMTP configuration are
        // central-only — the artifact path must NEVER fetch them, and the per-site
        // command must NEVER carry them.
        await _notificationRepo.DidNotReceive().GetAllNotificationListsAsync(Arg.Any<CancellationToken>());
        await _notificationRepo.DidNotReceive().GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>());
        Assert.All(recorder.Received, cmd =>
        {
            Assert.Null(cmd.NotificationLists);
            Assert.Null(cmd.SmtpConfigurations);
        });
        // The per-site query (data connections) DOES vary per site and must still run
        // once per site.
        await _siteRepo.Received(1).GetDataConnectionsBySiteIdAsync(1, Arg.Any<CancellationToken>());
        await _siteRepo.Received(1).GetDataConnectionsBySiteIdAsync(2, Arg.Any<CancellationToken>());
        await _siteRepo.Received(1).GetDataConnectionsBySiteIdAsync(3, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task RetryForSiteAsync_SingleSitePath_StillRunsTheGlobalQueriesOnce()
    {
        // DeploymentManager-023: the single-site convenience overload still owns its
        // own global-fetch (it cannot inherit from a sweep), so for one site every
        // global query is issued exactly once. Pin this so a future refactor cannot
        // accidentally route RetryForSiteAsync through the multi-site loop and lose
        // the audit row's deploymentId guarantee.
        var recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
        var service = CreateServiceWithCommActor(probe);

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

        Assert.True(result.IsSuccess);
        await _templateRepo.Received(1).GetAllSharedScriptsAsync(Arg.Any<CancellationToken>());
        await _externalSystemRepo.Received(1).GetAllExternalSystemsAsync(Arg.Any<CancellationToken>());
        await _externalSystemRepo.Received(1).GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>());
        // DeploymentManager-025/-027: central-only — never fetched, never shipped.
        await _notificationRepo.DidNotReceive().GetAllNotificationListsAsync(Arg.Any<CancellationToken>());
        await _notificationRepo.DidNotReceive().GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>());
        Assert.All(recorder.Received, cmd =>
        {
            Assert.Null(cmd.NotificationLists);
            Assert.Null(cmd.SmtpConfigurations);
        });
    }

    [Fact]
    public async Task RetryForSiteAsync_SiteSucceeds_ReturnsSuccessAndAudits()
    {
        var recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
        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>());
    }

    // ── PLAN-07 Task 4: single-site artifact deployment ──

    [Fact]
    public async Task DeployToSiteAsync_DeploysOnlyToTargetSite()
    {
        // Two sites exist; deploying to site 1 must build/send exactly one per-site
        // command (for SITE1) and never touch SITE2's per-site data.
        var site1 = new Site("Site One", "SITE1") { Id = 1 };
        _siteRepo.GetSiteByIdAsync(1, Arg.Any<CancellationToken>()).Returns(site1);
        var recorder = new ArtifactProbeRecorder();
        var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
        var service = CreateServiceWithCommActor(probe);

        var result = await service.DeployToSiteAsync(1, "tester");

        Assert.True(result.IsSuccess);
        Assert.Single(result.Value.SiteResults);
        Assert.Equal("SITE1", result.Value.SiteResults[0].SiteId);
        Assert.Single(recorder.Received);
        await _siteRepo.Received(1).GetDataConnectionsBySiteIdAsync(1, Arg.Any<CancellationToken>());
        await _siteRepo.DidNotReceive().GetDataConnectionsBySiteIdAsync(2, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task DeployToSiteAsync_UnknownSite_ReturnsFailure()
    {
        _siteRepo.GetSiteByIdAsync(999, Arg.Any<CancellationToken>()).Returns((Site?)null);
        var service = CreateService();

        var result = await service.DeployToSiteAsync(999, "tester");

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

    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>
    /// Per-test recorder for <see cref="ArtifactProbeActor"/>. DeploymentManager-024:
    /// each test owns its own instance, passed into the actor's constructor, so
    /// the received-command list is no longer shared static state that races
    /// under parallel test execution.
    /// </summary>
    private sealed class ArtifactProbeRecorder
    {
        public readonly ConcurrentBag<DeployArtifactsCommand> Received = new();
    }

    /// <summary>
    /// Stand-in CentralCommunicationActor for artifact deployment. Records every
    /// <see cref="DeployArtifactsCommand"/> it receives into the per-test
    /// <see cref="ArtifactProbeRecorder"/> and replies success unless the target
    /// site id is in the configured failure set.
    /// </summary>
    private class ArtifactProbeActor : ReceiveActor
    {
        public ArtifactProbeActor(ArtifactProbeRecorder recorder, params string[] failingSites)
        {
            var failSet = new HashSet<string>(failingSites);

            Receive<SiteEnvelope>(env =>
            {
                if (env.Message is DeployArtifactsCommand cmd)
                {
                    recorder.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));
                }
            });
        }
    }
}
