namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests;

/// <summary>
/// The schema-name allow-list is the injection boundary for the one value that cannot be
/// parameterized (a T-SQL object name), so it gets adversarial coverage rather than a happy path.
/// </summary>
public sealed class SqlServerSecretsOptionsTests
{
    [Theory]
    [InlineData("zbsecrets")]
    [InlineData("ZbSecrets")]
    [InlineData("_private")]
    [InlineData("schema_1")]
    public void SchemaName_accepts_plain_identifiers(string value)
    {
        var options = new SqlServerSecretsOptions { SchemaName = value };

        Assert.Equal(value, options.SchemaName);
    }

    [Theory]
    // The bracket-escape trick that would otherwise break out of the [quoting] at the use site.
    [InlineData("evil]]; DROP TABLE secret; --")]
    [InlineData("dbo.secret")]
    [InlineData("with space")]
    [InlineData("quote'name")]
    [InlineData("semi;colon")]
    [InlineData("1leading_digit")]
    [InlineData("")]
    [InlineData("   ")]
    public void SchemaName_rejects_anything_that_is_not_a_plain_identifier(string value)
    {
        Assert.Throws<ArgumentException>(() => new SqlServerSecretsOptions { SchemaName = value });
    }

    [Fact]
    public void SchemaName_rejects_an_over_long_identifier()
    {
        Assert.Throws<ArgumentException>(() =>
            new SqlServerSecretsOptions { SchemaName = new string('a', 129) });
    }

    [Fact]
    public void Validate_rejects_a_missing_connection_string()
    {
        InvalidOperationException ex =
            Assert.Throws<InvalidOperationException>(() => new SqlServerSecretsOptions().Validate());

        Assert.Contains("ConnectionString", ex.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Validate_rejects_a_non_positive_sync_interval()
    {
        var options = new SqlServerSecretsOptions
        {
            ConnectionString = "Server=x;",
            SyncInterval = TimeSpan.Zero,
        };

        Assert.Throws<InvalidOperationException>(options.Validate);
    }

    [Fact]
    public void Validate_rejects_a_non_positive_command_timeout()
    {
        var options = new SqlServerSecretsOptions
        {
            ConnectionString = "Server=x;",
            CommandTimeout = TimeSpan.FromSeconds(-1),
        };

        Assert.Throws<InvalidOperationException>(options.Validate);
    }

    [Fact]
    public void Validate_accepts_a_fully_configured_instance()
    {
        var options = new SqlServerSecretsOptions { ConnectionString = "Server=x;Database=y;" };

        options.Validate();
    }

    [Fact]
    public void CreateSchemaDdl_quotes_the_schema_name_everywhere_it_appears()
    {
        string ddl = SqlServerSecretsSchema.CreateSchemaDdl("zbsecrets");

        Assert.Contains("[zbsecrets].[secret]", ddl, StringComparison.Ordinal);
        Assert.Contains("[zbsecrets].[schema_version]", ddl, StringComparison.Ordinal);
        // Every DDL statement is guarded, so re-running the migration on a provisioned database is
        // a no-op rather than an error.
        Assert.Contains("IF NOT EXISTS", ddl, StringComparison.Ordinal);
        Assert.Contains("IS NULL", ddl, StringComparison.Ordinal);
    }
}
