using Microsoft.AspNetCore.Builder;
using ZB.MOM.WW.MxGateway.Server;

namespace ZB.MOM.WW.MxGateway.Tests.Configuration;

// Mutates the process-global Secrets__SqlitePath that GatewayApplication.CreateBuilder reads;
// serialized against every other collection so a parallel host-building test cannot inherit the
// deliberately-rejected path. See GlobalEnvironmentCollection.
[Collection(TestSupport.GlobalEnvironmentCollection.Name)]
public sealed class SecretsStorePathGuardTests
{
    private const string SqlitePathVariable = "Secrets__SqlitePath";

    /// <summary>
    /// Verifies the store-path guard runs in the <em>pre-host</em> secrets container, which is the
    /// only place it matters.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <c>CreateBuilder</c> resolves <c>${secret:}</c> references before the host exists, using a
    /// throwaway <see cref="Microsoft.Extensions.DependencyInjection.ServiceCollection"/> that
    /// contains no <c>IHostEnvironment</c> — and it runs the store migrator, which <b>creates the
    /// database</b>. A library that infers the content root from <c>IHostEnvironment</c> alone
    /// cannot distinguish "no content root" from "no host registered" and skips the rule here, so
    /// the store is created at the rejected path and only then does the real host refuse to start.
    /// The leftover empty database with its <c>-wal</c>/<c>-shm</c> siblings is precisely the
    /// artifact that made the 2026-08-09 credential loss read as "the database is there, it's just
    /// empty". The gateway therefore passes the content root explicitly.
    /// </para>
    /// <para>
    /// The assertion that no file was created is the load-bearing one. A test that merely observed
    /// a failed boot would pass even while the store was being written, because the failure arrives
    /// afterwards either way — which is exactly how this defect survived its first release.
    /// </para>
    /// </remarks>
    [Fact]
    public void CreateBuilder_RejectsSecretsStoreUnderContentRoot_WithoutCreatingIt()
    {
        string? original = Environment.GetEnvironmentVariable(SqlitePathVariable);
        string contentRoot = ResolveContentRoot();
        string rejected = Path.Combine(contentRoot, $"probe-secrets-{Guid.NewGuid():N}.db");

        try
        {
            Environment.SetEnvironmentVariable(SqlitePathVariable, rejected);

            // Capture rather than Assert.ThrowsAny, so the store-creation assertions below are
            // reported first. Ordering matters here: "it threw" is the weaker claim, and asserting
            // it first would mask the stronger one — that nothing was written before it threw.
            Exception? thrown = Record.Exception(() => GatewayApplication.CreateBuilder([]));

            Assert.False(File.Exists(rejected), $"the rejected store was created at {rejected}");
            Assert.False(File.Exists(rejected + "-wal"), "a write-ahead log was created for the rejected store");
            Assert.False(File.Exists(rejected + "-shm"), "a shared-memory file was created for the rejected store");
            Assert.NotNull(thrown);
        }
        finally
        {
            Environment.SetEnvironmentVariable(SqlitePathVariable, original);

            // Delete defensively: if the guard ever regresses this test writes a database into the
            // content root, which on a dev machine is the source tree.
            foreach (string leftover in new[] { rejected, rejected + "-wal", rejected + "-shm" })
            {
                if (File.Exists(leftover))
                {
                    File.Delete(leftover);
                }
            }
        }
    }

    /// <summary>
    /// Verifies a store path outside the content root is accepted <em>and actually used</em>.
    /// </summary>
    /// <remarks>
    /// The assertion is that the database exists afterwards, not merely that nothing threw. A
    /// not-null builder is very close to a tautology once no exception escaped, so it would pass
    /// even if the pre-host container had stopped opening the store altogether — which would also
    /// silently void the negative test above, since that one can only observe a file the migration
    /// would otherwise have written. Proving the accepted path gets a real database is what keeps
    /// the rejected-path assertion meaningful.
    /// </remarks>
    [Fact]
    public void CreateBuilder_AcceptsSecretsStoreOutsideContentRoot_AndCreatesIt()
    {
        string? original = Environment.GetEnvironmentVariable(SqlitePathVariable);
        string directory = Directory.CreateTempSubdirectory("mxgw-secrets-ok").FullName;
        string accepted = Path.Combine(directory, "secrets.db");

        try
        {
            Environment.SetEnvironmentVariable(SqlitePathVariable, accepted);

            WebApplicationBuilder builder = GatewayApplication.CreateBuilder([]);

            Assert.NotNull(builder);
            Assert.True(File.Exists(accepted), $"the accepted store was not created at {accepted}");
        }
        finally
        {
            Environment.SetEnvironmentVariable(SqlitePathVariable, original);

            // The store runs in WAL mode with connection pooling, so a pooled handle can outlive the
            // migration and keep secrets.db (plus its -wal/-shm sidecars) open. Windows refuses to
            // delete a directory holding open files where Unix does not, so clear the pool first;
            // the catch is belt-and-braces for a sidecar whose handle outlasts even that.
            Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
            try
            {
                Directory.Delete(directory, recursive: true);
            }
            catch (IOException)
            {
                // Best-effort cleanup of the temp store; a locked file must not fail the test.
            }
            catch (UnauthorizedAccessException)
            {
                // Best-effort cleanup of the temp store; a locked file must not fail the test.
            }
        }
    }

    // The content root CreateBuilder will use, taken from a builder created with the suite's normal
    // (valid) store path rather than assumed from the test's working directory.
    private static string ResolveContentRoot() =>
        GatewayApplication.CreateBuilder([]).Environment.ContentRootPath;
}
