using ClawdDotNet.Tools.Database;
using Shouldly;

namespace ClawdDotNet.Tools.Tests.Database;

/// <summary>
/// S1 aus der Bestandsaufnahme: Die Tabellen-Whitelist suchte den erlaubten Namen als
/// Teilzeichenkette irgendwo im Statement — auch in einem Kommentar. Bei
/// allowedTables ["prices"] genügte deshalb "DELETE FROM users -- prices", um eine
/// beliebige andere Tabelle zu löschen.
///
/// Die Gegenproben sind genauso wichtig wie die Angriffe: Ein zu strenger Filter, der
/// legitime Abfragen blockiert, wäre im Betrieb genauso unbrauchbar.
/// </summary>
public sealed class SqlGuardTests
{
    private static readonly string[] Allowed = ["prices", "signals", "portfolio"];

    private static SqlInspection Inspect(string sql) => SqlGuard.Inspect(sql, Allowed);

    // ═══════════════════════════════════════════════════════════
    //  Angriffe — alle müssen abgelehnt werden
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData("DELETE FROM users -- prices")]
    [InlineData("DELETE FROM users /* prices */")]
    [InlineData("DROP TABLE users -- prices")]
    [InlineData("UPDATE users SET admin=1 -- prices")]
    [InlineData("SELECT * FROM users # prices")]
    public void Kommentare_koennen_keine_erlaubte_Tabelle_vortaeuschen(string sql)
    {
        var result = Inspect(sql);

        result.IsValid.ShouldBeFalse($"'{sql}' muss abgelehnt werden");
    }

    [Theory]
    [InlineData("SELECT * FROM users WHERE note = 'prices'")]
    [InlineData("INSERT INTO users (name) VALUES ('prices')")]
    [InlineData("SELECT * FROM users WHERE a = \"prices\"")]
    public void String_Literale_koennen_keine_erlaubte_Tabelle_vortaeuschen(string sql)
    {
        Inspect(sql).IsValid.ShouldBeFalse();
    }

    [Theory]
    [InlineData("SELECT * FROM prices; DROP TABLE users")]
    [InlineData("SELECT * FROM prices; DELETE FROM users")]
    [InlineData("SELECT 1; SELECT 2")]
    public void Mehrere_Statements_werden_abgelehnt(string sql)
    {
        var result = Inspect(sql);

        result.IsValid.ShouldBeFalse();
        result.Error.ShouldContain("Mehrere Statements");
    }

    [Theory]
    [InlineData("SELECT * FROM prices JOIN users ON users.id = prices.uid")]
    [InlineData("SELECT * FROM prices, users")]
    [InlineData("INSERT INTO users SELECT * FROM prices")]
    public void Ein_einziger_unerlaubter_Join_reicht_zur_Ablehnung(string sql)
    {
        // Entscheidend: JEDE referenzierte Tabelle muss erlaubt sein, nicht irgendeine.
        var result = Inspect(sql);

        result.IsValid.ShouldBeFalse();
        result.Error.ShouldContain("users");
    }

    [Fact]
    public void Ohne_Whitelist_wird_nichts_durchgelassen()
    {
        SqlGuard.Inspect("SELECT * FROM prices", []).IsValid.ShouldBeFalse();
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Leere_Statements_werden_abgelehnt(string? sql)
    {
        SqlGuard.Inspect(sql, Allowed).IsValid.ShouldBeFalse();
    }

    // ═══════════════════════════════════════════════════════════
    //  Gegenproben — legitime Abfragen müssen durchgehen
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData("SELECT * FROM prices")]
    [InlineData("SELECT ticker, close FROM prices WHERE ticker = 'NVDA' ORDER BY ts DESC LIMIT 10")]
    [InlineData("SELECT * FROM prices JOIN signals ON signals.ticker = prices.ticker")]
    [InlineData("select * from PRICES")]
    [InlineData("SELECT * FROM prices;")]
    public void Erlaubte_Leseabfragen_gehen_durch(string sql)
    {
        var result = Inspect(sql);

        result.IsValid.ShouldBeTrue(result.Error);
        result.Operation.ShouldBe(SqlOperation.Read);
    }

    [Theory]
    [InlineData("SELECT * FROM prices WHERE note = 'update'")]
    [InlineData("SELECT * FROM prices WHERE kommentar = 'bitte delete beachten'")]
    [InlineData("SELECT * FROM prices WHERE aktion = 'drop'")]
    public void Harmlose_Schluesselwoerter_in_Werten_gelten_nicht_als_Schreibzugriff(string sql)
    {
        // Der alte Substring-Test stufte diese Abfragen fälschlich als Schreib- bzw.
        // Strukturzugriff ein und blockierte sie für ReadOnly-Agenten.
        var result = Inspect(sql);

        result.IsValid.ShouldBeTrue(result.Error);
        result.Operation.ShouldBe(SqlOperation.Read);
    }

    [Theory]
    [InlineData("UPDATE prices SET close = 1 WHERE id = 2", SqlOperation.Write)]
    [InlineData("INSERT INTO signals (ticker) VALUES ('NVDA')", SqlOperation.Write)]
    [InlineData("DELETE FROM portfolio WHERE id = 5", SqlOperation.Write)]
    [InlineData("DROP TABLE prices", SqlOperation.Schema)]
    [InlineData("ALTER TABLE prices ADD COLUMN x INT", SqlOperation.Schema)]
    [InlineData("TRUNCATE TABLE prices", SqlOperation.Schema)]
    public void Die_Operation_wird_am_ersten_Schluesselwort_bestimmt(string sql, SqlOperation expected)
    {
        var result = Inspect(sql);

        result.Operation.ShouldBe(expected);
        result.IsValid.ShouldBeTrue(result.Error);
    }

    [Fact]
    public void Schema_qualifizierte_Namen_werden_erkannt()
    {
        var result = Inspect("SELECT * FROM crypto.prices WHERE id = 1");

        result.IsValid.ShouldBeTrue(result.Error);
    }

    [Fact]
    public void Der_Fehlertext_nennt_die_abgelehnte_Tabelle_und_die_erlaubten()
    {
        var result = Inspect("SELECT * FROM geheim");

        result.Error.ShouldContain("geheim");
        result.Error.ShouldContain("prices");
    }

    [Fact]
    public void Escaptes_Apostroph_im_Literal_bricht_die_Erkennung_nicht()
    {
        var result = Inspect("SELECT * FROM prices WHERE name = 'O''Brien'");

        result.IsValid.ShouldBeTrue(result.Error);
    }
}
