use strict;
use warnings;
use Test::More;
use Test::Exception;
use Time::HiRes qw(sleep);
use DBIx::Fast::Cache;

eval { require LRU::Cache; 1 }
  or plan skip_all => 'LRU::Cache required for the default cache backend';

# --- Basic set/get roundtrip ----------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 100, default_ttl => 0);
    $c->set('k1', 'v1');
    is $c->get('k1'), 'v1', 'set/get string';

    my $row = { id => 1, name => 'Alice' };
    $c->set('row:1', $row);
    is_deeply $c->get('row:1'), $row, 'set/get hashref roundtrip';

    is $c->get('nope'), undef, 'get miss returns undef';
}

# --- Stats counters --------------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);
    $c->set('a', 1);
    $c->get('a');
    $c->get('a');
    $c->get('miss');

    my $s = $c->stats;
    is $s->{sets},   1,   'stats: sets';
    is $s->{hits},   2,   'stats: hits';
    is $s->{misses}, 1,   'stats: misses';
    is $s->{total},  3,   'stats: total = hits + misses';
    cmp_ok $s->{hit_ratio}, '>', 0.66, 'stats: hit_ratio';
    cmp_ok $s->{hit_ratio}, '<', 0.67, 'stats: hit_ratio (upper)';

    $c->reset_stats;
    my $s2 = $c->stats;
    is $s2->{hits}, 0, 'reset_stats clears counters';
    is $s2->{sets}, 0, 'reset_stats clears sets too';
}

# --- TTL expiry ------------------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 1);
    $c->set('short', 'val', ttl => 0.1);   # 100ms

    is $c->get('short'), 'val', 'fresh entry is hit';
    sleep 0.15;
    is $c->get('short'), undef, 'expired entry is miss';

    my $s = $c->stats;
    is $s->{expirations}, 1, 'stats: expiration counted';
    is $s->{misses},      1, 'stats: expired = miss';
}

# --- TTL = 0 means no expiry ----------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);
    $c->set('forever', 'v', ttl => 0);
    sleep 0.05;
    is $c->get('forever'), 'v', 'ttl=0 disables expiry';
}

# --- Tag-based invalidation ------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 100, default_ttl => 0);
    $c->set('p:1', 'prod1', tags => ['products']);
    $c->set('p:2', 'prod2', tags => ['products']);
    $c->set('u:1', 'user1', tags => ['users']);

    is $c->get('p:1'), 'prod1', 'tagged entry stored';
    is $c->get('u:1'), 'user1', 'second tag stored';

    my $n = $c->invalidate('products');
    is $n, 2, 'invalidate returns count';

    is $c->get('p:1'), undef, 'invalidated entry is gone';
    is $c->get('p:2'), undef, 'second invalidated entry is gone';
    is $c->get('u:1'), 'user1', 'other tag is intact';

    is $c->invalidate('nonexistent'), 0, 'invalidate unknown tag returns 0';
}

# --- Multi-tag key: invalidating any tag drops the key --------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 100, default_ttl => 0);
    $c->set('k', 'v', tags => ['t1', 't2']);

    $c->invalidate('t1');
    is $c->get('k'), undef, 'multi-tag key dropped by any tag';
}

# --- Delete + clear --------------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);
    $c->set('a', 1);
    $c->set('b', 2);

    $c->delete('a');
    is $c->get('a'), undef, 'delete one key';
    is $c->get('b'), 2,     'other key intact';

    $c->clear;
    is $c->get('b'), undef, 'clear drops everything';
}

# --- LRU eviction at capacity ---------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 3, default_ttl => 0);
    $c->set("k$_", "v$_") for 1 .. 3;

    # Touch k1 so k2 is LRU
    $c->get('k1');
    $c->set('k4', 'v4');

    is $c->get('k1'), 'v1', 'recently-used key survives eviction';
    is $c->get('k4'), 'v4', 'newest key present';
    is $c->get('k2'), undef, 'LRU key was evicted';
}

# --- build_key is stable ---------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);
    my $k1 = $c->build_key('SELECT * FROM t WHERE id = ?', [42]);
    my $k2 = $c->build_key('SELECT * FROM t WHERE id = ?', [42]);
    is $k1, $k2, 'build_key is deterministic';

    my $k3 = $c->build_key('SELECT * FROM t WHERE id = ?', [43]);
    isnt $k1, $k3, 'different params produce different keys';

    my $k4 = $c->build_key('SELECT * FROM t WHERE id = ?', [undef]);
    my $k5 = $c->build_key('SELECT * FROM t WHERE id = ?', ['']);
    isnt $k4, $k5, 'undef and empty string are distinguishable';
}

# --- Validation ------------------------------------------------------------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10);
    throws_ok { $c->set('k', 'v', tags => 'notarray') } qr/tags must be an arrayref/,
        'tags must be arrayref';

    throws_ok { DBIx::Fast::Cache->new(backend => 'INVALID') }
        qr/Unknown cache backend/,
        'unknown backend rejected';
}

# --- CHI backend (opt-in) -------------------------------------------------
SKIP: {
    eval { require CHI; 1 } or skip 'CHI not installed', 5;

    my $chi = CHI->new(driver => 'RawMemory', global => 1);
    my $c = DBIx::Fast::Cache->new(backend => $chi, default_ttl => 0);

    $c->set('k', 'v', tags => ['t1']);
    is $c->get('k'), 'v', 'CHI backend: get/set';

    $c->invalidate('t1');
    is $c->get('k'), undef, 'CHI backend: tag invalidation';

    $c->set('a', 1);
    $c->set('b', 2);
    $c->clear;
    is $c->get('a'), undef, 'CHI backend: clear';

    # CHI uses remove() not delete() - normalization works
    $c->set('x', 'y');
    $c->delete('x');
    is $c->get('x'), undef, 'CHI backend: delete normalized to remove()';

    isa_ok $c->backend, 'CHI::Driver', 'CHI backend reachable via accessor';
}

# --- Tag index sweep (silent LRU evictions must not leak the index) --------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);

    # 100 distinct tagged keys through a 10-slot LRU: 90 get evicted silently.
    # Without the sweep the tag index would keep all 100 key-tag pairs.
    $c->set("k$_", $_, tags => ["tag$_"]) for 1..100;

    my $s = $c->stats;
    cmp_ok $s->{index_pairs}, '<', 45,
        'tag index stays bounded despite silent evictions';
    cmp_ok $s->{evictions}, '>', 0, 'sweep detected the backend evictions';
}

# --- Stats accuracy: delete/invalidate count what actually happened --------
{
    my $c = DBIx::Fast::Cache->new(max_size => 10, default_ttl => 0);

    $c->delete('never-set');
    is $c->stats->{invalidations}, 0,
        'delete of a missing key is not an invalidation';

    $c->set('e', 'v', ttl => 0.05, tags => ['exp']);
    sleep 0.1;
    is $c->invalidate('exp'), 0,
        'invalidate returns 0 for an already-expired entry';
    my $s = $c->stats;
    is $s->{invalidations}, 0,   'expired entry not counted as invalidation';
    is $s->{expirations},   1,   'expired entry counted as expiration';
}

# --- CHI-like backends receive the TTL on set() ----------------------------
{
    package Mock::CHI {
        # Claims CHI::Driver identity via an isa() override instead of @ISA,
        # so Perl never tries to resolve methods through a package that is
        # not loaded (that emits "can not locate package CHI::Driver"
        # warnings when CHI is absent). CHI itself is not needed.
        sub isa    { $_[1] eq 'CHI::Driver' || UNIVERSAL::isa(@_) }
        sub new    { bless { store => {}, ttl_args => {} }, shift }
        sub get    { $_[0]{store}{$_[1]} }
        sub set    { $_[0]{ttl_args}{$_[1]} = $_[3]; $_[0]{store}{$_[1]} = $_[2] }
        sub remove { delete $_[0]{store}{$_[1]} }
        sub clear  { $_[0]{store} = {} }
    }

    my $mock = Mock::CHI->new;
    my $c = DBIx::Fast::Cache->new(backend => $mock, default_ttl => 0);

    $c->set('with-ttl', 'v', ttl => 30);
    is $mock->{ttl_args}{'with-ttl'}, 30,
        'TTL is pushed down to a CHI backend set()';

    $c->set('no-ttl', 'v');
    is $mock->{ttl_args}{'no-ttl'}, undef,
        'no TTL (immortal entry) uses the 2-arg backend set()';

    is $c->get('with-ttl'), 'v', 'roundtrip through the CHI-like backend';
}

done_testing;
