//go:build integration

package identity

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"testing"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"qycw/go-server/internal/auth"
	"qycw/go-server/internal/config"
	redisstore "qycw/go-server/internal/redis"
)

func TestIdentityLifecycleIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" || os.Getenv("QYCW_REDIS_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 and QYCW_REDIS_INTEGRATION=1 to run identity integration checks")
	}
	dsn := firstIntegrationEnv("GO_DB_DSN", "DB_DSN")
	if dsn == "" {
		t.Fatal("GO_DB_DSN or DB_DSN is required")
	}
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		t.Fatalf("open mysql: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("mysql handle: %v", err)
	}
	defer sqlDB.Close()

	redisDB, _ := strconv.Atoi(firstIntegrationEnv("GO_REDIS_DB", "REDIS_DB"))
	redisClient, err := redisstore.Open(context.Background(), redisstore.Options{
		Addr:     integrationEnvDefault("127.0.0.1:6379", "GO_REDIS_ADDR", "REDIS_ADDR"),
		Password: firstIntegrationEnv("GO_REDIS_PASSWORD", "REDIS_PASSWORD"),
		DB:       redisDB,
	})
	if err != nil {
		t.Fatalf("open redis: %v", err)
	}
	defer redisClient.Close()

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin mysql transaction: %v", tx.Error)
	}
	defer tx.Rollback()
	cfg := config.Bootstrap{
		AppName:                  "QYCW identity integration",
		JWTSecretKey:             "identity-integration-secret-at-least-32-bytes",
		AccessTokenExpireMinutes: 30,
		RefreshTokenExpireDays:   1,
	}
	service := NewService(tx, redisClient, cfg, nil)
	ctx := context.Background()
	suffix := strconv.FormatInt(time.Now().UTC().UnixNano(), 36)
	inviteCode := "IT" + suffix

	inviter := User{Username: "inviter_" + suffix, Role: "user", Status: "active", IDCardVerified: "unverified", InviteCode: &inviteCode}
	if err := tx.Create(&inviter).Error; err != nil {
		t.Fatalf("create inviter: %v", err)
	}
	if err := tx.Create(&UserAgent{UserID: inviter.ID, Status: "active", InviteCode: &inviteCode}).Error; err != nil {
		t.Fatalf("create inviter agent: %v", err)
	}
	if err := tx.Create(&AffiliateClick{AffCode: inviteCode, AgentID: &inviter.ID}).Error; err != nil {
		t.Fatalf("create affiliate click: %v", err)
	}

	email := fmt.Sprintf("identity-%s@example.com", suffix)
	username := "identity_" + suffix
	password := "InitialPass123!"
	if err := service.saveCode(ctx, "email_register", email, "123456"); err != nil {
		t.Fatalf("save register code: %v", err)
	}
	registered, err := service.Register(ctx, RegisterRequest{Email: email, Username: username, Code: "123456", Password: &password, InviteCode: &inviteCode})
	if err != nil {
		t.Fatalf("register: %v", err)
	}
	registeredUser := registered["user"].(UserResponse)
	defer service.RevokeAllRefresh(context.Background(), registeredUser.ID)
	var stored User
	if err := tx.Where("id = ?", registeredUser.ID).First(&stored).Error; err != nil {
		t.Fatalf("load registered user: %v", err)
	}
	if stored.BoundAgentID == nil || *stored.BoundAgentID != inviter.ID {
		t.Fatalf("bound_agent_id = %v, want %d", stored.BoundAgentID, inviter.ID)
	}
	var click AffiliateClick
	if err := tx.Where("aff_code = ?", inviteCode).First(&click).Error; err != nil || click.RegisteredUserID == nil || *click.RegisteredUserID != stored.ID {
		t.Fatalf("affiliate conversion = %#v error=%v", click, err)
	}

	passwordLogin, err := service.Login(ctx, LoginRequest{Email: email, Password: password}, false)
	if err != nil {
		t.Fatalf("password login: %v", err)
	}
	passwordPair := identityIntegrationPair(t, passwordLogin)
	if _, err := service.CurrentUser(ctx, passwordPair.AccessToken); err != nil {
		t.Fatalf("current user: %v", err)
	}

	if err := service.saveCode(ctx, "email_login", email, "234567"); err != nil {
		t.Fatalf("save email login code: %v", err)
	}
	codeLogin, err := service.LoginByCode(ctx, LoginByCodeRequest{Email: email, Code: "234567"})
	if err != nil {
		t.Fatalf("email code login: %v", err)
	}
	codePair := identityIntegrationPair(t, codeLogin)
	rotated, err := service.Refresh(ctx, codePair.RefreshToken)
	if err != nil {
		t.Fatalf("refresh rotation: %v", err)
	}
	if _, err := service.Refresh(ctx, codePair.RefreshToken); err == nil {
		t.Fatal("old refresh token remained valid after rotation")
	}
	if err := service.Logout(ctx, rotated.RefreshToken); err != nil {
		t.Fatalf("logout: %v", err)
	}
	if _, err := service.Refresh(ctx, rotated.RefreshToken); err == nil {
		t.Fatal("logged-out refresh token remained valid")
	}

	phone := "139" + fmt.Sprintf("%08d", time.Now().UnixNano()%100000000)
	if err := tx.Model(&User{}).Where("id = ?", stored.ID).Updates(map[string]any{"phone": phone, "is_phone_verified": true}).Error; err != nil {
		t.Fatalf("set phone: %v", err)
	}
	if err := service.saveCode(ctx, "sms_login", phone, "345678"); err != nil {
		t.Fatalf("save sms login code: %v", err)
	}
	if _, err := service.LoginBySMS(ctx, LoginBySMSRequest{Phone: phone, Code: "345678"}); err != nil {
		t.Fatalf("sms code login: %v", err)
	}

	banLogin, err := service.Login(ctx, LoginRequest{Email: email, Password: password}, false)
	if err != nil {
		t.Fatalf("login before ban: %v", err)
	}
	banPair := identityIntegrationPair(t, banLogin)
	if err := tx.Model(&User{}).Where("id = ?", stored.ID).Update("status", "banned").Error; err != nil {
		t.Fatalf("ban user: %v", err)
	}
	if _, err := service.CurrentUser(ctx, banPair.AccessToken); err == nil {
		t.Fatal("access token remained usable after ban")
	}
	if _, err := service.Refresh(ctx, banPair.RefreshToken); err == nil {
		t.Fatal("refresh token remained usable after ban")
	}
	if err := tx.Model(&User{}).Where("id = ?", stored.ID).Update("status", "active").Error; err != nil {
		t.Fatalf("reactivate user: %v", err)
	}

	changeLogin, err := service.Login(ctx, LoginRequest{Email: email, Password: password}, false)
	if err != nil {
		t.Fatalf("login before password change: %v", err)
	}
	changePair := identityIntegrationPair(t, changeLogin)
	if err := tx.Where("id = ?", stored.ID).First(&stored).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	newPassword := "ChangedPass456!"
	if _, err := service.ChangePassword(ctx, stored, PasswordChangeRequest{OldPassword: &password, NewPassword: newPassword}); err != nil {
		t.Fatalf("change password: %v", err)
	}
	if _, err := service.Refresh(ctx, changePair.RefreshToken); err == nil {
		t.Fatal("refresh token remained valid after password change")
	}
	if _, err := service.Login(ctx, LoginRequest{Email: email, Password: password}, false); err == nil {
		t.Fatal("old password remained valid")
	}
	if _, err := service.Login(ctx, LoginRequest{Email: email, Password: newPassword}, false); err != nil {
		t.Fatalf("new password login: %v", err)
	}
}

func identityIntegrationPair(t *testing.T, result map[string]any) auth.TokenPair {
	t.Helper()
	pair, ok := result["tokens"].(auth.TokenPair)
	if !ok || pair.AccessToken == "" || pair.RefreshToken == "" {
		t.Fatalf("tokens = %#v", result["tokens"])
	}
	return pair
}

func firstIntegrationEnv(keys ...string) string {
	for _, key := range keys {
		if value := os.Getenv(key); value != "" {
			return value
		}
	}
	return ""
}

func integrationEnvDefault(fallback string, keys ...string) string {
	if value := firstIntegrationEnv(keys...); value != "" {
		return value
	}
	return fallback
}
