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 txn_test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER) });

subtest 'accessors - level, savepoints, start_time, stats' => sub {
    my $tx = $db->transaction;

    is($tx->level, 0, 'level starts at 0');
    is_deeply($tx->savepoints, {}, 'savepoints starts empty');
    is($tx->start_time, undef, 'start_time initially undef');

    my $stats = $tx->stats;
    ok($stats, 'stats accessor works');
    is(ref $stats, 'HASH', 'stats is a hashref');
    ok(exists $stats->{total_count}, 'stats has total_count');
    ok(exists $stats->{deadlock_count}, 'stats has deadlock_count');
};

subtest 'do() without code block throws' => sub {
    throws_ok { $db->transaction->do("not a code ref") }
        qr/Transaction code block required/,
        'Non-code ref throws';
    throws_ok { $db->transaction->do(undef) }
        qr/Transaction code block required/,
        'undef throws';
};

subtest 'custom options - max_retries and retry_delay' => sub {
    $db->transaction->reset_stats;

    lives_ok {
        $db->transaction->do(
            sub { $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'custom', 1) },
            { max_retries => 1, retry_delay => 0 }
        );
    } 'Transaction with custom options works';

    is($db->val('SELECT value FROM txn_test WHERE name = ?', 'custom'), 1, 'Record inserted');
};

subtest 'savepoint - invalid name' => sub {
    $db->transaction->do(sub {
        throws_ok {
            $db->transaction->savepoint('invalid name!')
        } qr/Invalid savepoint name/,
          'Invalid savepoint name throws';

        throws_ok {
            $db->transaction->savepoint('')
        } qr/Savepoint name required/,
          'Empty savepoint name throws';

        throws_ok {
            $db->transaction->savepoint(undef)
        } qr/Savepoint name required/,
          'Undef savepoint name throws';
    });
};

subtest 'rollback_to - invalid name' => sub {
    $db->transaction->do(sub {
        throws_ok {
            $db->transaction->rollback_to('invalid name!')
        } qr/Invalid savepoint name/,
          'Invalid savepoint name in rollback_to throws';

        throws_ok {
            $db->transaction->rollback_to('')
        } qr/Savepoint name required/,
          'Empty name in rollback_to throws';

        throws_ok {
            $db->transaction->rollback_to(undef)
        } qr/Savepoint name required/,
          'Undef name in rollback_to throws';
    });
};

subtest 'rollback_to - non-existent savepoint' => sub {
    my $result;
    $db->transaction->do(sub {
        $db->transaction->savepoint('sp_exists');
        $result = $db->transaction->rollback_to('sp_nonexistent');
    });
    ok(!$result, 'rollback_to non-existent savepoint returns falsy');
};

subtest 'nested transaction - inner die, outer catches' => sub {
    $db->transaction->reset_stats;

    eval {
        $db->transaction->do(sub {
            $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'nest_outer', 10);

            eval {
                $db->transaction->do(sub {
                    $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'nest_inner', 20);
                    die "inner failure";
                });
            };
            # Inner failed, but outer continues
            die $@ if $@;
        });
    };

    like($@, qr/inner failure/, 'Inner failure propagated');
    is($db->val('SELECT COUNT(*) FROM txn_test WHERE name = ?', 'nest_outer'), 0, 'Outer rolled back');
    is($db->val('SELECT COUNT(*) FROM txn_test WHERE name = ?', 'nest_inner'), 0, 'Inner rolled back');

    my $stats = $db->transaction->get_stats;
    ok($stats->{error_count} > 0, 'Error count incremented');
};

subtest 'stats after success and failure' => sub {
    $db->transaction->reset_stats;

    # Success
    $db->transaction->do(sub {
        $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'stat_ok', 100);
    });

    # Failure
    eval {
        $db->transaction->do(sub {
            die "forced failure";
        });
    };

    my $stats = $db->transaction->get_stats;
    is($stats->{total_count}, 2, 'Total count = 2');
    is($stats->{success_count}, 1, 'Success count = 1');
    is($stats->{error_count}, 1, 'Error count = 1');
    ok($stats->{total_duration} > 0, 'Duration recorded');
    ok($stats->{avg_duration} > 0, 'Avg duration calculated');
    ok(defined $stats->{max_duration}, 'Max duration set');
};

subtest 'savepoint with higher level cleanup on rollback_to' => sub {
    $db->transaction->do(sub {
        $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'sp_base', 1);
        $db->transaction->savepoint('sp1');
        $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'sp_after1', 2);
        $db->transaction->savepoint('sp2');
        $db->exec('INSERT INTO txn_test (name, value) VALUES (?, ?)', 'sp_after2', 3);

        # Rollback to sp1 should clean up sp2
        $db->transaction->rollback_to('sp1');

        # sp2 data should be gone
    });

    is($db->val('SELECT COUNT(*) FROM txn_test WHERE name = ?', 'sp_base'), 1, 'Base insert persisted');
    is($db->val('SELECT COUNT(*) FROM txn_test WHERE name = ?', 'sp_after1'), 0, 'After sp1 rolled back');
    is($db->val('SELECT COUNT(*) FROM txn_test WHERE name = ?', 'sp_after2'), 0, 'After sp2 rolled back');
};

subtest 'start_time set during transaction' => sub {
    my $captured_time;
    $db->transaction->do(sub {
        $captured_time = $db->transaction->start_time;
    });
    ok(defined $captured_time, 'start_time set during transaction');
    ok($captured_time > 0, 'start_time is a positive number');
};

done_testing;
