using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Host;
using ZB.MOM.WW.Secrets.Sqlite;

namespace ZB.MOM.WW.ScadaBridge.Host.Tests;

/// <summary>
/// Guards the content-root argument on the Layer-A pre-host <c>${secret:}</c> expander
/// (<see cref="SecretsRegistration.AddPreHostSqliteExpander"/>, called from <c>Program.cs</c>).
///
/// <para>
/// The argument is load-bearing and its absence is SILENT. The expander composes secrets into a
/// throwaway <see cref="ServiceCollection"/> with no <c>IHostEnvironment</c>; without an explicit
/// content root the under-content-root rule does not run, the migrator creates the store at the
/// rejected path, and the boot then fails once the real host registers <c>IHostEnvironment</c>.
/// The leftover empty database is the "the file is there, it's just empty" artifact that made the
/// 2026-08-09 MxGateway outage read as corruption rather than as a deployment error.
/// </para>
///
/// <para>
/// <b>These tests must resolve and run <see cref="SqliteSecretsStoreMigrator"/>.</b> Resolving
/// only the connection factory would make every file assertion here vacuous — that constructor
/// never touches the filesystem, so no store is created whether the guard runs or not, and the
/// tests would pass for the wrong reason. <see cref="Control_ValidPath_ActuallyCreatesAStore"/>
/// exists to prove the harness really does create files.
/// </para>
///
/// <para>
/// <b>Assertion order is deliberate: artifact first, outcome second.</b> Asserting "it threw"
/// first masks the real finding — a regression that stops the throw but still creates the store
/// would report "no exception was thrown" and never mention the database sitting at the rejected
/// path, which IS the defect. Verified by simulating the regression (dropping to the 3-argument
/// overload) and confirming these fail on the FILE assertion, not the exception one.
/// </para>
/// </summary>
public class PreHostSecretsContentRootTests
{
    private static string NewDir() =>
        Path.Combine(Path.GetTempPath(), "prehost-secrets-" + Guid.NewGuid().ToString("N"));

    private static IConfiguration Config(string sqlitePath) =>
        new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Secrets:SqlitePath"] = sqlitePath,
            ["Secrets:MasterKey:Source"] = "Environment",
            ["Secrets:MasterKey:EnvVarName"] = "ZB_SECRETS_MASTER_KEY",
        }).Build();

    /// <summary>
    /// Mimics the pre-host expander exactly: throwaway collection, no IHostEnvironment,
    /// register, then run the migrator — which is what creates the database.
    /// </summary>
    private static async Task<Exception?> ComposeAndMigrate(string contentRoot, string sqlitePath)
    {
        Directory.CreateDirectory(contentRoot);
        var services = new ServiceCollection();
        services.AddLogging();

        return await Record.ExceptionAsync(async () =>
        {
            services.AddPreHostSqliteExpander(Config(sqlitePath), contentRoot);
            await using var provider = services.BuildServiceProvider();
            await provider.GetRequiredService<SqliteSecretsStoreMigrator>().MigrateAsync(default);
        });
    }

    private static void AssertNoStoreAt(string path)
    {
        string[] leftovers = new[] { path, path + "-wal", path + "-shm" }
            .Where(File.Exists)
            .ToArray();

        Assert.True(
            leftovers.Length == 0,
            "A rejected path must not be left holding a store file — an empty database at the "
                + "rejected path is exactly the artifact this guard exists to prevent. Found: "
                + string.Join(", ", leftovers));
    }

    [Fact]
    public async Task Control_ValidPath_ActuallyCreatesAStore()
    {
        // Proves the harness is capable of creating a store, so that the "no file" assertions
        // in the other two tests are meaningful rather than vacuous.
        var storeDir = NewDir();
        Directory.CreateDirectory(storeDir);
        var path = Path.Combine(storeDir, "secrets.db");

        var thrown = await ComposeAndMigrate(NewDir(), path);

        Assert.Null(thrown);
        Assert.True(File.Exists(path), "the control must create a store, or the guards prove nothing");
    }

    [Fact]
    public async Task PathInsideContentRoot_IsRejected_AndLeavesNoStore()
    {
        var contentRoot = NewDir();
        var path = Path.Combine(contentRoot, "data", "scadabridge-secrets.db");

        var thrown = await ComposeAndMigrate(contentRoot, path);

        AssertNoStoreAt(path);
        Assert.NotNull(thrown);
    }

    [Fact]
    public async Task RelativePath_IsRejected_AndLeavesNoStore()
    {
        // Unique name: a stale scadabridge-secrets.db from an earlier run under the old relative
        // default sits in the test output directory and would make this assertion lie.
        var path = "prehost-probe-" + Guid.NewGuid().ToString("N") + ".db";

        var thrown = await ComposeAndMigrate(NewDir(), path);

        AssertNoStoreAt(path);
        Assert.NotNull(thrown);
    }
}
