package test

import (
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/kms/api-key-service/internal/auth"
	"github.com/kms/api-key-service/internal/config"
	"github.com/kms/api-key-service/internal/domain"
)

func TestJWTManager_GenerateToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
		Claims: map[string]string{
			"department": "engineering",
			"role":       "developer",
		},
	}

	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)
	assert.NotEmpty(t, tokenString)

	// Verify token structure (should have 3 parts separated by dots)
	parts := len(tokenString)
	assert.Greater(t, parts, 100) // JWT tokens are typically longer than 100 chars
}

func TestJWTManager_ValidateToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
		Claims: map[string]string{
			"department": "engineering",
		},
	}

	// Generate token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Validate token
	claims, err := jwtManager.ValidateToken(tokenString)
	require.NoError(t, err)
	assert.Equal(t, userToken.UserID, claims.UserID)
	assert.Equal(t, userToken.AppID, claims.AppID)
	assert.Equal(t, userToken.Permissions, claims.Permissions)
	assert.Equal(t, userToken.TokenType, claims.TokenType)
	assert.Equal(t, userToken.Claims, claims.Claims)
}

func TestJWTManager_ValidateToken_InvalidToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	// Test with invalid token
	_, err := jwtManager.ValidateToken("invalid.token.here")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Invalid token")
}

func TestJWTManager_ValidateToken_ExpiredToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read"},
		IssuedAt:    time.Now().Add(-2 * time.Hour),
		ExpiresAt:   time.Now().Add(-1 * time.Hour), // Expired 1 hour ago
		MaxValidAt:  time.Now().Add(-30 * time.Minute), // Max valid also expired
		TokenType:   domain.TokenTypeUser,
	}

	// Generate token (this should work even with past dates)
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Validate token (this should fail due to expiration)
	_, err = jwtManager.ValidateToken(tokenString)
	assert.Error(t, err)
	// The error could be either JWT expiration or our custom max valid check
	assert.True(t, 
		strings.Contains(err.Error(), "expired beyond maximum validity") || 
		strings.Contains(err.Error(), "token is expired"),
		"Expected expiration error, got: %s", err.Error())
}

func TestJWTManager_RefreshToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	// Generate original token
	originalToken, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Refresh token with new expiration
	newExpiration := time.Now().Add(2 * time.Hour)
	refreshedToken, err := jwtManager.RefreshToken(originalToken, newExpiration)
	require.NoError(t, err)
	assert.NotEmpty(t, refreshedToken)
	assert.NotEqual(t, originalToken, refreshedToken)

	// Validate refreshed token
	claims, err := jwtManager.ValidateToken(refreshedToken)
	require.NoError(t, err)
	assert.Equal(t, userToken.UserID, claims.UserID)
	assert.Equal(t, userToken.AppID, claims.AppID)
}

func TestJWTManager_RefreshToken_ExpiredMaxValid(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read"},
		IssuedAt:    time.Now().Add(-2 * time.Hour),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(-30 * time.Minute), // Max valid expired
		TokenType:   domain.TokenTypeUser,
	}

	// Generate token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Try to refresh (should fail due to max valid expiration)
	newExpiration := time.Now().Add(2 * time.Hour)
	_, err = jwtManager.RefreshToken(tokenString, newExpiration)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "expired beyond maximum validity")
}

func TestJWTManager_ExtractClaims(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(-1 * time.Hour), // Expired token
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	// Generate expired token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Extract claims (should work even for expired tokens)
	claims, err := jwtManager.ExtractClaims(tokenString)
	require.NoError(t, err)
	assert.Equal(t, userToken.UserID, claims.UserID)
	assert.Equal(t, userToken.AppID, claims.AppID)
	assert.Equal(t, userToken.Permissions, claims.Permissions)
}

func TestJWTManager_RevokeToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	// Generate token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Revoke token
	err = jwtManager.RevokeToken(tokenString)
	assert.NoError(t, err)

	// Check if token is revoked
	revoked, err := jwtManager.IsTokenRevoked(tokenString)
	assert.NoError(t, err)
	assert.True(t, revoked)
}

func TestJWTManager_RevokeToken_AlreadyExpired(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read"},
		IssuedAt:    time.Now().Add(-2 * time.Hour),
		ExpiresAt:   time.Now().Add(-1 * time.Hour), // Already expired
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	// Generate expired token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Revoke expired token (should succeed but not add to blacklist)
	err = jwtManager.RevokeToken(tokenString)
	assert.NoError(t, err)

	// Check if token is revoked (should be false since it was already expired)
	revoked, err := jwtManager.IsTokenRevoked(tokenString)
	assert.NoError(t, err)
	assert.False(t, revoked)
}

func TestJWTManager_IsTokenRevoked_NotRevoked(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	// Generate token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Check if token is revoked (should be false)
	revoked, err := jwtManager.IsTokenRevoked(tokenString)
	assert.NoError(t, err)
	assert.False(t, revoked)
}

func TestJWTManager_GetTokenInfo(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
		Claims: map[string]string{
			"department": "engineering",
		},
	}

	// Generate token
	tokenString, err := jwtManager.GenerateToken(userToken)
	require.NoError(t, err)

	// Get token info
	info := jwtManager.GetTokenInfo(tokenString)
	assert.Equal(t, userToken.UserID, info["user_id"])
	assert.Equal(t, userToken.AppID, info["app_id"])
	assert.Equal(t, userToken.Permissions, info["permissions"])
	assert.Equal(t, userToken.TokenType, info["token_type"])
	assert.NotNil(t, info["issued_at"])
	assert.NotNil(t, info["expires_at"])
	assert.NotNil(t, info["max_valid_at"])
	assert.NotNil(t, info["jti"])
}

func TestJWTManager_GetTokenInfo_InvalidToken(t *testing.T) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	// Get info for invalid token
	info := jwtManager.GetTokenInfo("invalid.token.here")
	assert.Contains(t, info["error"], "Invalid token format")
}

// Benchmark tests
func BenchmarkJWTManager_GenerateToken(b *testing.B) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := jwtManager.GenerateToken(userToken)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkJWTManager_ValidateToken(b *testing.B) {
	cfg := config.NewConfig()
	logger := zap.NewNop()
	jwtManager := auth.NewJWTManager(cfg, logger)

	userToken := &domain.UserToken{
		UserID:      "test-user-123",
		AppID:       "test-app-456",
		Permissions: []string{"read", "write"},
		IssuedAt:    time.Now(),
		ExpiresAt:   time.Now().Add(1 * time.Hour),
		MaxValidAt:  time.Now().Add(24 * time.Hour),
		TokenType:   domain.TokenTypeUser,
	}

	tokenString, err := jwtManager.GenerateToken(userToken)
	if err != nil {
		b.Fatal(err)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := jwtManager.ValidateToken(tokenString)
		if err != nil {
			b.Fatal(err)
		}
	}
}
