package transport

import (
	"context"
	"errors"
	"testing"
	"time"
)

func TestIdempotencyStorePutGet(t *testing.T) {
	s := NewIdempotencyStore()
	if _, ok := s.Get("missing"); ok {
		t.Fatal("expected missing key to return ok=false")
	}
	s.Put("k1", "job-1")
	if jobID, ok := s.Get("k1"); !ok || jobID != "job-1" {
		t.Errorf("Get(k1): got (%q, %v), want (job-1, true)", jobID, ok)
	}
}

func TestIdempotencyStoreExpiry(t *testing.T) {
	s := NewIdempotencyStore()
	// Manually insert an expired entry.
	s.entries["expired"] = dedupeEntry{
		key:       "expired",
		jobID:     "old-job",
		expiresAt: time.Now().Add(-1 * time.Minute),
	}
	if _, ok := s.Get("expired"); ok {
		t.Fatal("expected expired entry to return ok=false")
	}
	if _, exists := s.entries["expired"]; exists {
		t.Error("expected expired entry to be removed by Get")
	}
}

func TestIdempotencyStoreContext(t *testing.T) {
	ctx := WithIdempotencyKey(context.Background(), "key-1")
	if got := IdempotencyKeyFromContext(ctx); got != "key-1" {
		t.Errorf("IdempotencyKeyFromContext: got %q, want key-1", got)
	}
	ctx2 := context.Background()
	if got := IdempotencyKeyFromContext(ctx2); got != "" {
		t.Errorf("IdempotencyKeyFromContext(empty): got %q, want \"\"", got)
	}
}

func TestRetrySucceedsAfterTransient(t *testing.T) {
	calls := 0
	got, err := Do(context.Background(), DefaultRetryPolicy(),
		func(_ context.Context, attempt int) (string, bool, error) {
			calls++
			if attempt < 3 {
				return "", true, errors.New("connection refused: try again")
			}
			return "ok", true, nil
		})
	if err != nil {
		t.Fatalf("Do: %v", err)
	}
	if got != "ok" {
		t.Errorf("Do: got %q, want ok", got)
	}
	if calls != 3 {
		t.Errorf("Do: got %d calls, want 3", calls)
	}
}

func TestRetryNoKeyOnTransient(t *testing.T) {
	// Without an idempotency key AND a non-idempotent verb, a
	// transient error on the first attempt must NOT retry (REQ-037).
	calls := 0
	_, err := Do(context.Background(), DefaultRetryPolicy(),
		func(_ context.Context, _ int) (string, bool, error) {
			calls++
			return "", false, errors.New("connection refused")
		})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if calls != 1 {
		t.Errorf("expected 1 call (no retry without key), got %d", calls)
	}
}

func TestRetryPermanentError(t *testing.T) {
	calls := 0
	_, err := Do(context.Background(), DefaultRetryPolicy(),
		func(_ context.Context, _ int) (string, bool, error) {
			calls++
			return "", true, ErrPermanent
		})
	if !errors.Is(err, ErrPermanent) {
		t.Errorf("expected ErrPermanent, got %v", err)
	}
	if calls != 1 {
		t.Errorf("expected 1 call (permanent = no retry), got %d", calls)
	}
}

func TestRetryContextCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately
	calls := 0
	_, err := Do(ctx, DefaultRetryPolicy(),
		func(_ context.Context, _ int) (string, bool, error) {
			calls++
			return "", true, errors.New("EOF")
		})
	if !errors.Is(err, context.Canceled) {
		t.Errorf("expected context.Canceled, got %v", err)
	}
}

func TestIdempotencyStoreSweep(t *testing.T) {
	s := NewIdempotencyStore()
	s.Put("live-1", "job-1")
	s.entries["expired"] = dedupeEntry{
		key:       "expired",
		jobID:     "old-job",
		expiresAt: time.Now().Add(-1 * time.Minute),
	}
	s.Sweep()
	if _, ok := s.entries["expired"]; ok {
		t.Error("Sweep did not remove expired entry")
	}
	if _, ok := s.entries["live-1"]; !ok {
		t.Error("Sweep removed live entry")
	}
}

func TestIdempotencyStorePutEmpty(t *testing.T) {
	s := NewIdempotencyStore()
	s.Put("", "job-1")
	s.Put("k1", "")
	if _, ok := s.Get("k1"); ok {
		t.Error("Put with empty jobID should not store")
	}
	if _, ok := s.Get(""); ok {
		t.Error("Get with empty key should return false")
	}
}

func TestWithIdempotencyKeyEmpty(t *testing.T) {
	ctx := context.Background()
	got := WithIdempotencyKey(ctx, "")
	if got != ctx {
		t.Error("WithIdempotencyKey with empty key should return ctx unchanged")
	}
}

func TestIsTransient(t *testing.T) {
	cases := []struct {
		err  error
		want bool
	}{
		{nil, false},
		{errors.New("connection refused"), true},
		{errors.New("i/o timeout"), true},
		{errors.New("EOF"), true},
		{errors.New("no such host"), true},
		{errors.New("connection reset by peer"), true},
		{errors.New("invalid spec"), false},
	}
	for _, c := range cases {
		if got := IsTransient(c.err); got != c.want {
			t.Errorf("IsTransient(%v): got %v, want %v", c.err, got, c.want)
		}
	}
}
