package service

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/user-management-system/internal/auth"
	"github.com/user-management-system/internal/cache"
	"github.com/user-management-system/internal/domain"
	"github.com/user-management-system/internal/repository"
	gormsqlite "gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// =============================================================================
// Mock OAuth Manager
// =============================================================================

type mockOAuthManager struct {
	authURL      string
	exchangeErr  error
	userInfoErr  error
	oauthUser    *auth.OAuthUser
	providers    []auth.OAuthProviderInfo
	config       *auth.OAuthConfig
}

func (m *mockOAuthManager) GetAuthURL(provider auth.OAuthProvider, state string) (string, error) {
	return m.authURL, nil
}

func (m *mockOAuthManager) ExchangeCode(ctx context.Context, provider auth.OAuthProvider, code string) (*auth.OAuthToken, error) {
	if m.exchangeErr != nil {
		return nil, m.exchangeErr
	}
	return &auth.OAuthToken{AccessToken: "mock-token"}, nil
}

func (m *mockOAuthManager) GetUserInfo(ctx context.Context, provider auth.OAuthProvider, token *auth.OAuthToken) (*auth.OAuthUser, error) {
	if m.userInfoErr != nil {
		return nil, m.userInfoErr
	}
	if m.oauthUser != nil {
		return m.oauthUser, nil
	}
	return &auth.OAuthUser{
		OpenID:   "mock-openid",
		UnionID:  "mock-unionid",
		Nickname: "Mock User",
		Email:    "mock@test.com",
		Avatar:   "https://example.com/avatar.png",
	}, nil
}

func (m *mockOAuthManager) ValidateToken(token string) (bool, error) {
	return token != "", nil
}

func (m *mockOAuthManager) GetConfig(provider auth.OAuthProvider) (*auth.OAuthConfig, bool) {
	if m.config != nil {
		return m.config, true
	}
	return nil, false
}

func (m *mockOAuthManager) GetEnabledProviders() []auth.OAuthProviderInfo {
	return m.providers
}

// =============================================================================
// LoginByCode Internal Tests
// =============================================================================

func setupLoginByCodeInternalTestEnv(t *testing.T) (*AuthService, *gorm.DB) {
	t.Helper()

	db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
		DriverName: "sqlite",
		DSN:        fmt.Sprintf("file:logincode_internal_test_%d?mode=memory&cache=shared", time.Now().UnixNano()),
	}), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("failed to connect database: %v", err)
	}

	if err := db.AutoMigrate(&domain.User{}, &domain.LoginLog{}); err != nil {
		t.Fatalf("failed to migrate: %v", err)
	}

	userRepo := repository.NewUserRepository(db)
	loginLogRepo := repository.NewLoginLogRepository(db)
	socialRepo, _ := repository.NewSocialAccountRepository(db)
	jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
		HS256Secret:        fmt.Sprintf("test-secret-%d", time.Now().UnixNano()),
		AccessTokenExpire:  15 * time.Minute,
		RefreshTokenExpire: 7 * 24 * time.Hour,
	})

	l1Cache := cache.NewL1Cache()
	l2Cache := cache.NewRedisCache(false)
	cacheManager := cache.NewCacheManager(l1Cache, l2Cache)

	svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
	svc.SetLoginLogRepository(loginLogRepo)

	return svc, db
}

func TestLoginByCode_Internal(t *testing.T) {
	ctx := context.Background()

	t.Run("LoginByCode with nil service", func(t *testing.T) {
		var nilSvc *AuthService
		_, err := nilSvc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("LoginByCode without SMS service configured", func(t *testing.T) {
		svc, _ := setupLoginByCodeInternalTestEnv(t)
		_, err := svc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error when SMS service not configured")
		}
	})

	t.Run("LoginByCode with empty phone", func(t *testing.T) {
		svc, _ := setupLoginByCodeInternalTestEnv(t)
		smsProvider := &mockSMSProvider{}
		smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
		svc.SetSMSCodeService(smsCodeSvc)

		_, err := svc.LoginByCode(ctx, "", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for empty phone")
		}
	})

	t.Run("LoginByCode for non-existent phone", func(t *testing.T) {
		svc, _ := setupLoginByCodeInternalTestEnv(t)
		smsProvider := &mockSMSProvider{}
		smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
		svc.SetSMSCodeService(smsCodeSvc)

		_, err := svc.LoginByCode(ctx, "19999999999", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for non-existent phone")
		}
	})

	t.Run("LoginByCode for locked user", func(t *testing.T) {
		svc, db := setupLoginByCodeInternalTestEnv(t)
		smsProvider := &mockSMSProvider{}
		smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
		svc.SetSMSCodeService(smsCodeSvc)

		phone := "13800138002"
		user := &domain.User{
			Username: "lockeduser",
			Phone:    &phone,
			Password: "$2a$10$hash",
			Status:   domain.UserStatusLocked,
		}
		db.Create(user)

		_, err := svc.LoginByCode(ctx, "13800138002", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for locked user")
		}
	})

	t.Run("LoginByCode for inactive user", func(t *testing.T) {
		svc, db := setupLoginByCodeInternalTestEnv(t)
		smsProvider := &mockSMSProvider{}
		smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
		svc.SetSMSCodeService(smsCodeSvc)

		phone := "13800138003"
		user := &domain.User{
			Username: "inactiveuser",
			Phone:    &phone,
			Password: "$2a$10$hash",
			Status:   domain.UserStatusInactive,
		}
		db.Create(user)

		_, err := svc.LoginByCode(ctx, "13800138003", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for inactive user")
		}
	})

	t.Run("LoginByCode success", func(t *testing.T) {
		svc, db := setupLoginByCodeInternalTestEnv(t)
		cacheWithCode := &mockCacheWithGet{getResult: "123456", getFound: true}
		smsCodeSvc := NewSMSCodeService(&mockSMSProvider{}, cacheWithCode, DefaultSMSCodeConfig())
		svc.SetSMSCodeService(smsCodeSvc)

		phone := "13800138004"
		user := &domain.User{
			Username: "successuser",
			Phone:    &phone,
			Password: "$2a$10$hash",
			Status:   domain.UserStatusActive,
		}
		db.Create(user)

		resp, err := svc.LoginByCode(ctx, "13800138004", "123456", "127.0.0.1")
		if err != nil {
			t.Fatalf("LoginByCode failed: %v", err)
		}
		if resp.AccessToken == "" {
			t.Error("Expected access token")
		}
	})
}

// =============================================================================
// OAuthCallback Internal Tests
// =============================================================================

func TestOAuthCallback_Internal(t *testing.T) {
	t.Run("OAuthCallback with nil service", func(t *testing.T) {
		var nilSvc *AuthService
		_, err := nilSvc.OAuthCallback(context.Background(), "github", "code123")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("OAuthCallback without OAuth manager", func(t *testing.T) {
		db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
			DriverName: "sqlite",
			DSN:        fmt.Sprintf("file:oauth_no_manager_%d?mode=memory&cache=shared", time.Now().UnixNano()),
		}), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Silent),
		})
		if err != nil {
			t.Fatalf("failed to connect database: %v", err)
		}

		db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})

		userRepo := repository.NewUserRepository(db)
		socialRepo, _ := repository.NewSocialAccountRepository(db)
		jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
			HS256Secret:        "test-secret",
			AccessTokenExpire:  15 * time.Minute,
			RefreshTokenExpire: 7 * 24 * time.Hour,
		})

		svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)

		_, err = svc.OAuthCallback(context.Background(), "github", "code123")
		if err == nil {
			t.Error("Expected error when OAuth manager not configured")
		}
	})

	t.Run("OAuthCallback with exchange error", func(t *testing.T) {
		db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
			DriverName: "sqlite",
			DSN:        fmt.Sprintf("file:oauth_exchange_err_%d?mode=memory&cache=shared", time.Now().UnixNano()),
		}), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Silent),
		})
		if err != nil {
			t.Fatalf("failed to connect database: %v", err)
		}

		db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})

		userRepo := repository.NewUserRepository(db)
		socialRepo, _ := repository.NewSocialAccountRepository(db)
		jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
			HS256Secret:        "test-secret",
			AccessTokenExpire:  15 * time.Minute,
			RefreshTokenExpire: 7 * 24 * time.Hour,
		})

		svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)
		svc.oauthManager = &mockOAuthManager{exchangeErr: fmt.Errorf("exchange failed")}

		_, err = svc.OAuthCallback(context.Background(), "github", "code123")
		if err == nil {
			t.Error("Expected error when exchange fails")
		}
	})

	t.Run("OAuthCallback with user info error", func(t *testing.T) {
		db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
			DriverName: "sqlite",
			DSN:        fmt.Sprintf("file:oauth_userinfo_err_%d?mode=memory&cache=shared", time.Now().UnixNano()),
		}), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Silent),
		})
		if err != nil {
			t.Fatalf("failed to connect database: %v", err)
		}

		db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})

		userRepo := repository.NewUserRepository(db)
		socialRepo, _ := repository.NewSocialAccountRepository(db)
		jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
			HS256Secret:        "test-secret",
			AccessTokenExpire:  15 * time.Minute,
			RefreshTokenExpire: 7 * 24 * time.Hour,
		})

		svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)
		svc.oauthManager = &mockOAuthManager{userInfoErr: fmt.Errorf("user info failed")}

		_, err = svc.OAuthCallback(context.Background(), "github", "code123")
		if err == nil {
			t.Error("Expected error when user info fails")
		}
	})

	t.Run("OAuthCallback success with new user", func(t *testing.T) {
		db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
			DriverName: "sqlite",
			DSN:        fmt.Sprintf("file:oauth_new_user_%d?mode=memory&cache=shared", time.Now().UnixNano()),
		}), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Silent),
		})
		if err != nil {
			t.Fatalf("failed to connect database: %v", err)
		}

		db.AutoMigrate(&domain.User{}, &domain.SocialAccount{}, &domain.LoginLog{})

		userRepo := repository.NewUserRepository(db)
		socialRepo, _ := repository.NewSocialAccountRepository(db)
		loginLogRepo := repository.NewLoginLogRepository(db)
		jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
			HS256Secret:        "test-secret",
			AccessTokenExpire:  15 * time.Minute,
			RefreshTokenExpire: 7 * 24 * time.Hour,
		})

		l1Cache := cache.NewL1Cache()
		l2Cache := cache.NewRedisCache(false)
		cacheManager := cache.NewCacheManager(l1Cache, l2Cache)

		svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
		svc.oauthManager = &mockOAuthManager{}
		svc.SetLoginLogRepository(loginLogRepo)

		resp, err := svc.OAuthCallback(context.Background(), "github", "code123")
		if err != nil {
			t.Fatalf("OAuthCallback failed: %v", err)
		}
		if resp.AccessToken == "" {
			t.Error("Expected access token")
		}
	})

	t.Run("OAuthCallback success with existing social account", func(t *testing.T) {
		db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
			DriverName: "sqlite",
			DSN:        fmt.Sprintf("file:oauth_existing_%d?mode=memory&cache=shared", time.Now().UnixNano()),
		}), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Silent),
		})
		if err != nil {
			t.Fatalf("failed to connect database: %v", err)
		}

		db.AutoMigrate(&domain.User{}, &domain.SocialAccount{}, &domain.LoginLog{})

		// Create existing user and social account
		user := &domain.User{
			Username: "existinguser",
			Password: "$2a$10$hash",
			Status:   domain.UserStatusActive,
		}
		db.Create(user)

		socialAccount := &domain.SocialAccount{
			UserID:   user.ID,
			Provider: "github",
			OpenID:   "mock-openid",
			Status:   domain.SocialAccountStatusActive,
		}
		db.Create(socialAccount)

		userRepo := repository.NewUserRepository(db)
		socialRepo, _ := repository.NewSocialAccountRepository(db)
		loginLogRepo := repository.NewLoginLogRepository(db)
		jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
			HS256Secret:        "test-secret",
			AccessTokenExpire:  15 * time.Minute,
			RefreshTokenExpire: 7 * 24 * time.Hour,
		})

		l1Cache := cache.NewL1Cache()
		l2Cache := cache.NewRedisCache(false)
		cacheManager := cache.NewCacheManager(l1Cache, l2Cache)

		svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
		svc.oauthManager = &mockOAuthManager{}
		svc.SetLoginLogRepository(loginLogRepo)

		resp, err := svc.OAuthCallback(context.Background(), "github", "code123")
		if err != nil {
			t.Fatalf("OAuthCallback failed: %v", err)
		}
		if resp.AccessToken == "" {
			t.Error("Expected access token")
		}
		if resp.User.Username != "existinguser" {
			t.Errorf("Expected username 'existinguser', got %s", resp.User.Username)
		}
	})
}

// =============================================================================
// OAuthBindCallback Tests
// =============================================================================

func TestOAuthBindCallback_Internal(t *testing.T) {
	t.Run("OAuthBindCallback with nil service", func(t *testing.T) {
		var nilSvc *AuthService
		_, err := nilSvc.OAuthBindCallback(context.Background(), 1, "github", "code123")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})
}

// =============================================================================
// StartSocialAccountBinding Tests
// =============================================================================

func TestStartSocialAccountBinding_Internal(t *testing.T) {
	t.Run("StartSocialAccountBinding with nil service", func(t *testing.T) {
		var nilSvc *AuthService
		_, _, err := nilSvc.StartSocialAccountBinding(context.Background(), 1, "github", "", "", "")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})
}
