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

eval "use DBD::SQLite 1.74";
plan skip_all => "DBD::SQLite 1.74 required" if $@;

my $db = DBIx::Fast->new(
    dsn        => 'dbi:SQLite:dbname=:memory:',
    driver     => 'SQLite',
    RaiseError => 1,
    PrintError => 0,
);

$db->exec(q{
    CREATE TABLE products (
        sku   TEXT PRIMARY KEY,
        name  TEXT,
        stock INTEGER,
        price REAL
    )
});

# --- upsert: insert path -----------------------------------------------------

subtest 'upsert inserts when no conflict' => sub {
    $db->upsert('products',
        { sku => 'A', name => 'Apple', stock => 10, price => 1.5 },
        ['sku'],
    );

    my $row = $db->hash('SELECT * FROM products WHERE sku = ?', 'A');
    is($row->{name},  'Apple', 'name inserted');
    is($row->{stock}, 10,      'stock inserted');
    is($row->{price}, 1.5,     'price inserted');
};

subtest 'upsert updates on conflict (default: all non-key cols)' => sub {
    $db->upsert('products',
        { sku => 'A', name => 'Apple v2', stock => 20, price => 2.0 },
        ['sku'],
    );

    my $row = $db->hash('SELECT * FROM products WHERE sku = ?', 'A');
    is($row->{name},  'Apple v2', 'name updated');
    is($row->{stock}, 20,         'stock updated');
    is($row->{price}, 2.0,        'price updated');

    is($db->val('SELECT COUNT(*) FROM products'), 1,
       'still one row — was UPDATE, not duplicate INSERT');
};

subtest 'upsert with explicit update_cols updates only those columns' => sub {
    $db->upsert('products',
        { sku => 'A', name => 'IGNORED', stock => 99, price => 9.99 },
        ['sku'],
        ['stock'],   # only update stock
    );

    # Check SQL shape *immediately* before any other query touches last_sql
    like($db->last_sql, qr/ON CONFLICT \("sku"\) DO UPDATE SET/,
         'SQL uses ON CONFLICT syntax (SQLite/Pg)');
    like($db->last_sql, qr/"stock" = excluded\."stock"/,
         'uses excluded.col reference');
    unlike($db->last_sql, qr/"name" = excluded/,
           'name NOT in SET clause (was excluded from update_cols)');

    my $row = $db->hash('SELECT * FROM products WHERE sku = ?', 'A');
    is($row->{name},  'Apple v2', 'name preserved (not in update_cols)');
    is($row->{price}, 2.0,        'price preserved (not in update_cols)');
    is($row->{stock}, 99,         'stock updated');
};

subtest 'upsert raises when nothing to update' => sub {
    throws_ok {
        $db->upsert('products',
            { sku => 'only_key' },
            ['sku'],
        );
    } qr/nothing to update on conflict/,
        'rejects empty update set';
};

# --- insert_many -------------------------------------------------------------

subtest 'insert_many inserts a batch in one statement' => sub {
    $db->exec('DELETE FROM products');
    my $n = $db->insert_many('products', [
        { sku => 'B', name => 'Banana',  stock => 5, price => 0.5 },
        { sku => 'C', name => 'Cherry',  stock => 8, price => 1.2 },
        { sku => 'D', name => 'Date',    stock => 3, price => 2.0 },
    ]);

    # Check SQL shape *before* any other query overwrites last_sql
    my @placeholders = $db->last_sql =~ /\(\?, \?, \?, \?\)/g;
    is(scalar @placeholders, 3,
       'single INSERT with 3 row-tuples (proves bulk, not per-row loop)');

    is($n, 3, 'returns row count');
    is($db->val('SELECT COUNT(*) FROM products'), 3, 'all rows persisted');
};

subtest 'insert_many rejects mismatched row schemas' => sub {
    throws_ok {
        $db->insert_many('products', [
            { sku => 'X', name => 'X', stock => 1, price => 1 },
            { sku => 'Y', name => 'Y' },  # missing stock, price
        ]);
    } qr/row 1 has different columns than row 0/,
        'rejects row with missing column';

    throws_ok {
        $db->insert_many('products', [
            { sku => 'X', name => 'X', stock => 1, price => 1 },
            { sku => 'Y', name => 'Y', stock => 1, price => 1, extra => 1 },
        ]);
    } qr/row 1 has different columns than row 0/,
        'rejects row with extra column';
};

subtest 'insert_many rejects empty batch' => sub {
    throws_ok { $db->insert_many('products', []) }
        qr/at least one row required/,
        'rejects empty array';

    throws_ok { $db->insert_many('products', 'not-an-array') }
        qr/at least one row required/,
        'rejects non-arrayref';
};

# --- upsert_many -------------------------------------------------------------

subtest 'upsert_many mixes inserts and updates' => sub {
    $db->exec('DELETE FROM products');
    $db->insert('products',
                { sku => 'A', name => 'A', stock => 1, price => 1 });

    my $n = $db->upsert_many('products',
        [
            { sku => 'A', name => 'A-updated', stock => 100, price => 10 },
            { sku => 'E', name => 'E',         stock => 2,   price => 2  },
            { sku => 'F', name => 'F',         stock => 3,   price => 3  },
        ],
        ['sku'],
    );
    is($n, 3, 'returns batch size');

    is($db->val('SELECT COUNT(*) FROM products'), 3,
       'A updated in place + E and F inserted');
    is($db->val('SELECT stock FROM products WHERE sku=?', 'A'), 100,
       'A stock updated');
    is($db->val('SELECT name  FROM products WHERE sku=?', 'E'), 'E',
       'E inserted');
};

# --- Security: identifier injection blocked ----------------------------------

subtest 'upsert rejects identifier injection in all parameter positions' => sub {
    throws_ok {
        $db->upsert('products',
            { q{sku, name) VALUES ('pwned','x'} => 'evil' },
            ['sku'],
        );
    } qr/Invalid identifier/,
        'row keys validated';

    throws_ok {
        $db->upsert('products',
            { sku => 'Z' },
            [q{sku; DROP TABLE products}],
        );
    } qr/Invalid identifier/,
        'conflict_keys validated';

    throws_ok {
        $db->upsert('products',
            { sku => 'Z', name => 'Z' },
            ['sku'],
            [q{name = (SELECT secret FROM x), id}],
        );
    } qr/Invalid identifier/,
        'update_cols validated';
};

subtest 'insert_many rejects identifier injection in row keys' => sub {
    throws_ok {
        $db->insert_many('products', [
            { q{sku) VALUES ('pwned'); -- } => 'x' },
        ]);
    } qr/Invalid identifier/,
        'malicious row keys rejected';
};

subtest 'upsert_many rejects identifier injection' => sub {
    throws_ok {
        $db->upsert_many('products',
            [ { sku => 'Z', name => 'Z', stock => 1, price => 1 } ],
            [ q{sku, name = (SELECT secret FROM x)} ],
        );
    } qr/Invalid identifier/,
        'malicious conflict_keys rejected';
};

done_testing;
