using System.Security.Cryptography;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Infrastructure.Persistence;
using SIGCM2.Infrastructure.Security;
using Xunit;

namespace SIGCM2.TestSupport;

/// <summary>
/// WebApplicationFactory for integration tests against SIGCM2.Api.
/// Uses SIGCM2_Test_Api database (isolated from Application.Tests which uses SIGCM2_Test_App).
///
/// <para>
/// <b>Per-test DI overrides</b> — use <see cref="CreateClientWithOverrides"/> when a test needs to
/// replace a scoped service (e.g. <c>IProductQueryRepository</c>, <c>IAvisoQueryRepository</c>)
/// without touching the shared factory:
/// </para>
/// <code>
/// using var client = _factory.CreateClientWithOverrides(services =>
/// {
///     services.RemoveAll&lt;IMyRepository&gt;();
///     services.AddScoped&lt;IMyRepository&gt;(_ => new MyFakeRepository());
/// });
/// </code>
/// <para>
/// Internally this calls <see cref="WebApplicationFactory{TEntryPoint}.WithWebHostBuilder"/> which
/// creates an independent child host. The RSA key singletons are re-loaded from the same PEM files
/// and do not conflict with the parent host — each child host owns its own DI container.
/// The child factory (and its host) is disposed when the returned <see cref="HttpClient"/> is disposed.
/// </para>
/// <para>
/// <b>Why this works safely</b>: <c>WithWebHostBuilder</c> does NOT share the parent host's DI root.
/// It re-runs <c>ConfigureWebHost</c> (re-loading RSA keys from disk), then applies the caller's
/// <c>ConfigureTestServices</c> on top. The RSA singleton lives in the child's root scope and is
/// disposed with that child factory — no cross-factory leakage.
/// </para>
/// </summary>
public sealed class TestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    // Resolved once — absolute paths independent of working directory
    private static readonly string RepoRoot = ResolveRepoRoot();
    private static readonly string PrivateKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "private.pem");
    private static readonly string PublicKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "public.pem");

    private readonly SqlTestFixture _dbFixture = new(TestConnectionString);

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        // Step 1: Override configuration BEFORE services are built
        builder.ConfigureAppConfiguration((ctx, config) =>
        {
            // Clear all existing sources and rebuild with test values
            // This ensures our paths win over appsettings.json
            config.AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:SqlServer"] = TestConnectionString,
                ["Jwt:Issuer"] = "sigcm2.api",
                ["Jwt:Audience"] = "sigcm2.web",
                ["Jwt:AccessTokenMinutes"] = "60",
                ["Jwt:RefreshTokenDays"] = "7",
                ["Jwt:PrivateKeyPath"] = PrivateKeyPath,
                ["Jwt:PublicKeyPath"] = PublicKeyPath,
                ["Jwt:PrivateKey"] = null,
                ["Jwt:PublicKey"] = null,
                ["Cors:AllowedOrigins:0"] = "http://localhost:5173",
                ["Serilog:MinimumLevel:Default"] = "Warning",
            });
        });

        builder.UseEnvironment("Testing");

        // Step 2: Replace SqlConnectionFactory singleton with the correct test connection.
        // ConfigureAppConfiguration alone is insufficient because WebApplication.CreateBuilder
        // evaluates configuration for singleton construction before overrides apply.
        // ConfigureTestServices runs AFTER all services are registered, so it wins.
        builder.ConfigureTestServices(services =>
        {
            // Remove the existing SqlConnectionFactory singleton registered by AddInfrastructure
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(SqlConnectionFactory));
            if (descriptor is not null)
                services.Remove(descriptor);

            // Re-register with the test connection string
            services.AddSingleton(new SqlConnectionFactory(TestConnectionString));
        });
    }

    /// <summary>
    /// Creates an <see cref="HttpClient"/> against a child host that inherits all base configuration
    /// but applies the caller's additional <paramref name="overrides"/> on top via
    /// <c>ConfigureTestServices</c>.
    ///
    /// <para>
    /// The returned <see cref="HttpClient"/> is tied to the child factory's lifetime.
    /// Dispose the client when the test finishes to release the child host:
    /// <code>using var client = _factory.CreateClientWithOverrides(s => { ... });</code>
    /// </para>
    ///
    /// <para>
    /// Typical usage — inject a stub repository for a specific test scenario:
    /// </para>
    /// <code>
    /// using var client = _factory.CreateClientWithOverrides(services =>
    /// {
    ///     services.RemoveAll&lt;IProductQueryRepository&gt;();
    ///     services.AddScoped&lt;IProductQueryRepository&gt;(_ => new AlwaysInUseProductQueryRepository());
    /// });
    /// var resp = await client.SendAsync(...);
    /// Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
    /// </code>
    /// </summary>
    /// <param name="overrides">
    /// Action applied to <see cref="IServiceCollection"/> after all production services are
    /// registered. Use <c>services.RemoveAll&lt;T&gt;()</c> then <c>services.AddScoped&lt;T&gt;(...)</c>
    /// to replace an existing registration.
    /// </param>
    /// <returns>
    /// A new <see cref="HttpClient"/> backed by the child host. The client owns the child factory
    /// via <see cref="WebApplicationFactory{TEntryPoint}.CreateClient()"/>'s disposal chain.
    /// </returns>
    public HttpClient CreateClientWithOverrides(Action<IServiceCollection> overrides)
    {
        var child = WithWebHostBuilder(builder =>
            builder.ConfigureTestServices(overrides));
        return child.CreateClient();
    }

    public async Task InitializeAsync()
    {
        await _dbFixture.InitializeAsync();
    }

    public new async Task DisposeAsync()
    {
        await _dbFixture.DisposeAsync();
        await base.DisposeAsync();
    }

    private static string ResolveRepoRoot()
    {
        // Walk up from AppContext.BaseDirectory looking for SIGCM2.slnx
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (dir.GetFiles("SIGCM2.slnx").Length > 0)
                return dir.FullName;
            dir = dir.Parent;
        }

        // Walk up from assembly location
        var assemblyLocation = typeof(TestWebAppFactory).Assembly.Location;
        dir = new DirectoryInfo(Path.GetDirectoryName(assemblyLocation)!);
        while (dir is not null)
        {
            if (dir.GetFiles("SIGCM2.slnx").Length > 0)
                return dir.FullName;
            dir = dir.Parent;
        }

        // Known absolute path (last resort for this machine)
        const string knownPath = @"E:\SIG-CM2.0";
        if (Directory.Exists(knownPath) && File.Exists(Path.Combine(knownPath, "SIGCM2.slnx")))
            return knownPath;

        throw new InvalidOperationException(
            $"Could not find repo root containing SIGCM2.slnx. " +
            $"AppContext.BaseDirectory: {AppContext.BaseDirectory}");
    }
}
