using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using ScadaLink.ClusterInfrastructure;
using ScadaLink.Communication;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.ConfigurationDatabase;
using ScadaLink.DataConnectionLayer;
using ScadaLink.DeploymentManager;
using ScadaLink.ExternalSystemGateway;
using ScadaLink.HealthMonitoring;
using ScadaLink.Host;
using ScadaLink.Host.Actors;
using ScadaLink.InboundAPI;
using ScadaLink.ManagementService;
using ScadaLink.NotificationService;
using ScadaLink.Security;
using ScadaLink.SiteEventLogging;
using ScadaLink.Communication.Grpc;
using ScadaLink.SiteRuntime;
using ScadaLink.SiteRuntime.Persistence;
using ScadaLink.SiteRuntime.Repositories;
using ScadaLink.SiteRuntime.Scripts;
using ScadaLink.SiteRuntime.Streaming;
using ScadaLink.StoreAndForward;
using ScadaLink.TemplateEngine;
using ScadaLink.TemplateEngine.Flattening;
using ScadaLink.TemplateEngine.Services;
using ScadaLink.TemplateEngine.Validation;

namespace ScadaLink.Host.Tests;

/// <summary>
/// Removes AkkaHostedService from running as a hosted service while keeping it
/// resolvable in DI (other services like AkkaHealthReportTransport depend on it).
/// </summary>
internal static class AkkaHostedServiceRemover
{
    internal static void RemoveAkkaHostedServiceOnly(IServiceCollection services)
    {
        // Pattern used in Program.cs:
        //   services.AddSingleton<AkkaHostedService>();             // index N
        //   services.AddHostedService(sp => sp.GetRequiredService<AkkaHostedService>());  // index N+1
        // We keep the singleton so DI resolution works, but remove the IHostedService
        // factory so StartAsync is never called.
        int akkaIndex = -1;
        for (int i = 0; i < services.Count; i++)
        {
            if (services[i].ServiceType == typeof(AkkaHostedService))
            {
                akkaIndex = i;
                break;
            }
        }

        if (akkaIndex < 0) return;

        // The IHostedService factory is the next registration after the singleton
        for (int i = akkaIndex + 1; i < services.Count; i++)
        {
            if (services[i].ServiceType == typeof(IHostedService)
                && services[i].ImplementationFactory != null)
            {
                services.RemoveAt(i);
                break;
            }
        }
    }
}

/// <summary>
/// Verifies every expected DI service resolves from the Central composition root.
/// Uses WebApplicationFactory to exercise the real Program.cs pipeline.
/// </summary>
public class CentralCompositionRootTests : IDisposable
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly string? _previousEnv;

    public CentralCompositionRootTests()
    {
        _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
        Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central");

        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureAppConfiguration((_, config) =>
                {
                    config.AddInMemoryCollection(new Dictionary<string, string?>
                    {
                        ["ScadaLink:Node:NodeHostname"] = "localhost",
                        ["ScadaLink:Node:RemotingPort"] = "0",
                        ["ScadaLink:Cluster:SeedNodes:0"] = "akka.tcp://scadalink@localhost:2551",
                        ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:2552",
                        ["ScadaLink:Database:SkipMigrations"] = "true",
                        ["ScadaLink:Security:JwtSigningKey"] = "test-signing-key-must-be-at-least-32-chars-long!",
                        ["ScadaLink:Security:LdapServer"] = "localhost",
                        ["ScadaLink:Security:LdapPort"] = "3893",
                        ["ScadaLink:Security:LdapUseTls"] = "false",
                        ["ScadaLink:Security:AllowInsecureLdap"] = "true",
                        ["ScadaLink:Security:LdapSearchBase"] = "dc=scadalink,dc=local",
                    });
                });
                builder.UseSetting("ScadaLink:Node:Role", "Central");
                builder.UseSetting("ScadaLink:Database:SkipMigrations", "true");
                builder.ConfigureServices(services =>
                {
                    // Replace SQL Server with in-memory database
                    var descriptorsToRemove = services
                        .Where(d =>
                            d.ServiceType == typeof(DbContextOptions<ScadaLinkDbContext>) ||
                            d.ServiceType == typeof(DbContextOptions) ||
                            d.ServiceType == typeof(ScadaLinkDbContext) ||
                            d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true)
                        .ToList();
                    foreach (var d in descriptorsToRemove)
                        services.Remove(d);

                    services.AddDbContext<ScadaLinkDbContext>(options =>
                        options.UseInMemoryDatabase($"CompositionRootTests_{Guid.NewGuid()}"));

                    // Keep AkkaHostedService in DI (other services depend on it)
                    // but prevent it from starting by removing only its IHostedService registration.
                    AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(services);
                });
            });

        // Trigger host build
        _ = _factory.Server;
    }

    public void Dispose()
    {
        _factory.Dispose();
        Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv);
    }

    // --- Singletons ---

    [Theory]
    [MemberData(nameof(CentralSingletonServices))]
    public void Central_ResolveSingleton(Type serviceType)
    {
        var service = _factory.Services.GetService(serviceType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> CentralSingletonServices => new[]
    {
        new object[] { typeof(CommunicationService) },
        new object[] { typeof(ISiteHealthCollector) },
        new object[] { typeof(CentralHealthAggregator) },
        new object[] { typeof(ICentralHealthAggregator) },
        new object[] { typeof(OperationLockManager) },
        new object[] { typeof(OAuth2TokenService) },
        new object[] { typeof(InboundScriptExecutor) },
    };

    // --- Scoped services ---

    [Theory]
    [MemberData(nameof(CentralScopedServices))]
    public void Central_ResolveScoped(Type serviceType)
    {
        using var scope = _factory.Services.CreateScope();
        var service = scope.ServiceProvider.GetService(serviceType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> CentralScopedServices => new[]
    {
        // TemplateEngine
        new object[] { typeof(TemplateService) },
        new object[] { typeof(SharedScriptService) },
        new object[] { typeof(InstanceService) },
        new object[] { typeof(SiteService) },
        new object[] { typeof(AreaService) },
        new object[] { typeof(TemplateDeletionService) },
        // DeploymentManager
        new object[] { typeof(IFlatteningPipeline) },
        new object[] { typeof(DeploymentService) },
        new object[] { typeof(ArtifactDeploymentService) },
        // Security
        new object[] { typeof(LdapAuthService) },
        new object[] { typeof(JwtTokenService) },
        new object[] { typeof(RoleMapper) },
        // InboundAPI
        new object[] { typeof(ApiKeyValidator) },
        new object[] { typeof(RouteHelper) },
        // ExternalSystemGateway
        new object[] { typeof(ExternalSystemClient) },
        new object[] { typeof(IExternalSystemClient) },
        new object[] { typeof(DatabaseGateway) },
        new object[] { typeof(IDatabaseGateway) },
        // NotificationService
        new object[] { typeof(NotificationDeliveryService) },
        new object[] { typeof(INotificationDeliveryService) },
        // ConfigurationDatabase repositories
        new object[] { typeof(ScadaLinkDbContext) },
        new object[] { typeof(ISecurityRepository) },
        new object[] { typeof(ICentralUiRepository) },
        new object[] { typeof(ITemplateEngineRepository) },
        new object[] { typeof(IDeploymentManagerRepository) },
        new object[] { typeof(ISiteRepository) },
        new object[] { typeof(IExternalSystemRepository) },
        new object[] { typeof(INotificationRepository) },
        new object[] { typeof(IInboundApiRepository) },
        new object[] { typeof(IAuditService) },
        new object[] { typeof(IInstanceLocator) },
        // CentralUI
        new object[] { typeof(AuthenticationStateProvider) },
    };

    // --- Transient services ---

    [Theory]
    [MemberData(nameof(CentralTransientServices))]
    public void Central_ResolveTransient(Type serviceType)
    {
        using var scope = _factory.Services.CreateScope();
        var service = scope.ServiceProvider.GetService(serviceType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> CentralTransientServices => new[]
    {
        new object[] { typeof(FlatteningService) },
        new object[] { typeof(DiffService) },
        new object[] { typeof(RevisionHashService) },
        new object[] { typeof(ScriptCompiler) },
        new object[] { typeof(SemanticValidator) },
        new object[] { typeof(ValidationService) },
    };

    // --- Options ---

    [Theory]
    [MemberData(nameof(CentralOptions))]
    public void Central_ResolveOptions(Type optionsType)
    {
        var service = _factory.Services.GetService(optionsType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> CentralOptions => new[]
    {
        new object[] { typeof(IOptions<NodeOptions>) },
        new object[] { typeof(IOptions<ClusterOptions>) },
        new object[] { typeof(IOptions<DatabaseOptions>) },
        new object[] { typeof(IOptions<CommunicationOptions>) },
        new object[] { typeof(IOptions<HealthMonitoringOptions>) },
        new object[] { typeof(IOptions<NotificationOptions>) },
        new object[] { typeof(IOptions<LoggingOptions>) },
        new object[] { typeof(IOptions<SecurityOptions>) },
        new object[] { typeof(IOptions<InboundApiOptions>) },
        new object[] { typeof(IOptions<ManagementServiceOptions>) },
        new object[] { typeof(IOptions<ExternalSystemGatewayOptions>) },
    };

    // --- Hosted services ---

    [Fact]
    public void Central_CentralHealthAggregator_RegisteredAsHostedService()
    {
        var hostedServices = _factory.Services.GetServices<IHostedService>();
        Assert.Contains(hostedServices, s => s.GetType() == typeof(CentralHealthAggregator));
    }
}

/// <summary>
/// Verifies every expected DI service resolves from the Site composition root.
/// Uses the extracted SiteServiceRegistration.Configure() so the test always
/// matches the real Program.cs registration (WebApplicationBuilder + gRPC).
/// </summary>
public class SiteCompositionRootTests : IDisposable
{
    private readonly WebApplication _host;
    private readonly string _tempDbPath;

    public SiteCompositionRootTests()
    {
        _tempDbPath = Path.Combine(Path.GetTempPath(), $"scadalink_test_{Guid.NewGuid()}.db");

        var builder = WebApplication.CreateBuilder();
        builder.Configuration.Sources.Clear();
        builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["ScadaLink:Node:Role"] = "Site",
            ["ScadaLink:Node:NodeHostname"] = "test-site",
            ["ScadaLink:Node:SiteId"] = "TestSite",
            ["ScadaLink:Node:RemotingPort"] = "0",
            ["ScadaLink:Node:GrpcPort"] = "0",
            ["ScadaLink:Database:SiteDbPath"] = _tempDbPath,
            // ClusterOptions requires at least one seed node (ClusterOptionsValidator).
            ["ScadaLink:Cluster:SeedNodes:0"] = "akka.tcp://scadalink@localhost:2551",
            ["ScadaLink:Cluster:SeedNodes:1"] = "akka.tcp://scadalink@localhost:2552",
        });

        // gRPC server registration (mirrors Program.cs site section)
        builder.Services.AddGrpc();
        builder.Services.AddSingleton<ScadaLink.Communication.Grpc.SiteStreamGrpcServer>();

        SiteServiceRegistration.Configure(builder.Services, builder.Configuration);

        // Keep AkkaHostedService in DI (other services depend on it)
        // but prevent it from starting by removing only its IHostedService registration.
        AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(builder.Services);

        _host = builder.Build();
    }

    public void Dispose()
    {
        (_host as IDisposable)?.Dispose();
        try { File.Delete(_tempDbPath); } catch { /* best effort */ }
    }

    // --- Singletons ---

    [Theory]
    [MemberData(nameof(SiteSingletonServices))]
    public void Site_ResolveSingleton(Type serviceType)
    {
        var service = _host.Services.GetService(serviceType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> SiteSingletonServices => new[]
    {
        new object[] { typeof(CommunicationService) },
        new object[] { typeof(ISiteHealthCollector) },
        new object[] { typeof(SiteStorageService) },
        new object[] { typeof(ScriptCompilationService) },
        new object[] { typeof(SharedScriptLibrary) },
        new object[] { typeof(SiteStreamManager) },
        new object[] { typeof(ISiteStreamSubscriber) },
        new object[] { typeof(SiteStreamGrpcServer) },
        new object[] { typeof(IDataConnectionFactory) },
        new object[] { typeof(StoreAndForwardStorage) },
        new object[] { typeof(StoreAndForwardService) },
        new object[] { typeof(ReplicationService) },
        new object[] { typeof(ISiteEventLogger) },
        new object[] { typeof(IEventLogQueryService) },
        new object[] { typeof(OAuth2TokenService) },
        new object[] { typeof(ISiteIdentityProvider) },
        new object[] { typeof(IHealthReportTransport) },
    };

    // --- Scoped services ---

    [Theory]
    [MemberData(nameof(SiteScopedServices))]
    public void Site_ResolveScoped(Type serviceType)
    {
        using var scope = _host.Services.CreateScope();
        var service = scope.ServiceProvider.GetService(serviceType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> SiteScopedServices => new[]
    {
        new object[] { typeof(IExternalSystemRepository) },
        new object[] { typeof(INotificationRepository) },
        new object[] { typeof(ExternalSystemClient) },
        new object[] { typeof(IExternalSystemClient) },
        new object[] { typeof(DatabaseGateway) },
        new object[] { typeof(IDatabaseGateway) },
        new object[] { typeof(NotificationDeliveryService) },
        new object[] { typeof(INotificationDeliveryService) },
    };

    // --- Implementation type assertions ---

    [Fact]
    public void Site_ExternalSystemRepository_IsSiteImplementation()
    {
        using var scope = _host.Services.CreateScope();
        var repo = scope.ServiceProvider.GetRequiredService<IExternalSystemRepository>();
        Assert.IsType<SiteExternalSystemRepository>(repo);
    }

    [Fact]
    public void Site_NotificationRepository_IsSiteImplementation()
    {
        using var scope = _host.Services.CreateScope();
        var repo = scope.ServiceProvider.GetRequiredService<INotificationRepository>();
        Assert.IsType<SiteNotificationRepository>(repo);
    }

    // --- Options ---

    [Theory]
    [MemberData(nameof(SiteOptions))]
    public void Site_ResolveOptions(Type optionsType)
    {
        var service = _host.Services.GetService(optionsType);
        Assert.NotNull(service);
    }

    public static IEnumerable<object[]> SiteOptions => new[]
    {
        new object[] { typeof(IOptions<NodeOptions>) },
        new object[] { typeof(IOptions<ClusterOptions>) },
        new object[] { typeof(IOptions<DatabaseOptions>) },
        new object[] { typeof(IOptions<CommunicationOptions>) },
        new object[] { typeof(IOptions<HealthMonitoringOptions>) },
        new object[] { typeof(IOptions<NotificationOptions>) },
        new object[] { typeof(IOptions<LoggingOptions>) },
        new object[] { typeof(IOptions<SiteRuntimeOptions>) },
        new object[] { typeof(IOptions<DataConnectionOptions>) },
        new object[] { typeof(IOptions<StoreAndForwardOptions>) },
        new object[] { typeof(IOptions<SiteEventLogOptions>) },
    };

    // --- Hosted services ---

    [Theory]
    [MemberData(nameof(SiteHostedServices))]
    public void Site_HostedServiceRegistered(Type expectedType)
    {
        var hostedServices = _host.Services.GetServices<IHostedService>();
        Assert.Contains(hostedServices, s => s.GetType() == expectedType);
    }

    public static IEnumerable<object[]> SiteHostedServices => new[]
    {
        new object[] { typeof(HealthReportSender) },
        new object[] { typeof(SiteStorageInitializer) },
        new object[] { typeof(EventLogPurgeService) },
    };
}
