package service_test

import (
	"context"
	"encoding/json"
	"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"
	"github.com/user-management-system/internal/service"
	gormsqlite "gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// =============================================================================
// Auth Social Account Binding Tests
// =============================================================================

type socialTestEnv struct {
	db         *gorm.DB
	authSvc    *service.AuthService
	userRepo   *repository.UserRepository
	socialRepo repository.SocialAccountRepository
}

func setupSocialTestEnv(t *testing.T) *socialTestEnv {
	t.Helper()

	dsn := fmt.Sprintf("file:social_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
	db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
		DriverName: "sqlite",
		DSN:        dsn,
	}), &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.SocialAccount{}); err != nil {
		t.Fatalf("failed to migrate: %v", err)
	}

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

	userRepo := repository.NewUserRepository(db)
	socialRepo, err := repository.NewSocialAccountRepository(db)
	if err != nil {
		t.Fatalf("failed to create social account repository: %v", err)
	}
	l1Cache := cache.NewL1Cache()
	l2Cache := cache.NewRedisCache(false)
	cacheManager := cache.NewCacheManager(l1Cache, l2Cache)

	// Pass socialRepo to NewAuthService so GetSocialAccounts works
	authSvc := service.NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)

	return &socialTestEnv{
		db:         db,
		authSvc:    authSvc,
		userRepo:   userRepo,
		socialRepo: socialRepo,
	}
}

func TestAuthService_GetSocialAccounts(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user
	user := &domain.User{
		Username: "socialuser",
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

	t.Run("Get social accounts with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		accounts, err := nilSvc.GetSocialAccounts(ctx, user.ID)
		if err != nil {
			t.Errorf("Expected nil error for nil service, got: %v", err)
		}
		if len(accounts) != 0 {
			t.Errorf("Expected empty accounts for nil service, got: %d", len(accounts))
		}
	})

	t.Run("Get social accounts for user with no accounts", func(t *testing.T) {
		accounts, err := env.authSvc.GetSocialAccounts(ctx, user.ID)
		if err != nil {
			t.Fatalf("GetSocialAccounts failed: %v", err)
		}
		if len(accounts) != 0 {
			t.Errorf("Expected empty accounts, got: %d", len(accounts))
		}
	})

	t.Run("Get social accounts for user with accounts", func(t *testing.T) {
		// Create social accounts
		socialAccount := &domain.SocialAccount{
			UserID:   user.ID,
			Provider: "github",
			OpenID:   "github123",
			Status:   domain.SocialAccountStatusActive,
		}
		env.db.Create(socialAccount)

		accounts, err := env.authSvc.GetSocialAccounts(ctx, user.ID)
		if err != nil {
			t.Fatalf("GetSocialAccounts failed: %v", err)
		}
		if len(accounts) != 1 {
			t.Errorf("Expected 1 account, got: %d", len(accounts))
		}
		if accounts[0].Provider != "github" {
			t.Errorf("Expected provider 'github', got: %s", accounts[0].Provider)
		}
	})
}

func TestAuthService_BindSocialAccount(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user
	user := &domain.User{
		Username: "binduser",
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

	t.Run("Bind social account with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		err := nilSvc.BindSocialAccount(ctx, user.ID, "github", "openid123")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("Bind social account for non-existent user", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, 9999, "github", "openid123")
		if err == nil {
			t.Error("Expected error for non-existent user")
		}
	})

	t.Run("Bind social account for inactive user", func(t *testing.T) {
		inactiveUser := &domain.User{
			Username: "inactivesocial",
			Password: "$2a$10$hash",
			Status:   domain.UserStatusInactive,
		}
		env.db.Create(inactiveUser)

		err := env.authSvc.BindSocialAccount(ctx, inactiveUser.ID, "github", "openid456")
		if err == nil {
			t.Error("Expected error for inactive user")
		}
	})

	t.Run("Bind social account with empty provider", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, user.ID, "", "openid123")
		if err == nil {
			t.Error("Expected error for empty provider")
		}
	})

	t.Run("Bind social account with empty openID", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, user.ID, "github", "")
		if err == nil {
			t.Error("Expected error for empty openID")
		}
	})

	t.Run("Bind social account success", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "google789")
		if err != nil {
			t.Fatalf("BindSocialAccount failed: %v", err)
		}

		// Verify binding
		accounts, _ := env.authSvc.GetSocialAccounts(ctx, user.ID)
		if len(accounts) == 0 {
			t.Error("Expected social account to be created")
		}
	})

	t.Run("Bind same provider with same openID (idempotent)", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "google789")
		if err != nil {
			t.Fatalf("Expected no error for same binding: %v", err)
		}
	})

	t.Run("Bind same provider with different openID", func(t *testing.T) {
		err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "different_openid")
		if err == nil {
			t.Error("Expected error for different openID on same provider")
		}
	})
}

func TestAuthService_BindSocialAccount_AlreadyBound(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create two users
	user1 := &domain.User{
		Username: "binduser1",
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user1)

	user2 := &domain.User{
		Username: "binduser2",
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user2)

	// Bind social account to user1
	env.authSvc.BindSocialAccount(ctx, user1.ID, "wechat", "wechat123")

	// Try to bind same openID to user2
	err := env.authSvc.BindSocialAccount(ctx, user2.ID, "wechat", "wechat123")
	if err == nil {
		t.Error("Expected error when binding already bound account")
	}
}

func TestAuthService_UnbindSocialAccount(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user with password
	hashedPassword, _ := auth.HashPassword("Password123!")
	user := &domain.User{
		Username: "unbinduser",
		Password: hashedPassword,
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

	// Create social account
	socialAccount := &domain.SocialAccount{
		UserID:   user.ID,
		Provider: "github",
		OpenID:   "github123",
		Status:   domain.SocialAccountStatusActive,
	}
	env.db.Create(socialAccount)

	t.Run("Unbind social account with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		err := nilSvc.UnbindSocialAccount(ctx, user.ID, "github", "Password123!", "")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("Unbind social account for non-existent user", func(t *testing.T) {
		err := env.authSvc.UnbindSocialAccount(ctx, 9999, "github", "Password123!", "")
		if err == nil {
			t.Error("Expected error for non-existent user")
		}
	})

	t.Run("Unbind social account not bound", func(t *testing.T) {
		err := env.authSvc.UnbindSocialAccount(ctx, user.ID, "nonexistent_provider", "Password123!", "")
		if err == nil {
			t.Error("Expected error for non-bound provider")
		}
	})

	t.Run("Unbind social account with wrong password", func(t *testing.T) {
		err := env.authSvc.UnbindSocialAccount(ctx, user.ID, "github", "wrongpassword", "")
		if err == nil {
			t.Error("Expected error for wrong password")
		}
	})
}

// =============================================================================
// Verify Sensitive Action Tests
// =============================================================================

func TestAuthService_VerifySensitiveAction(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	t.Run("Verify with nil user", func(t *testing.T) {
		var nilSvc *service.AuthService
		err := nilSvc.VerifyTOTP(ctx, 1, "code", "")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("Verify with user without password or TOTP", func(t *testing.T) {
		user := &domain.User{
			Username: "nosecretuser",
			Status:   domain.UserStatusActive,
		}
		env.db.Create(user)

		err := env.authSvc.VerifyTOTP(ctx, user.ID, "123456", "")
		if err == nil {
			t.Error("Expected error when no verification method available")
		}
	})
}

// =============================================================================
// Start Social Account Binding Tests
// =============================================================================

func TestAuthService_StartSocialAccountBinding(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user with password
	hashedPassword, _ := auth.HashPassword("Password123!")
	user := &domain.User{
		Username: "startbinduser",
		Password: hashedPassword,
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

	t.Run("Start binding with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		_, _, err := nilSvc.StartSocialAccountBinding(ctx, user.ID, "github", "http://localhost", "Password123!", "")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("Start binding for non-existent user", func(t *testing.T) {
		_, _, err := env.authSvc.StartSocialAccountBinding(ctx, 9999, "github", "http://localhost", "Password123!", "")
		if err == nil {
			t.Error("Expected error for non-existent user")
		}
	})

	t.Run("Start binding for inactive user", func(t *testing.T) {
		inactiveUser := &domain.User{
			Username: "inactivestartbind",
			Password: hashedPassword,
			Status:   domain.UserStatusInactive,
		}
		env.db.Create(inactiveUser)

		_, _, err := env.authSvc.StartSocialAccountBinding(ctx, inactiveUser.ID, "github", "http://localhost", "Password123!", "")
		if err == nil {
			t.Error("Expected error for inactive user")
		}
	})

	t.Run("Start binding with wrong password", func(t *testing.T) {
		_, _, err := env.authSvc.StartSocialAccountBinding(ctx, user.ID, "github", "http://localhost", "wrongpassword", "")
		if err == nil {
			t.Error("Expected error for wrong password")
		}
	})
}

// =============================================================================
// OAuth Bind Callback Tests
// =============================================================================

func TestAuthService_OAuthBindCallback(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user
	user := &domain.User{
		Username: "oauthcallbackuser",
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

	t.Run("OAuth bind callback with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		_, err := nilSvc.OAuthBindCallback(ctx, user.ID, "github", "code123")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("OAuth bind callback for non-existent user", func(t *testing.T) {
		_, err := env.authSvc.OAuthBindCallback(ctx, 9999, "github", "code123")
		if err == nil {
			t.Error("Expected error for non-existent user")
		}
	})

	t.Run("OAuth bind callback for inactive user", func(t *testing.T) {
		inactiveUser := &domain.User{
			Username: "inactivecallback",
			Password: "$2a$10$hash",
			Status:   domain.UserStatusInactive,
		}
		env.db.Create(inactiveUser)

		_, err := env.authSvc.OAuthBindCallback(ctx, inactiveUser.ID, "github", "code123")
		if err == nil {
			t.Error("Expected error for inactive user")
		}
	})
}

// =============================================================================
// Verify TOTP Tests
// =============================================================================

func TestAuthService_VerifyTOTP(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	t.Run("Verify TOTP with nil service", func(t *testing.T) {
		var nilSvc *service.AuthService
		err := nilSvc.VerifyTOTP(ctx, 1, "123456", "")
		if err == nil {
			t.Error("Expected error for nil service")
		}
	})

	t.Run("Verify TOTP for non-existent user", func(t *testing.T) {
		err := env.authSvc.VerifyTOTP(ctx, 9999, "123456", "")
		if err == nil {
			t.Error("Expected error for non-existent user")
		}
	})

	t.Run("Verify TOTP for user without TOTP", func(t *testing.T) {
		user := &domain.User{
			Username: "nototpverify",
			Password: "$2a$10$hash",
			Status:   domain.UserStatusActive,
		}
		env.db.Create(user)

		err := env.authSvc.VerifyTOTP(ctx, user.ID, "123456", "")
		if err == nil {
			t.Error("Expected error for user without TOTP")
		}
	})
}

func TestAuthService_VerifyTOTPWithTrustedDevice(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create user with TOTP
	user := &domain.User{
		Username:    "totptrusted",
		Password:    "$2a$10$hash",
		Status:      domain.UserStatusActive,
		TOTPEnabled: true,
		TOTPSecret:  "JBSWY3DPEHPK3PXP", // test secret
	}
	env.db.Create(user)

	// Create device service
	deviceRepo := repository.NewDeviceRepository(env.db)
	userRepo := repository.NewUserRepository(env.db)
	deviceSvc := service.NewDeviceService(deviceRepo, userRepo)

	// Update auth service with device service
	authSvcWithDevice := service.NewAuthService(userRepo, nil, nil, nil, 8, 5, 15*time.Minute)
	authSvcWithDevice.SetDeviceService(deviceSvc)

	t.Run("Verify TOTP without device ID", func(t *testing.T) {
		err := authSvcWithDevice.VerifyTOTP(ctx, user.ID, "123456", "")
		if err == nil {
			// Should fail because the code is wrong
		}
	})

	t.Run("Verify TOTP with non-existent device", func(t *testing.T) {
		err := authSvcWithDevice.VerifyTOTP(ctx, user.ID, "123456", "nonexistent_device")
		if err == nil {
			// Should fail because device doesn't exist
		}
	})
}

// =============================================================================
// Verify TOTP Code or Recovery Code Tests
// =============================================================================

func TestAuthService_VerifyTOTPCodeOrRecoveryCode(t *testing.T) {
	// Create recovery codes hash
	recoveryCodes := []string{"code1", "code2", "code3"}
	recoveryCodesJSON, _ := json.Marshal(recoveryCodes)

	user := &domain.User{
		Username:          "recoveryuser",
		Password:          "$2a$10$hash",
		Status:            domain.UserStatusActive,
		TOTPEnabled:       true,
		TOTPSecret:        "JBSWY3DPEHPK3PXP",
		TOTPRecoveryCodes: string(recoveryCodesJSON),
	}

	t.Run("User has TOTP enabled but wrong code", func(t *testing.T) {
		// This tests the logic path where TOTP validation fails
		// The function should try recovery codes
		if !user.TOTPEnabled {
			t.Error("Expected TOTP to be enabled")
		}
	})
}

// =============================================================================
// Login By Code Tests
// =============================================================================

func TestAuthService_LoginByCode(t *testing.T) {
	env := setupSocialTestEnv(t)
	ctx := context.Background()

	// Create test user with phone
	phone := "13800138000"
	user := &domain.User{
		Username: "logincodeuser",
		Phone:    &phone,
		Password: "$2a$10$hash",
		Status:   domain.UserStatusActive,
	}
	env.db.Create(user)

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

	t.Run("Login by code with empty phone", func(t *testing.T) {
		_, err := env.authSvc.LoginByCode(ctx, "", "123456", "127.0.0.1")
		if err == nil {
			t.Error("Expected error for empty phone")
		}
	})

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

