#!perl
use strict;
use warnings FATAL => 'all';

use Test::More 0.98;
use Test::Exception;
use File::Temp qw(tempdir);
use File::Spec;
use DBIx::Fast;

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

# Always a throwaway temp DB: this suite writes, and the committed fixture
# t/db/test.db must stay read-only (schema.t asserts its exact contents,
# and a mutated committed binary dirties the working tree on every run).
my ( $db, $db_path, $dsn );

my $tmpdir = tempdir( CLEANUP => 1 );
$db_path = File::Spec->catfile( $tmpdir, 'test.sqlite' );
$dsn     = "dbi:SQLite:dbname=$db_path";

sub new_db {
    return DBIx::Fast->new(
        dsn        => $dsn,
        driver     => 'SQLite',
        PrintError => 1,
        tn         => 0
    );
}

subtest 'constructor' => sub {
    $db = new_db();

    isa_ok( $db, 'DBIx::Fast' );
    ok( $db->db, 'has db handle' );
    is( $db->dbd, 'SQLite', 'driver detected as SQLite' ) if $db->dbd;

    done_testing;
};

subtest 'schema setup' => sub {
    lives_ok {
        $db->exec(
            q{
            CREATE TABLE test (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                name       TEXT NOT NULL,
                status     INTEGER,
                time       TEXT
            )
        }
        );
    }
    'created table test';

    done_testing;
};

subtest 'capabilities' => sub {
    can_ok( $db, qw(insert update up delete q val all hash array count) );

    done_testing;
};

subtest 'cleanup & error capture' => sub {
    lives_ok { $db->delete( 'test', { id => { '>' => 0 } } ) }
    'wipe table test';

    # PrintError => 1 makes DBI warn the error too; capture it so the
    # deliberate missing-table probes do not leak noise into the output.
    my @warnings;
    local $SIG{__WARN__} = sub { push @warnings, @_ };

    for (qw(be eb)) {
        throws_ok { $db->execute("SELECT * FROM $_") }
            qr/no such table|Exception/i,
            "Exception => No such table ($_)";
    }

    is scalar( grep { /no such table/ } @warnings ), 2,
        'PrintError warned once per missing table';

    is ref $db->errors, 'ARRAY', 'errors array present';
    cmp_ok scalar( @{ $db->errors } ), '>=', 2, 'errors captured (>=2)';

    done_testing;
};

subtest 'empty counts' => sub {
    is $db->val('select count(*) from test'), 0, 'empty via val()';
    is $db->count('test'),                    0, 'count() empty';
    is $db->count( 'test', { id => { '>' => 999 } } ), 0,
      'count() with WHERE empty';

    done_testing;
};

subtest 'insert + last_id' => sub {
    ok $db->insert( 'test', { name => 'test', status => 1 }, time => 'time' ),
      'insert returns truthy';
    cmp_ok $db->last_id, '>=', 1, 'last insert id set';

    done_testing;
};

subtest 'update (WHERE with 1 and 2 keys)' => sub {
    $db->update(
        'test',
        { sen => { name => 'update t3st' }, where => { id => 1 } },
        time => 'time'
    );

    my $v = $db->val('select name from test where id = 1');
    like $v, qr/update t3st/, 'update with 1 where key';

    $db->update(
        'test',
        {
            sen   => { name => 'Mator Change' },
            where => { id   => 1, name => 'update t3st' }
        },
        time => 'time'
    );
    $v = $db->val('select name from test where id = 1');
    like $v, qr/Mator Change/, 'update with 2 where keys';

    done_testing;
};

subtest 'update (avoid duplicate keys) + up()' => sub {
    $db->update(
        'test',
        {
            sen   => { name => 'Maxim SQL' },
            where => { id   => 1, name => 'Mator Change' }
        },
        time => 'time'
    );

    my $v = $db->val('select name from test where id = 1');
    like $v, qr/Maxim SQL/, 'update stable';

    $db->up( 'test', { name => 'update test' }, { id => 1 }, 'time' );

    $v = $db->val( 'select name from test where id = ?', 1 );
    like $v, qr/update test/, 'up() updated row';

    ## Contract: val() returns the FIRST column of the first row. A multi-column
    ## SELECT must never return a later column (regression: a 0.17 build returned
    ## the last one and pagination counters showed prices instead of totals).
    $v = $db->val( 'select id, name from test where id = ?', 1 );
    is $v, 1, 'val() multi-column returns first column';

    done_testing;
};

subtest 'bulk inserts + flat/array/results' => sub {
    $db->insert( 'test', { name => rand(6) }, time => 'time' ) for 1 .. 5;

    my @flat = $db->flat('SELECT name FROM test WHERE 1');

    is ref \@flat, 'ARRAY', 'flat() returns list';
    cmp_ok scalar(@flat), '>=', 6, 'flat() >= 6 rows';

    $db->flat('SELECT name FROM test WHERE 1');
    is ref $db->results, 'ARRAY', 'results after flat() is arrayref';

    $db->array('SELECT * FROM test WHERE 1');
    is ref $db->results, 'ARRAY', 'results after array() is arrayref';

    $db->hash( 'SELECT * FROM test WHERE id = ? ', 1 );

    is ref $db->results, 'HASH', 'results after hash() is hashref';
    like $db->results->{time}, qr/^\d{4}-\d{2}-\d{2} /,
      'time is timestamp (NOW)';

    $db->all('select * from test');
    is ref $db->results, 'ARRAY', 'results after all() is arrayref';

    done_testing;
};

subtest 'delete variants + last_sql' => sub {
    ok $db->delete( 'test', { id => { '=' => $db->last_id } } ),
      'delete last_id';
    is $db->sql, 'DELETE FROM "test" WHERE "id" = ?', 'Delete SQL (=)';

    $db->delete( 'test', { id => { '>' => 999 } } );
    is $db->sql, 'DELETE FROM "test" WHERE "id" > ?', 'Delete SQL (>)';

    $db->delete( 'test', { id => 999 } );
    is $db->sql, 'DELETE FROM "test" WHERE "id" = ?', 'Delete SQL (= scalar)';

    done_testing;
};

subtest 'delete compound WHERE' => sub {
    # Insert a row to delete with compound WHERE
    $db->insert( 'test', { name => 'compound_del', status => 77 } );
    my $cid = $db->last_id;
    ok $cid, "inserted row for compound delete (id=$cid)";

    # Delete with two WHERE keys
    lives_ok { $db->delete( 'test', { id => $cid, status => 77 } ) }
      'delete with compound WHERE does not die';

    # Verify it was actually deleted
    my $check = $db->val( 'SELECT id FROM test WHERE id = ?', $cid );
    ok !$check, 'row deleted by compound WHERE';

    # Insert again and try with wrong compound (should not delete)
    $db->insert( 'test', { name => 'compound_safe', status => 88 } );
    my $cid2 = $db->last_id;

    $db->delete( 'test', { id => $cid2, status => 99 } );    # wrong status
    my $still = $db->val( 'SELECT id FROM test WHERE id = ?', $cid2 );
    ok $still, 'row NOT deleted when compound WHERE does not match';

    # Cleanup
    $db->delete( 'test', { id => $cid2 } );

    done_testing;
};

subtest 'count compound WHERE' => sub {
    $db->insert( 'test', { name => 'count_a', status => 55 } );
    $db->insert( 'test', { name => 'count_b', status => 55 } );
    $db->insert( 'test', { name => 'count_c', status => 66 } );

    my $total_55 = $db->count( 'test', { status => 55 } );
    cmp_ok $total_55, '>=', 2, 'count with single WHERE key';

    my $exact = $db->count( 'test', { name => 'count_a', status => 55 } );
    is $exact, 1, 'count with compound WHERE returns 1';

    my $zero = $db->count( 'test', { name => 'count_a', status => 66 } );
    is $zero, 0, 'count with mismatched compound WHERE returns 0';

    # Cleanup
    $db->delete( 'test', { name => 'count_a' } );
    $db->delete( 'test', { name => 'count_b' } );
    $db->delete( 'test', { name => 'count_c' } );

    done_testing;
};

subtest '_make_where SQL generation' => sub {
    # Test internal SQL generation directly
    $db->sql('SELECT * FROM test');
    $db->_make_where({ id => 1 });
    like $db->sql, qr/WHERE "id" = \?/, 'single key WHERE (column quoted)';

    $db->sql('DELETE FROM test');
    $db->_make_where({ id => 1, status => 5 });
    like $db->sql, qr/WHERE .+ AND .+/, 'compound WHERE has AND';
    unlike $db->sql, qr/AND\s*$/, 'no trailing AND';

    # Verify both placeholders present
    my $placeholders = () = $db->sql =~ /\?/g;
    is $placeholders, 2, 'compound WHERE has 2 placeholders';
    is scalar @{$db->p}, 2, 'compound WHERE has 2 bind values';

    done_testing;
};

subtest 'TableName validator' => sub {
    is $db->TableName('TableName'), 'TableName', 'valid table name';
    throws_ok { $db->TableName('bad table') } qr/TableName not valid/,
        'invalid table name throws (format check, no tn needed)';

    done_testing;
};

# --- merged from the former t/dbix-fast.t (its unique coverage) ------------

subtest 'execute() + make_sen() with named params' => sub {
    my $res = $db->execute('SELECT :a AS a, :b AS b', { a => 10, b => 20 }, 'arrayref');
    is(ref $res, 'ARRAY', 'execute returns arrayref');
    is($res->[0]{a}, 10, 'param :a bound');
    is($res->[0]{b}, 20, 'param :b bound');

    my $res2 = $db->execute('SELECT :x + :x AS s', { x => 2 }, 'arrayref');
    is($res2->[0]{s}, 4, 'multiple occurrences replaced');

    done_testing;
};

subtest 'now() format' => sub {
    like $db->now, qr/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
        'now() is a datetime string';
    done_testing;
};

subtest 'dsn: sqlite URI style' => sub {
    my $uri_dbfile = File::Spec->catfile($tmpdir, 'uri.sqlite');
    open my $fh, '>', $uri_dbfile or die "Cannot touch $uri_dbfile: $!";
    close $fh;
    my $db2 = DBIx::Fast->new( dsn => "sqlite:///$uri_dbfile" );
    isa_ok($db2, 'DBIx::Fast', 'object created from sqlite URI');
    $db2->exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)');
    $db2->insert('t', { id => 1, v => 'ok' });
    is( $db2->val('SELECT v FROM t WHERE id=1'), 'ok', 'sqlite URI DSN works' );
    done_testing;
};

done_testing();
