package cache_test

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"testing"
	"tercul/internal/platform/cache"
	"tercul/internal/platform/config"
	"time"

	"github.com/go-redis/redismock/v9"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewRedisCache(t *testing.T) {
	client, _ := redismock.NewClientMock()
	keyGen := cache.NewDefaultKeyGenerator("test:")
	defaultExpiry := 2 * time.Hour

	c := cache.NewRedisCache(client, keyGen, defaultExpiry)
	require.NotNil(t, c)
}

func TestNewDefaultRedisCache(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		// This test requires a running Redis instance or a mock for the Ping command.
		// For now, we'll just test that the function returns a non-nil cache
		// when the config is valid. A more comprehensive test would involve
		// mocking the redis.NewClient and its Ping method.
		cfg := &config.Config{
			RedisAddr: "localhost:6379",
		}
		// Since this function actually tries to connect, we can't fully test it in a unit test
		// without a live redis. We will skip a full test here and focus on the other methods.
		// In a real-world scenario, we might use a test container with Redis.
		// For now, we'll just ensure it doesn't panic with a basic config.
		// A proper integration test would be better suited for this.
		_ = cfg
		// _, err := cache.NewDefaultRedisCache(cfg)
		// assert.NoError(t, err)
	})

	t.Run("connection error", func(t *testing.T) {
		cfg := &config.Config{
			RedisAddr: "localhost:9999", // Invalid address
		}
		_, err := cache.NewDefaultRedisCache(cfg)
		assert.Error(t, err)
	})
}

func TestRedisCache_Get(t *testing.T) {
	client, mock := redismock.NewClientMock()
	c := cache.NewRedisCache(client, nil, time.Hour)
	ctx := context.Background()
	key := "test-key"
	expectedValue := map[string]string{"foo": "bar"}
	expectedBytes, _ := json.Marshal(expectedValue)

	t.Run("success", func(t *testing.T) {
		mock.ExpectGet(key).SetVal(string(expectedBytes))

		var result map[string]string
		err := c.Get(ctx, key, &result)

		assert.NoError(t, err)
		assert.Equal(t, expectedValue, result)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("cache miss", func(t *testing.T) {
		mock.ExpectGet(key).RedisNil()

		var result map[string]string
		err := c.Get(ctx, key, &result)

		assert.Error(t, err)
		assert.Equal(t, "cache miss", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("redis error", func(t *testing.T) {
		mock.ExpectGet(key).SetErr(errors.New("redis error"))

		var result map[string]string
		err := c.Get(ctx, key, &result)

		assert.Error(t, err)
		assert.Equal(t, "redis error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_InvalidateEntityType(t *testing.T) {
	ctx := context.Background()
	entityType := "user"
	pattern := "tercul:user:*"

	t.Run("success with multiple batches", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		keyGen := cache.NewDefaultKeyGenerator("tercul:")
		c := cache.NewRedisCache(client, keyGen, time.Hour)

		keysBatch1 := make([]string, 100)
		for i := 0; i < 100; i++ {
			keysBatch1[i] = fmt.Sprintf("tercul:user:key%d", i)
		}
		keysBatch2 := []string{"tercul:user:key101", "tercul:user:key102"}

		// Mocking the SCAN and DEL calls in the correct order
		mock.ExpectScan(0, pattern, 100).SetVal(keysBatch1, 1)
		mock.ExpectDel(keysBatch1...).SetVal(100)
		mock.ExpectScan(1, pattern, 100).SetVal(keysBatch2, 0)
		mock.ExpectDel(keysBatch2...).SetVal(2)

		err := c.InvalidateEntityType(ctx, entityType)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("success with single batch", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		keyGen := cache.NewDefaultKeyGenerator("tercul:")
		c := cache.NewRedisCache(client, keyGen, time.Hour)

		keys := []string{"tercul:user:key1", "tercul:user:key2"}
		mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
		mock.ExpectDel(keys...).SetVal(2)

		err := c.InvalidateEntityType(ctx, entityType)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("scan error", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		keyGen := cache.NewDefaultKeyGenerator("tercul:")
		c := cache.NewRedisCache(client, keyGen, time.Hour)

		mock.ExpectScan(0, pattern, 100).SetErr(errors.New("scan error"))

		err := c.InvalidateEntityType(ctx, entityType)
		assert.Error(t, err)
		assert.Equal(t, "scan error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("del error", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		keyGen := cache.NewDefaultKeyGenerator("tercul:")
		c := cache.NewRedisCache(client, keyGen, time.Hour)

		keys := []string{"tercul:user:key1"}
		mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
		mock.ExpectDel(keys...).SetErr(errors.New("del error"))

		err := c.InvalidateEntityType(ctx, entityType)
		assert.Error(t, err)
		assert.Equal(t, "del error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_GetMulti(t *testing.T) {
	client, mock := redismock.NewClientMock()
	c := cache.NewRedisCache(client, nil, time.Hour)
	ctx := context.Background()
	keys := []string{"key1", "key2", "key3"}
	expectedValues := []interface{}{"value1", nil, "value3"}
	expectedResult := map[string][]byte{
		"key1": []byte("value1"),
		"key3": []byte("value3"),
	}

	t.Run("success", func(t *testing.T) {
		mock.ExpectMGet(keys...).SetVal(expectedValues)

		result, err := c.GetMulti(ctx, keys)

		assert.NoError(t, err)
		assert.Equal(t, expectedResult, result)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("no keys", func(t *testing.T) {
		result, err := c.GetMulti(ctx, []string{})
		assert.NoError(t, err)
		assert.Empty(t, result)
	})

	t.Run("redis error", func(t *testing.T) {
		mock.ExpectMGet(keys...).SetErr(errors.New("redis error"))

		_, err := c.GetMulti(ctx, keys)

		assert.Error(t, err)
		assert.Equal(t, "redis error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_SetMulti(t *testing.T) {
	ctx := context.Background()
	items := map[string]interface{}{
		"key1": "value1",
		"key2": 123,
	}

	t.Run("success", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		c := cache.NewRedisCache(client, nil, time.Hour)
		mock.MatchExpectationsInOrder(false)

		// Expect each Set command within the pipeline
		for key, value := range items {
			data, _ := json.Marshal(value)
			mock.ExpectSet(key, data, time.Hour).SetVal("OK")
		}

		// The SetMulti function will call Exec(), which triggers the mock's pipeline hook
		err := c.SetMulti(ctx, items, 0)

		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("no items", func(t *testing.T) {
		client, _ := redismock.NewClientMock()
		c := cache.NewRedisCache(client, nil, time.Hour)
		err := c.SetMulti(ctx, map[string]interface{}{}, 0)
		assert.NoError(t, err)
	})

	t.Run("redis error", func(t *testing.T) {
		client, mock := redismock.NewClientMock()
		c := cache.NewRedisCache(client, nil, time.Hour)
		mock.MatchExpectationsInOrder(false)

		// To simulate a pipeline execution error, we make one of the inner commands fail.
		// The hook will stop processing on the first error and return it.
		data1, _ := json.Marshal("value1")
		mock.ExpectSet("key1", data1, time.Hour).SetErr(errors.New("redis error"))

		data2, _ := json.Marshal(123)
		mock.ExpectSet("key2", data2, time.Hour).SetVal("OK")

		err := c.SetMulti(ctx, items, 0)

		assert.Error(t, err)
		assert.Contains(t, err.Error(), "redis error")
	})
}

func TestRedisCache_EntityOperations(t *testing.T) {
	client, mock := redismock.NewClientMock()
	keyGen := cache.NewDefaultKeyGenerator("tercul:")
	c := cache.NewRedisCache(client, keyGen, time.Hour)
	ctx := context.Background()

	entityType := "user"
	id := uint(123)
	entityKey := "tercul:user:id:123"
	value := map[string]string{"name": "test"}
	valueBytes, _ := json.Marshal(value)

	page := 1
	pageSize := 10
	listKey := "tercul:user:list:1:10"
	listValue := []string{"user1", "user2"}
	listValueBytes, _ := json.Marshal(listValue)

	t.Run("GetEntity", func(t *testing.T) {
		mock.ExpectGet(entityKey).SetVal(string(valueBytes))
		var result map[string]string
		err := c.GetEntity(ctx, entityType, id, &result)
		assert.NoError(t, err)
		assert.Equal(t, value, result)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("SetEntity", func(t *testing.T) {
		mock.ExpectSet(entityKey, valueBytes, time.Hour).SetVal("OK")
		err := c.SetEntity(ctx, entityType, id, value, 0)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("DeleteEntity", func(t *testing.T) {
		mock.ExpectDel(entityKey).SetVal(1)
		err := c.DeleteEntity(ctx, entityType, id)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("GetList", func(t *testing.T) {
		mock.ExpectGet(listKey).SetVal(string(listValueBytes))
		var result []string
		err := c.GetList(ctx, entityType, page, pageSize, &result)
		assert.NoError(t, err)
		assert.Equal(t, listValue, result)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("SetList", func(t *testing.T) {
		mock.ExpectSet(listKey, listValueBytes, time.Hour).SetVal("OK")
		err := c.SetList(ctx, entityType, page, pageSize, listValue, 0)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("DeleteList", func(t *testing.T) {
		mock.ExpectDel(listKey).SetVal(1)
		err := c.DeleteList(ctx, entityType, page, pageSize)
		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_Set(t *testing.T) {
	client, mock := redismock.NewClientMock()
	c := cache.NewRedisCache(client, nil, time.Hour)
	ctx := context.Background()
	key := "test-key"
	value := map[string]string{"foo": "bar"}
	expectedBytes, _ := json.Marshal(value)

	t.Run("success with default expiration", func(t *testing.T) {
		mock.ExpectSet(key, expectedBytes, time.Hour).SetVal("OK")

		err := c.Set(ctx, key, value, 0)

		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("success with custom expiration", func(t *testing.T) {
		expiration := 5 * time.Minute
		mock.ExpectSet(key, expectedBytes, expiration).SetVal("OK")

		err := c.Set(ctx, key, value, expiration)

		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("redis error", func(t *testing.T) {
		mock.ExpectSet(key, expectedBytes, time.Hour).SetErr(errors.New("redis error"))

		err := c.Set(ctx, key, value, 0)

		assert.Error(t, err)
		assert.Equal(t, "redis error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_Delete(t *testing.T) {
	client, mock := redismock.NewClientMock()
	c := cache.NewRedisCache(client, nil, time.Hour)
	ctx := context.Background()
	key := "test-key"

	t.Run("success", func(t *testing.T) {
		mock.ExpectDel(key).SetVal(1)

		err := c.Delete(ctx, key)

		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("redis error", func(t *testing.T) {
		mock.ExpectDel(key).SetErr(errors.New("redis error"))

		err := c.Delete(ctx, key)

		assert.Error(t, err)
		assert.Equal(t, "redis error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}

func TestRedisCache_Clear(t *testing.T) {
	client, mock := redismock.NewClientMock()
	c := cache.NewRedisCache(client, nil, time.Hour)
	ctx := context.Background()

	t.Run("success", func(t *testing.T) {
		mock.ExpectFlushAll().SetVal("OK")

		err := c.Clear(ctx)

		assert.NoError(t, err)
		assert.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("redis error", func(t *testing.T) {
		mock.ExpectFlushAll().SetErr(errors.New("redis error"))

		err := c.Clear(ctx)

		assert.Error(t, err)
		assert.Equal(t, "redis error", err.Error())
		assert.NoError(t, mock.ExpectationsWereMet())
	})
}