package crowdsec

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestHubCacheStoreLoadAndExpire(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Minute)
	require.NoError(t, err)

	ctx := context.Background()
	meta, err := cache.Store(ctx, "crowdsecurity/demo", "etag1", "hub", "preview-text", []byte("archive-bytes"))
	require.NoError(t, err)
	require.NotEmpty(t, meta.CacheKey)

	loaded, err := cache.Load(ctx, "crowdsecurity/demo")
	require.NoError(t, err)
	require.Equal(t, meta.CacheKey, loaded.CacheKey)
	require.Equal(t, "etag1", loaded.Etag)

	cache.nowFn = func() time.Time { return meta.RetrievedAt.Add(2 * time.Minute) }
	_, err = cache.Load(ctx, "crowdsecurity/demo")
	require.ErrorIs(t, err, ErrCacheExpired)
}

func TestHubCacheRejectsBadSlug(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Hour)
	require.NoError(t, err)

	_, err = cache.Store(context.Background(), "../bad", "etag", "hub", "preview", []byte("data"))
	require.Error(t, err)

	_, err = cache.Store(context.Background(), "..\\bad", "etag", "hub", "preview", []byte("data"))
	require.Error(t, err)
}

func TestHubCacheListAndEvict(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Hour)
	require.NoError(t, err)

	ctx := context.Background()
	_, err = cache.Store(ctx, "crowdsecurity/demo", "etag1", "hub", "preview", []byte("data1"))
	require.NoError(t, err)
	_, err = cache.Store(ctx, "crowdsecurity/other", "etag2", "hub", "preview", []byte("data2"))
	require.NoError(t, err)

	entries, err := cache.List(ctx)
	require.NoError(t, err)
	require.Len(t, entries, 2)

	require.NoError(t, cache.Evict(ctx, "crowdsecurity/demo"))
	entries, err = cache.List(ctx)
	require.NoError(t, err)
	require.Len(t, entries, 1)
	require.Equal(t, "crowdsecurity/other", entries[0].Slug)
}

func TestHubCacheTouchUpdatesTTL(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Minute)
	require.NoError(t, err)

	ctx := context.Background()
	meta, err := cache.Store(ctx, "crowdsecurity/demo", "etag1", "hub", "preview", []byte("data1"))
	require.NoError(t, err)

	cache.nowFn = func() time.Time { return meta.RetrievedAt.Add(30 * time.Second) }
	require.NoError(t, cache.Touch(ctx, "crowdsecurity/demo"))

	cache.nowFn = func() time.Time { return meta.RetrievedAt.Add(2 * time.Minute) }
	_, err = cache.Load(ctx, "crowdsecurity/demo")
	require.ErrorIs(t, err, ErrCacheExpired)
}

func TestHubCachePreviewExistsAndSize(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Hour)
	require.NoError(t, err)

	ctx := context.Background()
	archive := []byte("archive-bytes-here")
	_, err = cache.Store(ctx, "crowdsecurity/demo", "etag123", "hub", "preview-content", archive)
	require.NoError(t, err)

	preview, err := cache.LoadPreview(ctx, "crowdsecurity/demo")
	require.NoError(t, err)
	require.Equal(t, "preview-content", preview)
	require.True(t, cache.Exists(ctx, "crowdsecurity/demo"))
	require.GreaterOrEqual(t, cache.Size(ctx), int64(len(archive)))
}

func TestHubCacheExistsHonorsTTL(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Second)
	require.NoError(t, err)

	ctx := context.Background()
	meta, err := cache.Store(ctx, "crowdsecurity/demo", "etag123", "hub", "preview", []byte("data"))
	require.NoError(t, err)

	cache.nowFn = func() time.Time { return meta.RetrievedAt.Add(3 * time.Second) }
	require.False(t, cache.Exists(ctx, "crowdsecurity/demo"))
}

func TestSanitizeSlugCases(t *testing.T) {
	require.Equal(t, "demo/preset", sanitizeSlug(" demo/preset "))
	require.Equal(t, "", sanitizeSlug("../traverse"))
	require.Equal(t, "", sanitizeSlug("/abs/path"))
	require.Equal(t, "", sanitizeSlug("\\windows\\bad"))
	require.Equal(t, "", sanitizeSlug("bad spaces %"))
}

func TestNewHubCacheRequiresBaseDir(t *testing.T) {
	_, err := NewHubCache("", time.Hour)
	require.Error(t, err)
}

func TestHubCacheTouchMissing(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)

	err = cache.Touch(context.Background(), "missing")
	require.ErrorIs(t, err, ErrCacheMiss)
}

func TestHubCacheTouchInvalidSlug(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)

	err = cache.Touch(context.Background(), "../bad")
	require.Error(t, err)
}

func TestHubCacheStoreContextCanceled(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err = cache.Store(ctx, "demo", "etag", "hub", "preview", []byte("data"))
	require.ErrorIs(t, err, context.Canceled)
}

func TestHubCacheLoadInvalidSlug(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)

	_, err = cache.Load(context.Background(), "../bad")
	require.Error(t, err)
}

func TestHubCacheExistsContextCanceled(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	require.False(t, cache.Exists(ctx, "demo"))
}

func TestHubCacheListSkipsExpired(t *testing.T) {
	cacheDir := t.TempDir()
	cache, err := NewHubCache(cacheDir, time.Second)
	require.NoError(t, err)
	ctx := context.Background()
	fixed := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	cache.nowFn = func() time.Time { return fixed }
	_, err = cache.Store(ctx, "crowdsecurity/demo", "etag", "hub", "preview", []byte("data"))
	require.NoError(t, err)

	cache.nowFn = func() time.Time { return fixed.Add(3 * time.Second) }
	entries, err := cache.List(ctx)
	require.NoError(t, err)
	require.Len(t, entries, 0)
}

func TestHubCacheEvictInvalidSlug(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)
	err = cache.Evict(context.Background(), "../bad")
	require.Error(t, err)
}

func TestHubCacheListContextCanceled(t *testing.T) {
	cache, err := NewHubCache(t.TempDir(), time.Hour)
	require.NoError(t, err)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = cache.List(ctx)
	require.ErrorIs(t, err, context.Canceled)
}

// ============================================
// TTL Tests
// ============================================

func TestHubCacheTTL(t *testing.T) {
	t.Run("returns configured TTL", func(t *testing.T) {
		cache, err := NewHubCache(t.TempDir(), 2*time.Hour)
		require.NoError(t, err)
		require.Equal(t, 2*time.Hour, cache.TTL())
	})

	t.Run("returns minute TTL", func(t *testing.T) {
		cache, err := NewHubCache(t.TempDir(), time.Minute)
		require.NoError(t, err)
		require.Equal(t, time.Minute, cache.TTL())
	})

	t.Run("returns zero TTL if configured", func(t *testing.T) {
		cache, err := NewHubCache(t.TempDir(), 0)
		require.NoError(t, err)
		require.Equal(t, time.Duration(0), cache.TTL())
	})
}
