use strict;
use warnings;
use Test::More;
use Test::Exception;

my $dsn = $ENV{DBIX_FAST_TEST_MARIADB}
  or plan skip_all => 'DBIX_FAST_TEST_MARIADB env var not set '
                    . '(e.g. mariadb://root:test123@127.0.0.1:3307/audit)';

eval "use DBD::MariaDB 1.23";
plan skip_all => 'DBD::MariaDB 1.23 required' if $@;
eval { require DBIx::Fast; 1 }
  or plan skip_all => 'DBIx::Fast required';

DBIx::Fast->import;

my $db = eval {
    DBIx::Fast->new(
        dsn        => $dsn,
        RaiseError => 1,
        PrintError => 0,
    );
};
plan skip_all => "Cannot connect to MariaDB ($dsn): $@" if $@ or !$db;

# Schema for the audit
$db->exec(q{DROP TABLE IF EXISTS secmdb_users});
$db->exec(q{
    CREATE TABLE secmdb_users (
        id     INT PRIMARY KEY AUTO_INCREMENT,
        name   VARCHAR(64),
        secret VARCHAR(128)
    )
});

$db->exec(q{INSERT INTO secmdb_users (id, name, secret) VALUES (1, 'alice', 'the_real_password')});
$db->exec(q{INSERT INTO secmdb_users (id, name, secret) VALUES (2, 'bob', 'bob_password')});

# --- Pre-fix exploit confirmed against MariaDB 11; these tests now ensure
#     the fix (SQL::Abstract quote_char + _safe_columns) blocks the attack.

subtest 'MariaDB: SQL::Abstract uses backtick quoting' => sub {
    $db->insert('secmdb_users', { name => 'QuoteCheck' });
    like($db->last_sql, qr/`secmdb_users`/,
         'INSERT uses backtick-quoted table for MariaDB');
    like($db->last_sql, qr/`name`/,
         'INSERT uses backtick-quoted columns for MariaDB');
    $db->delete('secmdb_users', { name => 'QuoteCheck' });
};

subtest 'MariaDB: insert() rejects identifier injection' => sub {
    throws_ok {
        $db->insert('secmdb_users', {
            q{name = (SELECT secret FROM secmdb_users), id} => 99,
        });
    } qr/Invalid identifier/,
        'insert() identifier injection blocked';
};

subtest 'MariaDB: update() rejects SET injection (this is the original POC)' => sub {
    my $before = $db->all('SELECT id, name, secret FROM secmdb_users ORDER BY id');

    throws_ok {
        $db->update('secmdb_users', {
            sen   => {
                q{name = (SELECT secret FROM (SELECT secret FROM secmdb_users WHERE id=1) tmp), id} => 99,
            },
            where => { id => 1 },
        });
    } qr/Invalid identifier/,
        'update() SET-clause identifier injection blocked';

    my $after = $db->all('SELECT id, name, secret FROM secmdb_users ORDER BY id');
    is_deeply($after, $before, 'data unchanged after blocked injection');

    # Pre-fix this same payload exfiltrated "the_real_password" into name,
    # and changed id to 99. Verify alice is still alice.
    my $alice = $db->hash('SELECT * FROM secmdb_users WHERE id = 1');
    is($alice->{name}, 'alice',  'alice.name preserved');
    is($alice->{secret}, 'the_real_password', 'alice.secret preserved');
};

subtest 'MariaDB: delete() rejects identifier injection in WHERE' => sub {
    throws_ok {
        $db->delete('secmdb_users', { q{id; DROP TABLE secmdb_users} => 1 });
    } qr/Invalid identifier/,
        'delete() WHERE identifier injection blocked';

    ok($db->val('SELECT COUNT(*) FROM secmdb_users') >= 2,
       'secmdb_users table still present and populated');
};

subtest 'MariaDB: up() rejects malicious time_col' => sub {
    throws_ok {
        $db->up('secmdb_users', { name => 'bob2' }, { id => 2 },
                q{updated_at, name = (SELECT secret FROM secmdb_users WHERE id=1)});
    } qr/Invalid identifier/,
        'up() time_col identifier injection blocked';
};

subtest 'MariaDB: normal CRUD still works' => sub {
    lives_ok {
        $db->insert('secmdb_users', { name => 'carol', secret => 's3cr3t' });
    } 'insert() works';
    my $last = $db->last_id;
    ok($last, 'last_id is set');

    lives_ok {
        $db->update('secmdb_users',
            { sen => { name => 'caroline' }, where => { id => $last } });
    } 'update() works';

    is($db->val('SELECT name FROM secmdb_users WHERE id = ?', $last), 'caroline',
       'update() persisted');

    lives_ok {
        $db->delete('secmdb_users', { id => $last });
    } 'delete() works';
};

subtest 'MariaDB: txn() applies requested isolation level' => sub {
    for my $req ('READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE') {
        my $observed;
        $db->txn(sub {
            my $row = $db->hash(
                q{SELECT @@session.transaction_isolation AS lvl});
            $observed = $row->{lvl};
        }, { isolation => $req });

        my $req_norm = $req;
        $req_norm =~ s/ /-/g;
        is($observed, $req_norm,
           "txn() with isolation='$req' actually set the level inside the transaction");
    }
};

subtest 'MariaDB: txn() does not leak isolation to subsequent transactions' => sub {
    # Establish baseline outside any txn
    my $baseline = $db->val(
        q{SELECT @@session.transaction_isolation});
    ok($baseline, "baseline isolation: $baseline");

    # Run a txn with a different isolation
    $db->txn(sub {
        my $inside = $db->val(
            q{SELECT @@session.transaction_isolation});
        is($inside, 'SERIALIZABLE',
           'inside txn: isolation is SERIALIZABLE');
    }, { isolation => 'SERIALIZABLE' });

    # Verify session reverted (MariaDB scope of SET TRANSACTION is next txn only)
    my $after = $db->val(
        q{SELECT @@session.transaction_isolation});
    is($after, $baseline,
       'session isolation returns to baseline after the txn');
};

# Cleanup so the test is idempotent against re-runs
$db->exec(q{DROP TABLE IF EXISTS secmdb_users});

done_testing;
