package cache

import (
	"context"
	"testing"

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

// TestCacheGetSet tests basic Get and Set operations.
func TestCacheGetSet(t *testing.T) {
	ctx := context.Background()
	client := NewRedisClient(redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	}))

	// Test Set
	key := &CacheKey{Kind: "city", Code: "c146"}
	err := client.Set(ctx, key, []byte(`{"code":"c146"}`), CityTTL)
	if err != nil {
		t.Fatalf("expected no error from Set, got: %v", err)
	}

	// Test Get (cache hit)
	data, err := client.Get(ctx, key)
	if err != nil {
		t.Fatalf("expected no error from Get, got: %v", err)
	}
	if string(data) != `{"code":"c146"}` {
		t.Errorf("expected %s, got %s", `{"code":"c146"}`, string(data))
	}

	// Test Exists
	exists, err := client.Exists(ctx, key)
	if err != nil {
		t.Fatalf("expected no error from Exists, got: %v", err)
	}
	if !exists {
		t.Error("expected key to exist")
	}

	// Test Delete
	err = client.Delete(ctx, key)
	if err != nil {
		t.Fatalf("expected no error from Delete, got: %v", err)
	}

	// Test Get after Delete (cache miss)
	_, err = client.Get(ctx, key)
	if err != nil {
		t.Fatalf("expected no error from Get after Delete, got: %v", err)
	}
}

// TestCacheKeyString tests key string conversion.
func TestCacheKeyString(t *testing.T) {
	// City key
	cityKey := &CacheKey{Kind: "city", Code: "c146"}
	expectedCityKey := "cities:c146"
	if keyString(cityKey) != expectedCityKey {
		t.Errorf("expected %s, got %s", expectedCityKey, keyString(cityKey))
	}

	// Station key
	stationKey := &CacheKey{Kind: "station", Code: "s9600213"}
	expectedStationKey := "stations:s9600213"
	if keyString(stationKey) != expectedStationKey {
		t.Errorf("expected %s, got %s", expectedStationKey, keyString(stationKey))
	}

	// Search key
	searchKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"}
	expectedSearchKey := "search:c146:c213:2026-08-15"
	if keyString(searchKey) != expectedSearchKey {
		t.Errorf("expected %s, got %s", expectedSearchKey, keyString(searchKey))
	}
}

// TestCacheAsideGetOrSet tests the cache-aside GetOrSetFuncPattern.
func TestCacheAsideGetOrSet(t *testing.T) {
	ctx := context.Background()
	client := NewRedisClient(redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	}))

	fetchCallCount := 0
	fetchFunc := func() ([]byte, error) {
		fetchCallCount++
		return []byte(`{"found":true}`), nil
	}

	// First call: cache miss, should fetch from backend
	key := &CacheKey{Kind: "station", Code: "s9600213"}
	data, err := NewCacheAside(client).GetOrSetFuncPattern(ctx, key, fetchFunc, CityTTL)
	if err != nil {
		t.Fatalf("expected no error on cache miss, got: %v", err)
	}
	if string(data) != `{"found":true}` {
		t.Errorf("expected %s, got %s", `{"found":true}`, string(data))
	}
	if fetchCallCount != 1 {
		t.Errorf("expected 1 fetch call, got %d", fetchCallCount)
	}

	// Second call: cache hit, should not fetch from backend
	fetchCallCount = 0
	data, err = NewCacheAside(client).GetOrSetFuncPattern(ctx, key, fetchFunc, CityTTL)
	if err != nil {
		t.Fatalf("expected no error on cache hit, got: %v", err)
	}
	if string(data) != `{"found":true}` {
		t.Errorf("expected %s, got %s", `{"found":true}`, string(data))
	}
	if fetchCallCount != 0 {
		t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount)
	}
}

// TestCacheAsideGetCity tests GetCity with cache.
func TestCacheAsideGetCity(t *testing.T) {
	ctx := context.Background()
	client := NewRedisClient(redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	}))

	fetchCallCount := 0
	fetchFunc := func() ([]byte, error) {
		fetchCallCount++
		return []byte(`{"code":"c146","title":"Simferopol"}`), nil
	}

	key := &CacheKey{Kind: "city", Code: "c146"}
	data, err := NewCacheAside(client).GetCity(ctx, key, fetchFunc)
	if err != nil {
		t.Fatalf("expected no error on cache miss for city, got: %v", err)
	}
	if string(data) != `{"code":"c146","title":"Simferopol"}` {
		t.Errorf("expected %s, got %s", `{"code":"c146","title":"Simferopol"}`, string(data))
	}
	if fetchCallCount != 1 {
		t.Errorf("expected 1 fetch call, got %d", fetchCallCount)
	}

	// Second call: cache hit
	fetchCallCount = 0
	data, err = NewCacheAside(client).GetCity(ctx, key, fetchFunc)
	if err != nil {
		t.Fatalf("expected no error on cache hit for city, got: %v", err)
	}
	if fetchCallCount != 0 {
		t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount)
	}
}

// TestCacheAsideGetSearch tests GetSearch with near-term and far-term TTL.
func TestCacheAsideGetSearch(t *testing.T) {
	ctx := context.Background()
	client := NewRedisClient(redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	}))

	fetchNearTerm := func() ([]byte, error) {
		return []byte(`{"near_term":true}`), nil
	}
	fetchFarTerm := func() ([]byte, error) {
		return []byte(`{"far_term":true}`), nil
	}

	// Near-term search key
	nearKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"}
	// Far-term search key
	farKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-09-15"}

	// Near-term: should use SearchNearTermTTL (3 hours)
	data, err := NewCacheAside(client).GetSearch(ctx, nearKey, fetchNearTerm, false)
	if err != nil {
		t.Fatalf("expected no error on near-term search cache miss, got: %v", err)
	}
	if string(data) != `{"near_term":true}` {
		t.Errorf("expected %s, got %s", `{"near_term":true}`, string(data))
	}

	// Far-term: should use SearchFarTermTTL (7 days)
	data, err = NewCacheAside(client).GetSearch(ctx, farKey, fetchFarTerm, true)
	if err != nil {
		t.Fatalf("expected no error on far-term search cache miss, got: %v", err)
	}
	if string(data) != `{"far_term":true}` {
		t.Errorf("expected %s, got %s", `{"far_term":true}`, string(data))
	}
}

// TestCacheInvalidate tests invalidation operations.
func TestCacheInvalidate(t *testing.T) {
	ctx := context.Background()
	client := NewRedisClient(redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	}))

	// Set up some keys
	cityKey := &CacheKey{Kind: "city", Code: "c146"}
	stationKey := &CacheKey{Kind: "station", Code: "s9600213"}
	searchKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"}

	// Set values first
	client.Set(ctx, cityKey, []byte(`{"code":"c146"}`), CityTTL)
	client.Set(ctx, stationKey, []byte(`{"id":"s9600213"}`), CityTTL)
	client.Set(ctx, searchKey, []byte(`{"search":true}`), SearchNearTermTTL)

	// Invalidate city
	err := NewCacheAside(client).InvalidateCity(ctx, cityKey)
	if err != nil {
		t.Fatalf("expected no error invalidating city, got: %v", err)
	}

	// Invalidate station
	err = NewCacheAside(client).InvalidateStation(ctx, stationKey)
	if err != nil {
		t.Fatalf("expected no error invalidating station, got: %v", err)
	}

	// Invalidate search
	err = NewCacheAside(client).InvalidateSearch(ctx, searchKey)
	if err != nil {
		t.Fatalf("expected no error invalidating search, got: %v", err)
	}
}
