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

eval "use DBD::SQLite 1.74";
plan skip_all => "DBD::SQLite 1.74 required" if $@;
eval { require LRU::Cache; 1 }
  or plan skip_all => 'LRU::Cache required for the default cache backend';

my $db = DBIx::Fast->new(
    dsn        => 'dbi:SQLite:dbname=:memory:',
    driver     => 'SQLite',
    RaiseError => 1,
    PrintError => 0,
    cache      => { max_size => 100, default_ttl => 60 },
);

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

$db->insert('products', { sku => 'A', name => 'Apple',   stock => 10 });
$db->insert('products', { sku => 'B', name => 'Bread',   stock => 20 });
$db->insert('products', { sku => 'C', name => 'Cheese',  stock => 30 });

# --- hash: hit + miss ------------------------------------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;

    my $r1 = $db->cached(ttl => 60)->hash('SELECT * FROM products WHERE sku = ?', 'A');
    my $r2 = $db->cached(ttl => 60)->hash('SELECT * FROM products WHERE sku = ?', 'A');

    is_deeply $r1, $r2, 'hash cached: same result on hit';
    is $r1->{name}, 'Apple', 'hash returns correct row';

    my $s = $db->cache->stats;
    is $s->{misses}, 1, 'first call was miss';
    is $s->{hits},   1, 'second call was hit';
}

# --- val: round-trip via cache --------------------------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;
    my $n1 = $db->cached->val('SELECT stock FROM products WHERE sku = ?', 'B');
    my $n2 = $db->cached->val('SELECT stock FROM products WHERE sku = ?', 'B');
    is $n1, 20, 'val correct';
    is $n2, 20, 'val cached';
    is $db->cache->stats->{hits}, 1, 'second val was hit';
}

# --- all: arrayref of rows -------------------------------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;
    my $a1 = $db->cached->all('SELECT id, name FROM products ORDER BY id');
    my $a2 = $db->cached->all('SELECT id, name FROM products ORDER BY id');
    is_deeply $a1, $a2, 'all cached';
    is scalar @$a1, 3, 'all returns 3 rows';
    is $db->cache->stats->{hits}, 1, 'second all was hit';
}

# --- flat: list context ----------------------------------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;
    my @f1 = $db->cached->flat('SELECT name FROM products ORDER BY id');
    my @f2 = $db->cached->flat('SELECT name FROM products ORDER BY id');
    is_deeply \@f1, \@f2, 'flat cached';
    is_deeply \@f1, [qw(Apple Bread Cheese)], 'flat values correct';
    is $db->cache->stats->{hits}, 1, 'second flat was hit';
}

# --- different params -> different cache entries --------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;
    my $a = $db->cached->hash('SELECT * FROM products WHERE sku = ?', 'A');
    my $b = $db->cached->hash('SELECT * FROM products WHERE sku = ?', 'B');
    isnt $a->{name}, $b->{name}, 'different params -> different results';
    is $db->cache->stats->{misses}, 2, 'both were misses';
    is $db->cache->stats->{hits},   0, 'no hits across distinct keys';
}

# --- tag invalidation drops cached queries --------------------------------
{
    $db->cache->clear;
    $db->cache->reset_stats;

    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'C');
    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'C');
    is $db->cache->stats->{hits}, 1, 'tagged query cached';

    $db->cache->invalidate('products');

    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'C');
    is $db->cache->stats->{misses}, 2, 'invalidated -> next call is miss';
}

# --- auto-invalidation on writes (tag = table name convention) ----------
{
    $db->cache->clear;
    $db->cache->reset_stats;

    # Cache a read tagged with table name
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products ORDER BY id');
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products ORDER BY id');
    is $db->cache->stats->{hits}, 1, 'tagged read cached';

    # Insert -> auto-invalidates 'products'
    $db->insert('products', { sku => 'D', name => 'Donut', stock => 5 });

    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products ORDER BY id');
    is $db->cache->stats->{misses}, 2, 'insert invalidated tagged reads';
}

{
    $db->cache->clear;
    $db->cache->reset_stats;
    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'A');
    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'A');
    is $db->cache->stats->{hits}, 1, 'read cached before update';

    $db->up('products', { stock => 99 }, { sku => 'A' });

    $db->cached(ttl => 60, tag => 'products')
       ->hash('SELECT * FROM products WHERE sku = ?', 'A');
    is $db->cache->stats->{misses}, 2, 'update invalidated';
}

{
    $db->cache->clear;
    $db->cache->reset_stats;
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{hits}, 1, 'read cached before delete';

    $db->delete('products', { sku => 'D' });

    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{misses}, 2, 'delete invalidated';
}

{
    $db->cache->clear;
    $db->cache->reset_stats;
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{misses}, 1, 'read miss';

    $db->upsert('products', { sku => 'E', name => 'Egg', stock => 12 }, ['sku']);

    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{misses}, 2, 'upsert invalidated';
}

# A write to a different table must NOT invalidate.
{
    $db->exec('CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT)');
    $db->cache->clear;
    $db->cache->reset_stats;
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{hits}, 1, 'cached';

    $db->insert('customers', { name => 'Alice' });

    $db->cached(ttl => 60, tag => 'products')
       ->all('SELECT * FROM products');
    is $db->cache->stats->{hits}, 2, 'unrelated table write does not invalidate';
}

# --- cache disabled: proxy passes through ----------------------------------
{
    my $db_nocache = DBIx::Fast->new(
        dsn => 'dbi:SQLite:dbname=:memory:', driver => 'SQLite',
        RaiseError => 1, PrintError => 0,
    );
    $db_nocache->exec('CREATE TABLE t (id INT, name TEXT)');
    $db_nocache->insert('t', { id => 1, name => 'foo' });

    my $r = $db_nocache->cached(ttl => 60)
                       ->hash('SELECT * FROM t WHERE id = ?', 1);
    is $r->{name}, 'foo', 'cached() works without cache: passthrough';
    is $db_nocache->cache, undef, 'no cache instance created';
}

done_testing;
