package validator

import (
	"context"
	"testing"

	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	"github.com/redis/go-redis/v9"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go.uber.org/zap"
)

// MockRedisClient is a mock implementation of RedisClient interface
type MockRedisClient struct {
	mock.Mock
}

func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd {
	args := m.Called(ctx)
	return args.Get(0).(*redis.StatusCmd)
}

func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd {
	args := m.Called(ctx, key)
	return args.Get(0).(*redis.StringCmd)
}

// TestTokenValidator_Validate tests the token validation functionality
func TestTokenValidator_Validate(t *testing.T) {
	tests := []struct {
		name      string
		token     string
		setupMock func(*MockRedisClient)
		wantUser  string
		wantErr   bool
		errType   error
	}{
		{
			name:  "valid token",
			token: "valid-token-123",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping success
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)

				// Mock Get success
				getCmd := redis.NewStringCmd(context.Background())
				getCmd.SetVal("user-789")
				m.On("Get", mock.Anything, constants.RedisAuthTokenKey("valid-token-123")).Return(getCmd)
			},
			wantUser: "user-789",
			wantErr:  false,
		},
		{
			name:  "expired or invalid token (redis.Nil)",
			token: "expired-token",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping success
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)

				// Mock Get returns redis.Nil (key not found)
				getCmd := redis.NewStringCmd(context.Background())
				getCmd.SetErr(redis.Nil)
				m.On("Get", mock.Anything, constants.RedisAuthTokenKey("expired-token")).Return(getCmd)
			},
			wantUser: "",
			wantErr:  true,
			errType:  errors.ErrInvalidToken,
		},
		{
			name:  "Redis unavailable (fail closed)",
			token: "any-token",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping failure
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetErr(context.DeadlineExceeded)
				m.On("Ping", mock.Anything).Return(pingCmd)
			},
			wantUser: "",
			wantErr:  true,
			errType:  errors.ErrRedisUnavailable,
		},
		{
			name:  "context timeout in Redis operations",
			token: "timeout-token",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping success
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)

				// Mock Get with context timeout error
				getCmd := redis.NewStringCmd(context.Background())
				getCmd.SetErr(context.DeadlineExceeded)
				m.On("Get", mock.Anything, constants.RedisAuthTokenKey("timeout-token")).Return(getCmd)
			},
			wantUser: "",
			wantErr:  true,
		},
		{
			name:  "empty token",
			token: "",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping success
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)

				// Mock Get returns redis.Nil for empty token
				getCmd := redis.NewStringCmd(context.Background())
				getCmd.SetErr(redis.Nil)
				m.On("Get", mock.Anything, constants.RedisAuthTokenKey("")).Return(getCmd)
			},
			wantUser: "",
			wantErr:  true,
			errType:  errors.ErrInvalidToken,
		},
		{
			name:  "Redis returns empty user ID",
			token: "invalid-user-token",
			setupMock: func(m *MockRedisClient) {
				// Mock Ping success
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)

				// Mock Get returns empty string
				getCmd := redis.NewStringCmd(context.Background())
				getCmd.SetVal("")
				m.On("Get", mock.Anything, constants.RedisAuthTokenKey("invalid-user-token")).Return(getCmd)
			},
			wantUser: "",
			wantErr:  true,
			errType:  errors.ErrInvalidToken,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock Redis client
			mockRedis := new(MockRedisClient)
			if tt.setupMock != nil {
				tt.setupMock(mockRedis)
			}

			// Create validator with mock
			validator := NewTokenValidator(mockRedis, zap.NewNop())

			// Call Validate
			userID, err := validator.Validate(tt.token)

			// Assert results
			if tt.wantErr {
				assert.Error(t, err, "Expected error for test case: %s", tt.name)
				if tt.errType != nil {
					assert.ErrorIs(t, err, tt.errType, "Expected specific error type for test case: %s", tt.name)
				}
			} else {
				assert.NoError(t, err, "Expected no error for test case: %s", tt.name)
			}

			assert.Equal(t, tt.wantUser, userID, "User ID mismatch for test case: %s", tt.name)

			// Assert all expectations were met
			mockRedis.AssertExpectations(t)
		})
	}
}

// TestTokenValidator_IsAvailable tests the Redis availability check
func TestTokenValidator_IsAvailable(t *testing.T) {
	tests := []struct {
		name      string
		setupMock func(*MockRedisClient)
		want      bool
	}{
		{
			name: "Redis is available",
			setupMock: func(m *MockRedisClient) {
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetVal("PONG")
				m.On("Ping", mock.Anything).Return(pingCmd)
			},
			want: true,
		},
		{
			name: "Redis is unavailable",
			setupMock: func(m *MockRedisClient) {
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetErr(context.DeadlineExceeded)
				m.On("Ping", mock.Anything).Return(pingCmd)
			},
			want: false,
		},
		{
			name: "Redis connection refused",
			setupMock: func(m *MockRedisClient) {
				pingCmd := redis.NewStatusCmd(context.Background())
				pingCmd.SetErr(assert.AnError)
				m.On("Ping", mock.Anything).Return(pingCmd)
			},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock Redis client
			mockRedis := new(MockRedisClient)
			if tt.setupMock != nil {
				tt.setupMock(mockRedis)
			}

			// Create validator with mock
			validator := NewTokenValidator(mockRedis, zap.NewNop())

			// Call IsAvailable
			available := validator.IsAvailable()

			// Assert result
			assert.Equal(t, tt.want, available, "Availability mismatch for test case: %s", tt.name)

			// Assert all expectations were met
			mockRedis.AssertExpectations(t)
		})
	}
}

// TestTokenValidator_ValidateWithRealTimeout tests with actual context timeout
func TestTokenValidator_ValidateWithRealTimeout(t *testing.T) {
	// This test verifies that the validator uses a 50ms timeout internally
	// We test this by simulating a timeout error from Redis

	mockRedis := new(MockRedisClient)

	// Mock Ping success
	pingCmd := redis.NewStatusCmd(context.Background())
	pingCmd.SetVal("PONG")
	mockRedis.On("Ping", mock.Anything).Return(pingCmd)

	// Mock Get with timeout error
	getCmd := redis.NewStringCmd(context.Background())
	getCmd.SetErr(context.DeadlineExceeded)
	mockRedis.On("Get", mock.Anything, mock.Anything).Return(getCmd)

	// Create validator with mock
	validator := NewTokenValidator(mockRedis, zap.NewNop())

	// Call Validate (should return timeout error)
	userID, err := validator.Validate("timeout-token")

	// Should get timeout error
	assert.Error(t, err)
	assert.Equal(t, "", userID)
	assert.ErrorIs(t, err, context.DeadlineExceeded)

	mockRedis.AssertExpectations(t)
}
