using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;

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

/// <summary>
/// Regression guard for ConfigurationDatabase-004: secret-bearing columns
/// (SMTP credentials, external-system auth config, database connection strings)
/// must be encrypted at rest, not persisted verbatim.
/// </summary>
public class SecretEncryptionTests : IDisposable
{
    private readonly ScadaBridgeDbContext _context;
    private readonly IDataProtectionProvider _protectionProvider;

    public SecretEncryptionTests()
    {
        _protectionProvider = new EphemeralDataProtectionProvider();

        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite("DataSource=:memory:")
            .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
            .Options;

        _context = new ScadaBridgeDbContext(options, _protectionProvider);
        _context.Database.OpenConnection();
        _context.Database.EnsureCreated();
    }

    public void Dispose()
    {
        _context.Database.CloseConnection();
        _context.Dispose();
    }

    [Fact]
    public async Task DatabaseConnectionDefinition_ConnectionString_StoredEncrypted_RoundTrips()
    {
        const string secret = "Server=db;Database=X;User Id=svc;Password=SuperSecret123!";
        var def = new DatabaseConnectionDefinition("PrimaryDb", secret);
        _context.DatabaseConnectionDefinitions.Add(def);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        // Raw column value must not be the plaintext secret.
        var raw = await ReadRawColumnAsync("DatabaseConnectionDefinitions", "ConnectionString", def.Id);
        Assert.NotNull(raw);
        Assert.NotEqual(secret, raw);
        Assert.DoesNotContain("SuperSecret123!", raw);

        // Reading back through EF must transparently decrypt.
        var loaded = await _context.DatabaseConnectionDefinitions.SingleAsync(d => d.Id == def.Id);
        Assert.Equal(secret, loaded.ConnectionString);
    }

    [Fact]
    public async Task SmtpConfiguration_Credentials_StoredEncrypted_RoundTrips()
    {
        const string secret = "client_secret=oauth2-abc-very-secret";
        var smtp = new SmtpConfiguration("smtp.example.com", "OAuth2", "noreply@example.com")
        {
            Credentials = secret
        };
        _context.SmtpConfigurations.Add(smtp);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var raw = await ReadRawColumnAsync("SmtpConfigurations", "Credentials", smtp.Id);
        Assert.NotNull(raw);
        Assert.NotEqual(secret, raw);
        Assert.DoesNotContain("oauth2-abc-very-secret", raw);

        var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id);
        Assert.Equal(secret, loaded.Credentials);
    }

    [Fact]
    public async Task ExternalSystemDefinition_AuthConfiguration_StoredEncrypted_RoundTrips()
    {
        const string secret = "{\"apiKey\":\"live-key-do-not-leak\"}";
        var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "ApiKey")
        {
            AuthConfiguration = secret
        };
        _context.ExternalSystemDefinitions.Add(ext);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var raw = await ReadRawColumnAsync("ExternalSystemDefinitions", "AuthConfiguration", ext.Id);
        Assert.NotNull(raw);
        Assert.NotEqual(secret, raw);
        Assert.DoesNotContain("live-key-do-not-leak", raw);

        var loaded = await _context.ExternalSystemDefinitions.SingleAsync(e => e.Id == ext.Id);
        Assert.Equal(secret, loaded.AuthConfiguration);
    }

    [Fact]
    public async Task SmtpConfiguration_NullCredentials_RoundTripsAsNull()
    {
        var smtp = new SmtpConfiguration("smtp.example.com", "None", "noreply@example.com")
        {
            Credentials = null
        };
        _context.SmtpConfigurations.Add(smtp);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id);
        Assert.Null(loaded.Credentials);
    }

    [Fact]
    public async Task SmsConfiguration_AuthToken_StoredEncrypted_RoundTrips()
    {
        // ConfigurationDatabase-NNN regression: the Twilio Auth Token is a secret and
        // must be encrypted at rest exactly like SmtpConfiguration.Credentials. Guards
        // against a future refactor dropping SmsConfiguration from ApplySecretColumnEncryption.
        const string secret = "twilio-auth-token-do-not-leak";
        var sms = new SmsConfiguration("AC0123456789", "+15550000000")
        {
            AuthToken = secret
        };
        _context.SmsConfigurations.Add(sms);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var raw = await ReadRawColumnAsync("SmsConfigurations", "AuthToken", sms.Id);
        Assert.NotNull(raw);
        Assert.NotEqual(secret, raw);
        Assert.DoesNotContain("twilio-auth-token-do-not-leak", raw);

        var loaded = await _context.SmsConfigurations.SingleAsync(s => s.Id == sms.Id);
        Assert.Equal(secret, loaded.AuthToken);
        // AccountSid is NOT a secret (it also rides in the Twilio URL path) — stored verbatim.
        var rawSid = await ReadRawColumnAsync("SmsConfigurations", "AccountSid", sms.Id);
        Assert.Equal("AC0123456789", rawSid);
    }

    [Fact]
    public async Task SmsConfiguration_NullAuthToken_RoundTripsAsNull()
    {
        var sms = new SmsConfiguration("AC0123456789", "+15550000000")
        {
            AuthToken = null
        };
        _context.SmsConfigurations.Add(sms);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var loaded = await _context.SmsConfigurations.SingleAsync(s => s.Id == sms.Id);
        Assert.Null(loaded.AuthToken);
    }

    private async Task<string?> ReadRawColumnAsync(string table, string column, int id)
    {
        var connection = _context.Database.GetDbConnection();
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = $"SELECT \"{column}\" FROM \"{table}\" WHERE \"Id\" = $id";
        var p = cmd.CreateParameter();
        p.ParameterName = "$id";
        p.Value = id;
        cmd.Parameters.Add(p);
        var result = await cmd.ExecuteScalarAsync();
        return result == null || result == DBNull.Value ? null : (string)result;
    }
}
