package authorization

import (
	"context"
	"testing"
	"time"
	"tm/pkg/logger"

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

// MockRedisClient is a mock implementation of the Redis client interface
type MockRedisClient struct {
	mock.Mock
}

func (m *MockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
	args := m.Called(ctx, key, value, expiration)
	return args.Error(0)
}

func (m *MockRedisClient) Get(ctx context.Context, key string) (string, error) {
	args := m.Called(ctx, key)
	return args.String(0), args.Error(1)
}

func (m *MockRedisClient) Del(ctx context.Context, keys ...string) error {
	args := m.Called(ctx, keys)
	return args.Error(0)
}

func (m *MockRedisClient) Exists(ctx context.Context, keys ...string) (int64, error) {
	args := m.Called(ctx, keys)
	return args.Get(0).(int64), args.Error(1)
}

func (m *MockRedisClient) Close() error {
	args := m.Called()
	return args.Error(0)
}

// MockLogger is a mock implementation of the logger interface
type MockLogger struct {
	mock.Mock
}

func (m *MockLogger) Debug(msg string, fields map[string]interface{}) {
	m.Called(msg, fields)
}

func (m *MockLogger) Info(msg string, fields map[string]interface{}) {
	m.Called(msg, fields)
}

func (m *MockLogger) Warn(msg string, fields map[string]interface{}) {
	m.Called(msg, fields)
}

func (m *MockLogger) Error(msg string, fields map[string]interface{}) {
	m.Called(msg, fields)
}

func (m *MockLogger) Fatal(msg string, fields map[string]interface{}) {
	m.Called(msg, fields)
}

func (m *MockLogger) Sync() error {
	args := m.Called()
	return args.Error(0)
}

func (m *MockLogger) WithFields(fields map[string]interface{}) logger.Logger {
	args := m.Called(fields)
	return args.Get(0).(logger.Logger)
}

func TestGenerateTokenHash(t *testing.T) {
	service := &Service{}

	token1 := "test-token-1"
	token2 := "test-token-2"

	hash1 := service.generateTokenHash(token1)
	hash2 := service.generateTokenHash(token2)

	// Hash should be consistent for the same input
	assert.Equal(t, service.generateTokenHash(token1), hash1)

	// Different tokens should have different hashes
	assert.NotEqual(t, hash1, hash2)

	// Hash should be a valid hex string
	assert.Len(t, hash1, 64) // SHA-256 produces 32 bytes = 64 hex chars
}

func TestInvalidateToken_WithRedis(t *testing.T) {
	mockRedis := new(MockRedisClient)
	mockLogger := new(MockLogger)

	config := &AuthorizationConfig{
		AccessTokenSecret:  "test-secret",
		RefreshTokenSecret: "test-refresh-secret",
		AccessTokenExpiry:  15 * time.Minute,
		RefreshTokenExpiry: 7 * 24 * time.Hour,
		Issuer:             "test",
		Audience:           "test-api",
	}

	service := &Service{
		config: config,
		logger: mockLogger,
		redis:  mockRedis,
	}

	// Generate a valid token first
	token, err := service.GenerateAccessToken("user123", "test@example.com", "user", "company123")
	assert.NoError(t, err)

	// Mock Redis Set call
	mockRedis.On("Set", mock.Anything, mock.Anything, "1", mock.Anything).Return(nil)

	// Mock logger calls
	mockLogger.On("Info", mock.Anything, mock.Anything).Return()

	// Test invalidation
	err = service.InvalidateToken(token)
	assert.NoError(t, err)

	// Verify Redis was called
	mockRedis.AssertExpectations(t)
}

func TestInvalidateToken_WithoutRedis(t *testing.T) {
	mockLogger := new(MockLogger)

	config := &AuthorizationConfig{
		AccessTokenSecret:  "test-secret",
		RefreshTokenSecret: "test-refresh-secret",
		AccessTokenExpiry:  15 * time.Minute,
		RefreshTokenExpiry: 7 * 24 * time.Hour,
		Issuer:             "test",
		Audience:           "test-api",
	}

	service := &Service{
		config: config,
		logger: mockLogger,
		redis:  nil, // No Redis client
	}

	// Mock logger calls
	mockLogger.On("Warn", mock.Anything, mock.Anything).Return()

	// Test invalidation without Redis
	err := service.InvalidateToken("test-token")
	assert.NoError(t, err)

	// Verify warning was logged
	mockLogger.AssertExpectations(t)
}

func TestIsTokenBlacklisted_WithRedis(t *testing.T) {
	mockRedis := new(MockRedisClient)
	mockLogger := new(MockLogger)

	config := &AuthorizationConfig{
		AccessTokenSecret:  "test-secret",
		RefreshTokenSecret: "test-refresh-secret",
		AccessTokenExpiry:  15 * time.Minute,
		RefreshTokenExpiry: 7 * 24 * time.Hour,
		Issuer:             "test",
		Audience:           "test-api",
	}

	service := &Service{
		config: config,
		logger: mockLogger,
		redis:  mockRedis,
	}

	token := "test-token"
	tokenHash := service.generateTokenHash(token)

	// Test case 1: Token is blacklisted
	mockRedis.On("Exists", mock.Anything, []string{"blacklist:" + tokenHash}).Return(int64(1), nil).Once()
	mockLogger.On("Debug", mock.Anything, mock.Anything).Return()

	isBlacklisted, err := service.IsTokenBlacklisted(token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)

	// Test case 2: Token is not blacklisted
	mockRedis.On("Exists", mock.Anything, []string{"blacklist:" + tokenHash}).Return(int64(0), nil).Once()
	mockLogger.On("Debug", mock.Anything, mock.Anything).Return()

	isBlacklisted, err = service.IsTokenBlacklisted(token)
	assert.NoError(t, err)
	assert.False(t, isBlacklisted)

	mockRedis.AssertExpectations(t)
}

func TestIsTokenBlacklisted_WithoutRedis(t *testing.T) {
	mockLogger := new(MockLogger)

	config := &AuthorizationConfig{
		AccessTokenSecret:  "test-secret",
		RefreshTokenSecret: "test-refresh-secret",
		AccessTokenExpiry:  15 * time.Minute,
		RefreshTokenExpiry: 7 * 24 * time.Hour,
		Issuer:             "test",
		Audience:           "test-api",
	}

	service := &Service{
		config: config,
		logger: mockLogger,
		redis:  nil, // No Redis client
	}

	// Test blacklist check without Redis
	isBlacklisted, err := service.IsTokenBlacklisted("test-token")
	assert.NoError(t, err)
	assert.False(t, isBlacklisted)
}

func TestClearExpiredTokens(t *testing.T) {
	mockRedis := new(MockRedisClient)
	mockLogger := new(MockLogger)

	service := &Service{
		logger: mockLogger,
		redis:  mockRedis,
	}

	// Mock logger call
	mockLogger.On("Info", mock.Anything, mock.Anything).Return()

	// Test clearing expired tokens
	err := service.ClearExpiredTokens()
	assert.NoError(t, err)

	mockLogger.AssertExpectations(t)
}

func TestGetBlacklistStats(t *testing.T) {
	mockRedis := new(MockRedisClient)
	mockLogger := new(MockLogger)

	service := &Service{
		logger: mockLogger,
		redis:  mockRedis,
	}

	ctx := context.Background()

	// Test with Redis available
	stats, err := service.GetBlacklistStats(ctx)
	assert.NoError(t, err)
	assert.Equal(t, true, stats["redis_available"])

	// Test without Redis
	service.redis = nil
	stats, err = service.GetBlacklistStats(ctx)
	assert.NoError(t, err)
	assert.Equal(t, false, stats["redis_available"])
}
