package backends

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

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

// TestCacheBackendContract defines a set of tests that all CacheBackend implementations must pass
// This ensures that Memory, Redis, and Hybrid backends all behave consistently
func TestCacheBackendContract(t *testing.T) {
	// Test suite will be run against each backend type
	t.Run("MemoryBackend", func(t *testing.T) {
		backend := setupMemoryBackend(t)
		runContractTests(t, backend)
	})

	t.Run("RedisBackend", func(t *testing.T) {
		backend := setupRedisBackend(t)
		runContractTests(t, backend)
	})

	t.Run("HybridBackend", func(t *testing.T) {
		backend := setupHybridBackend(t)
		runContractTests(t, backend)
	})
}

// runContractTests executes all contract tests against a backend
func runContractTests(t *testing.T, backend CacheBackend) {
	t.Helper()

	ctx := context.Background()

	t.Run("BasicSetGet", func(t *testing.T) {
		testBasicSetGet(t, ctx, backend)
	})

	t.Run("GetNonExistent", func(t *testing.T) {
		testGetNonExistent(t, ctx, backend)
	})

	t.Run("UpdateExisting", func(t *testing.T) {
		testUpdateExisting(t, ctx, backend)
	})

	t.Run("Delete", func(t *testing.T) {
		testDelete(t, ctx, backend)
	})

	t.Run("DeleteNonExistent", func(t *testing.T) {
		testDeleteNonExistent(t, ctx, backend)
	})

	t.Run("Exists", func(t *testing.T) {
		testExists(t, ctx, backend)
	})

	t.Run("TTLExpiration", func(t *testing.T) {
		testTTLExpiration(t, ctx, backend)
	})

	t.Run("Clear", func(t *testing.T) {
		testClear(t, ctx, backend)
	})

	t.Run("Ping", func(t *testing.T) {
		testPing(t, ctx, backend)
	})

	t.Run("Stats", func(t *testing.T) {
		testStats(t, ctx, backend)
	})

	t.Run("ConcurrentAccess", func(t *testing.T) {
		testConcurrentAccess(t, ctx, backend)
	})

	t.Run("LargeValues", func(t *testing.T) {
		testLargeValues(t, ctx, backend)
	})

	t.Run("EmptyValues", func(t *testing.T) {
		testEmptyValues(t, ctx, backend)
	})

	t.Run("SpecialCharactersInKeys", func(t *testing.T) {
		testSpecialCharactersInKeys(t, ctx, backend)
	})
}

// testBasicSetGet verifies basic set and get operations
func testBasicSetGet(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "test-key-1"
	value := []byte("test-value-1")
	ttl := 1 * time.Minute

	// Set value
	err := backend.Set(ctx, key, value, ttl)
	require.NoError(t, err, "Set should not return error")

	// Get value
	retrieved, remainingTTL, exists, err := backend.Get(ctx, key)
	require.NoError(t, err, "Get should not return error")
	assert.True(t, exists, "Key should exist")
	assert.Equal(t, value, retrieved, "Retrieved value should match")
	assert.Greater(t, remainingTTL, 50*time.Second, "TTL should be close to original")
	assert.LessOrEqual(t, remainingTTL, ttl, "TTL should not exceed original")
}

// testGetNonExistent verifies behavior when getting non-existent keys
func testGetNonExistent(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "non-existent-key"

	retrieved, ttl, exists, err := backend.Get(ctx, key)
	require.NoError(t, err, "Get should not return error for non-existent key")
	assert.False(t, exists, "Key should not exist")
	assert.Nil(t, retrieved, "Value should be nil")
	assert.Equal(t, time.Duration(0), ttl, "TTL should be zero")
}

// testUpdateExisting verifies updating an existing key
func testUpdateExisting(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "update-key"
	value1 := []byte("original-value")
	value2 := []byte("updated-value")
	ttl := 1 * time.Minute

	// Set initial value
	err := backend.Set(ctx, key, value1, ttl)
	require.NoError(t, err)

	// Update value
	err = backend.Set(ctx, key, value2, ttl)
	require.NoError(t, err)

	// Verify updated value
	retrieved, _, exists, err := backend.Get(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists)
	assert.Equal(t, value2, retrieved, "Value should be updated")
}

// testDelete verifies delete operation
func testDelete(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "delete-key"
	value := []byte("delete-value")

	// Set value
	err := backend.Set(ctx, key, value, 1*time.Minute)
	require.NoError(t, err)

	// Verify exists
	exists, err := backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists)

	// Delete
	deleted, err := backend.Delete(ctx, key)
	require.NoError(t, err)
	assert.True(t, deleted, "Delete should return true for existing key")

	// Verify deleted
	exists, err = backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.False(t, exists, "Key should not exist after delete")
}

// testDeleteNonExistent verifies deleting non-existent keys
func testDeleteNonExistent(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "non-existent-delete-key"

	deleted, err := backend.Delete(ctx, key)
	require.NoError(t, err)
	assert.False(t, deleted, "Delete should return false for non-existent key")
}

// testExists verifies the Exists operation
func testExists(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "exists-key"
	value := []byte("exists-value")

	// Check non-existent key
	exists, err := backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.False(t, exists, "Key should not exist initially")

	// Set value
	err = backend.Set(ctx, key, value, 1*time.Minute)
	require.NoError(t, err)

	// Check existing key
	exists, err = backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists, "Key should exist after Set")
}

// testTTLExpiration verifies TTL expiration behavior
func testTTLExpiration(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "ttl-key"
	value := []byte("ttl-value")
	shortTTL := 100 * time.Millisecond

	// Set with short TTL
	err := backend.Set(ctx, key, value, shortTTL)
	require.NoError(t, err)

	// Verify exists immediately
	exists, err := backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists, "Key should exist immediately after Set")

	// Wait for expiration
	time.Sleep(200 * time.Millisecond)

	// Verify expired
	exists, err = backend.Exists(ctx, key)
	require.NoError(t, err)
	assert.False(t, exists, "Key should not exist after TTL expiration")
}

// testClear verifies Clear operation
func testClear(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	// Set multiple keys
	for i := 0; i < 5; i++ {
		key := fmt.Sprintf("clear-key-%d", i)
		value := []byte(fmt.Sprintf("clear-value-%d", i))
		err := backend.Set(ctx, key, value, 1*time.Minute)
		require.NoError(t, err)
	}

	// Give async writes time to complete before clearing
	// This prevents race conditions with async write workers
	time.Sleep(50 * time.Millisecond)

	// Clear all
	err := backend.Clear(ctx)
	require.NoError(t, err)

	// Verify all keys are gone
	for i := 0; i < 5; i++ {
		key := fmt.Sprintf("clear-key-%d", i)
		exists, err := backend.Exists(ctx, key)
		require.NoError(t, err)
		assert.False(t, exists, "Key should not exist after Clear")
	}
}

// testPing verifies Ping operation
func testPing(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	err := backend.Ping(ctx)
	assert.NoError(t, err, "Ping should succeed on healthy backend")
}

// testStats verifies GetStats operation
func testStats(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	stats := backend.GetStats()
	assert.NotNil(t, stats, "Stats should not be nil")

	// Stats should contain basic metrics
	_, hasHits := stats["hits"]
	_, hasMisses := stats["misses"]
	assert.True(t, hasHits || hasMisses, "Stats should contain hits or misses")
}

// testConcurrentAccess verifies thread safety
func testConcurrentAccess(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	var wg sync.WaitGroup
	goroutines := 10
	iterations := 20

	// Concurrent writes
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				key := fmt.Sprintf("concurrent-key-%d-%d", id, j)
				value := []byte(fmt.Sprintf("concurrent-value-%d-%d", id, j))
				err := backend.Set(ctx, key, value, 1*time.Minute)
				assert.NoError(t, err)

				// Read back
				retrieved, _, exists, err := backend.Get(ctx, key)
				assert.NoError(t, err)
				if exists {
					assert.Equal(t, value, retrieved)
				}
			}
		}(i)
	}

	wg.Wait()
}

// testLargeValues verifies handling of large values
func testLargeValues(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "large-value-key"
	value := GenerateLargeValue(1024 * 1024) // 1MB

	err := backend.Set(ctx, key, value, 1*time.Minute)
	require.NoError(t, err, "Should handle large values")

	retrieved, _, exists, err := backend.Get(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists)
	assert.Equal(t, len(value), len(retrieved), "Large value should be retrieved intact")
}

// testEmptyValues verifies handling of empty values
func testEmptyValues(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	key := "empty-value-key"
	value := []byte{}

	err := backend.Set(ctx, key, value, 1*time.Minute)
	require.NoError(t, err, "Should handle empty values")

	retrieved, _, exists, err := backend.Get(ctx, key)
	require.NoError(t, err)
	assert.True(t, exists, "Empty value should exist")
	assert.Equal(t, 0, len(retrieved), "Retrieved value should be empty")
}

// testSpecialCharactersInKeys verifies handling of special characters in keys
func testSpecialCharactersInKeys(t *testing.T, ctx context.Context, backend CacheBackend) {
	t.Helper()

	specialKeys := []string{
		"key:with:colons",
		"key/with/slashes",
		"key-with-dashes",
		"key_with_underscores",
		"key.with.dots",
		"key|with|pipes",
	}

	for _, key := range specialKeys {
		value := []byte(fmt.Sprintf("value-for-%s", key))

		err := backend.Set(ctx, key, value, 1*time.Minute)
		require.NoError(t, err, "Should handle special character in key: %s", key)

		retrieved, _, exists, err := backend.Get(ctx, key)
		require.NoError(t, err)
		assert.True(t, exists, "Key with special characters should exist: %s", key)
		assert.Equal(t, value, retrieved)
	}
}

// Helper functions to setup different backend types
// These will be implemented in respective test files

func setupMemoryBackend(t *testing.T) CacheBackend {
	t.Helper()
	// This will be implemented in memory_test.go
	// For now, return nil to allow compilation
	t.Skip("MemoryBackend implementation pending")
	return nil
}

func setupRedisBackend(t *testing.T) CacheBackend {
	t.Helper()
	// This will be implemented in redis_test.go
	// For now, return nil to allow compilation
	t.Skip("RedisBackend implementation pending")
	return nil
}

func setupHybridBackend(t *testing.T) CacheBackend {
	t.Helper()

	primary := newMockBackend()
	secondary := newMockBackend()

	config := &HybridConfig{
		Primary:         primary,
		Secondary:       secondary,
		AsyncBufferSize: 100,
		Logger:          NewTestLogger(t),
	}

	hybrid, err := NewHybridBackend(config)
	require.NoError(t, err)

	t.Cleanup(func() {
		hybrid.Close()
	})

	return hybrid
}
