use strict;
use warnings;
use Test::More;
use Time::HiRes ();
use File::Temp ();
use File::Spec ();

use Data::HashMap::Shared::II;
use Data::HashMap::Shared::IS;
use Data::HashMap::Shared::SI;
use Data::HashMap::Shared::SS;

sub tmpfile { File::Temp::tempnam(File::Spec->tmpdir, 'shm_newops') . '.shm' }

# update_ttl: int-value variant
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100, 0, 30);
    ok(!$map->update_ttl(1, 99, 60), 'II update_ttl: fails on missing key');
    $map->put(1, 100);
    ok($map->update_ttl(1, 200, 90), 'II update_ttl: succeeds on existing');
    is($map->get(1), 200, 'II update_ttl: value changed');
    my $rem = shm_ii_ttl_remaining $map, 1;
    ok($rem > 30 && $rem <= 90, "II update_ttl: TTL applied (rem=$rem)");
    # ttl=0 → permanent
    ok($map->update_ttl(1, 300, 0), 'II update_ttl: succeeds with ttl=0');
    is(scalar(shm_ii_ttl_remaining $map, 1), 0, 'II update_ttl: ttl=0 → permanent');
    unlink $path;
}

# update_ttl: string-value variant
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::SS->new($path, 100, 0, 30);
    ok(!$map->update_ttl("k", "v", 60), 'SS update_ttl: fails on missing');
    $map->put("k", "v0");
    ok($map->update_ttl("k", "v1", 90), 'SS update_ttl: succeeds');
    is($map->get("k"), "v1", 'SS update_ttl: value changed');
    my $rem = shm_ss_ttl_remaining $map, "k";
    ok($rem > 30 && $rem <= 90, "SS update_ttl: TTL applied (rem=$rem)");
    unlink $path;
}

# update_ttl: croak on non-TTL map
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);
    eval { $map->update_ttl(1, 1, 60) };
    like($@, qr/TTL-enabled/, 'update_ttl croaks on non-TTL map');
    unlink $path;
}

# cas_take: int-value
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);
    $map->put(1, 100);
    is($map->cas_take(1, 99), undef, 'II cas_take: mismatch returns undef');
    is($map->get(1), 100, 'II cas_take: mismatch leaves value');
    is($map->cas_take(1, 100), 100, 'II cas_take: match returns removed value');
    ok(!$map->exists(1), 'II cas_take: key removed on match');
    is($map->cas_take(999, 0), undef, 'II cas_take: missing key returns undef');
    unlink $path;
}

# cas_take: string-value (byte-only compare; UTF-8 flag ignored on expected)
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::SS->new($path, 100);
    $map->put("k", "v1");
    is($map->cas_take("k", "wrong"), undef, 'SS cas_take: mismatch returns undef');
    is($map->cas_take("k", "v1"), "v1", 'SS cas_take: match returns value');
    ok(!$map->exists("k"), 'SS cas_take: key removed');
    # UTF-8 toggle
    $map->put("u", "abc");
    my $up = "abc"; utf8::upgrade($up);
    is($map->cas_take("u", $up), "abc", 'SS cas_take: utf8-upgraded expected matches ASCII');
    unlink $path;
}

# cas_take: keyword form for int-value
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);
    $map->put(7, 42);
    my $got = shm_ii_cas_take $map, 7, 42;
    is($got, 42, 'II cas_take kw: returns removed value');
    ok(!$map->exists(7), 'II cas_take kw: key removed');
    unlink $path;
}

# remove_multi
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);
    $map->put($_, $_ * 10) for 1..5;
    is($map->size, 5, 'remove_multi setup: 5 keys');
    is($map->remove_multi(1, 3, 99), 2, 'II remove_multi: 2 removed, 1 missing');
    is($map->size, 3, 'II remove_multi: size updated');
    ok(!$map->exists(1), 'II remove_multi: key 1 gone');
    ok($map->exists(2), 'II remove_multi: key 2 retained');
    ok(!$map->exists(3), 'II remove_multi: key 3 gone');
    # Empty call
    is($map->remove_multi(), 0, 'II remove_multi: empty list returns 0');
    unlink $path;
}

# remove_multi: SS (string keys)
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::SS->new($path, 100);
    $map->put($_, "v$_") for qw(a b c d);
    is($map->remove_multi("a", "c", "missing"), 2, 'SS remove_multi: 2 removed');
    is($map->size, 2, 'SS remove_multi: size 2');
    ok($map->exists("b"), 'SS remove_multi: b retained');
    ok($map->exists("d"), 'SS remove_multi: d retained');
    unlink $path;
}

# get_with_ttl: TTL-less map
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);
    $map->put(1, 100);
    my @r = $map->get_with_ttl(1);
    is_deeply(\@r, [100, undef], 'II get_with_ttl no-TTL: (value, undef)');
    my @miss = $map->get_with_ttl(99);
    is_deeply(\@miss, [], 'II get_with_ttl: empty list on missing');
    unlink $path;
}

# get_with_ttl: TTL map, permanent + per-key TTL
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::IS->new($path, 100, 0, 30);
    $map->put(1, "hello");
    my ($v, $t) = $map->get_with_ttl(1);
    is($v, "hello", 'IS get_with_ttl: value');
    ok($t > 0 && $t <= 30, "IS get_with_ttl: TTL within default (t=$t)");

    $map->persist(1);
    my ($v2, $t2) = $map->get_with_ttl(1);
    is($v2, "hello", 'IS get_with_ttl: value after persist');
    is($t2, 0, 'IS get_with_ttl: 0 = permanent');

    $map->put_ttl(2, "world", 90);
    my ($v3, $t3) = $map->get_with_ttl(2);
    is($v3, "world", 'IS get_with_ttl: value with per-key TTL');
    ok($t3 > 30 && $t3 <= 90, "IS get_with_ttl: per-key TTL (t=$t3)");
    unlink $path;
}

# get_with_ttl: expired entry returns empty list
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100, 0, 30);
    $map->put_ttl(1, 100, 1);
    Time::HiRes::sleep(1.2);
    my @r = $map->get_with_ttl(1);
    is_deeply(\@r, [], 'get_with_ttl: empty on expired');
    unlink $path;
}

# get_with_ttl: SI string-key
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::SI->new($path, 100, 0, 60);
    $map->put("k", 42);
    my ($v, $t) = $map->get_with_ttl("k");
    is($v, 42, 'SI get_with_ttl: value');
    ok($t > 0 && $t <= 60, "SI get_with_ttl: TTL (t=$t)");
    unlink $path;
}

# get_with_ttl on a non-TTL map: value present, TTL undef
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 100);   # no TTL
    $map->put(5, 99);
    my ($v, $t) = $map->get_with_ttl(5);
    is($v, 99, 'get_with_ttl on non-TTL map: returns value');
    ok(!defined $t, 'get_with_ttl on non-TTL map: TTL is undef');
    unlink $path;
}

# get_or_set returns undef for a new key when a non-LRU map is full
{
    my $path = tmpfile();
    my $map = Data::HashMap::Shared::II->new($path, 4);   # tiny, LRU off
    my $full = 0;
    for my $k (1 .. 1000) {
        defined($map->get_or_set($k, $k)) or do { $full = $k; last };
    }
    ok($full, "get_or_set returns undef for a new key on a full non-LRU map (failed at key $full, size @{[$map->size]})");
    # an existing key still returns its stored value (not undef) when full
    is($map->get_or_set(1, -1), 1, 'get_or_set on existing key returns stored value even when full');
    unlink $path;
}

# new_sharded requires a real path_prefix (anonymous sharded maps unsupported)
{
    my $ok = eval { Data::HashMap::Shared::II->new_sharded(undef, 2, 100); 1 };
    ok(!$ok, 'new_sharded(undef, ...) dies');
    like($@, qr/path_prefix/, '  ...with a path_prefix diagnostic');
}

# incr/incr_by croak when a NEW key cannot be inserted (map at capacity) -- the
# one op class that dies rather than returning false. Existing keys still work.
{
    my $path = tmpfile();
    my $m = Data::HashMap::Shared::II->new($path, 2);   # tiny, no LRU
    my $n = 0;
    $n++ while $n < 10_000 && $m->put($n, $n);          # fill to capacity
    ok($n > 0 && $n < 10_000, "map filled to capacity at $n entries");
    is($m->incr(0), 1, 'incr on an existing key works at capacity (no insert needed)');
    ok(!eval { $m->incr(999_999); 1 }, 'incr on a new key croaks when the map is full');
    like($@, qr/increment failed/, '  ...naming the failed operation');
    ok(!eval { $m->incr_by(888_888, 5); 1 }, 'incr_by on a new key croaks when full');
    like($@, qr/incr_by failed/, '  ...naming the failed operation');
    unlink $path;
}

# get_with_ttl on an EXPIRED key returns empty list (like a missing key) --
# distinct from a never-existed key
{
    my $path = tmpfile();
    my $m = Data::HashMap::Shared::II->new($path, 100, 0, 1);   # ttl = 1s
    $m->put(1, 100);
    my @live = $m->get_with_ttl(1);
    is($live[0], 100, 'get_with_ttl: value present before expiry');
    Time::HiRes::sleep(1.2);                                                    # key 1 expires
    my @expired = $m->get_with_ttl(1);
    is_deeply(\@expired, [], 'get_with_ttl on an expired key returns empty list');
    unlink $path;
}

# set_multi on a non-LRU map that fills mid-batch returns the partial success count
{
    my $path = tmpfile();
    my $m = Data::HashMap::Shared::II->new($path, 2);           # ~3 usable slots
    my @pairs = map { ($_ => $_ * 10) } 1 .. 20;               # far over capacity
    my $count = $m->set_multi(@pairs);
    cmp_ok($count, '>', 0,  'set_multi: some pairs stored before the map fills');
    cmp_ok($count, '<', 20, 'set_multi: returns the partial count when the map fills mid-batch');
    is($m->size, $count, '  ...and the count matches the live size');
    unlink $path;
}

done_testing;
