package redis

import (
	"context"
	"errors"
	"os"
	"testing"
	"time"

	"github.com/redis/go-redis/v9"
)

func testClient(t *testing.T) *redis.Client {
	t.Helper()
	addr := os.Getenv("REDIS_ADDR")
	if addr == "" {
		t.Skip("REDIS_ADDR is not set")
	}
	url := addr
	if addr[:7] != "redis://" {
		url = "redis://" + addr
	}
	c := New(url)
	if c == nil {
		t.Fatal("failed to create redis client")
	}
	if err := Ping(context.Background(), c); err != nil {
		t.Fatal(err)
	}
	return c
}

func TestIdempotency_AcquireCompleteReplay(t *testing.T) {
	c := testClient(t)
	ctx := context.Background()
	key := IdemKey(1, "POST /pos", "test-key-1")
	if err := c.Del(ctx, key).Err(); err != nil {
		t.Fatal(err)
	}
	defer c.Del(ctx, key)

	acquired, rep, err := Claim(ctx, c, key, "fp-a", time.Minute, 30*time.Second)
	if err != nil || !acquired || rep != nil {
		t.Fatalf("claim = %v/%v/%v, want acquired", acquired, rep, err)
	}
	if err := Complete(ctx, c, key, "fp-a", time.Minute, 201, []byte(`{"ok":true}`), "application/json"); err != nil {
		t.Fatal(err)
	}
	// replay with the same fingerprint
	acquired, rep, err = Claim(ctx, c, key, "fp-a", time.Minute, 30*time.Second)
	if err != nil || acquired || rep == nil {
		t.Fatalf("replay claim = %v/%v/%v", acquired, rep, err)
	}
	if rep.Status != 201 || string(rep.Body) != `{"ok":true}` {
		t.Errorf("replay = %+v", rep)
	}
	// same key, different payload -> conflict
	_, _, err = Claim(ctx, c, key, "fp-b", time.Minute, 30*time.Second)
	if !errors.Is(err, ErrIdempotencyConflict) {
		t.Errorf("conflict err = %v, want ErrIdempotencyConflict", err)
	}
}

func TestIdempotency_ConcurrentClaimOnce(t *testing.T) {
	c := testClient(t)
	ctx := context.Background()
	key := IdemKey(2, "POST /invoices/1/pay", "test-key-2")
	if err := c.Del(ctx, key).Err(); err != nil {
		t.Fatal(err)
	}
	defer c.Del(ctx, key)

	acquired, _, err := Claim(ctx, c, key, "fp", time.Minute, 30*time.Second)
	if err != nil || !acquired {
		t.Fatalf("first claim = %v/%v", acquired, err)
	}
	acquired2, _, err := Claim(ctx, c, key, "fp", time.Minute, 30*time.Second)
	if err != nil || acquired2 {
		t.Fatalf("second claim = %v/%v, want pending (not acquired)", acquired2, err)
	}
}

func TestIdempotency_Expiry(t *testing.T) {
	c := testClient(t)
	ctx := context.Background()
	key := IdemKey(3, "POST /sales-orders", "test-key-3")
	if err := c.Del(ctx, key).Err(); err != nil {
		t.Fatal(err)
	}
	defer c.Del(ctx, key)

	acquired, _, err := Claim(ctx, c, key, "fp", time.Second, 30*time.Second)
	if err != nil || !acquired {
		t.Fatalf("claim = %v/%v", acquired, err)
	}
	if err := Complete(ctx, c, key, "fp", time.Second, 200, []byte(`{}`), "application/json"); err != nil {
		t.Fatal(err)
	}
	time.Sleep(1200 * time.Millisecond)
	rep, err := Replay(ctx, c, key, "fp")
	if err != nil {
		t.Fatal(err)
	}
	if rep != nil {
		t.Fatalf("replay after TTL = %+v, want nil", rep)
	}
	// expired slot is reclaimable
	acquired, _, err = Claim(ctx, c, key, "fp", time.Minute, 30*time.Second)
	if err != nil || !acquired {
		t.Fatalf("reclaim after expiry = %v/%v", acquired, err)
	}
}
