// Package grpc_test 验证 user-service gRPC 层的错误 reason 映射。
package grpc_test

import (
	"context"
	"testing"
	"time"

	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	authservice "hyapp/services/user-service/internal/service/auth"
	userservice "hyapp/services/user-service/internal/service/user"
	"hyapp/services/user-service/internal/testutil/mysqltest"
	grpcserver "hyapp/services/user-service/internal/transport/grpc"
)

const testAccessTokenTTLSec int64 = 604800

func newUserService(repository *mysqltest.Repository, options ...userservice.Option) *userservice.Service {
	base := []userservice.Option{
		userservice.WithIdentityRepository(repository),
		userservice.WithCountryRegionRepository(repository),
		userservice.WithCountryAdminRepository(repository),
		userservice.WithRegionAdminRepository(repository),
		userservice.WithRegionRebuildRepository(repository),
		userservice.WithDeviceRepository(repository),
	}
	return userservice.New(repository, append(base, options...)...)
}

func seedCountry(t *testing.T, repository *mysqltest.Repository, code string) {
	t.Helper()
	if _, ok, err := repository.ResolveEnabledCountryByCode(context.Background(), code); err != nil {
		t.Fatalf("resolve country %s failed: %v", code, err)
	} else if ok {
		return
	}
	repository.PutCountry(userdomain.Country{
		CountryName:        code + " Name",
		CountryCode:        code,
		CountryDisplayName: code + " Display",
		Enabled:            true,
	})
}

// TestLoginPasswordErrorCarriesReason 验证 transport 层把领域错误转换为 ErrorInfo.reason。
func TestLoginPasswordErrorCarriesReason(t *testing.T) {
	// Auth service 没有完整输入时返回 INVALID_ARGUMENT，transport 必须转成 gRPC ErrorInfo.reason。
	server := grpcserver.NewServer(authservice.New(authservice.Config{}), newUserService(mysqltest.NewRepository(t)))

	_, err := server.LoginPassword(context.Background(), &userv1.LoginPasswordRequest{
		Meta: &userv1.RequestMeta{DeviceId: "ios"},
	})
	if got := xerr.ReasonFromGRPC(err); got != xerr.InvalidArgument {
		t.Fatalf("reason mismatch: got %s", got)
	}
}

func TestLoginThirdPartyMapsRegistrationProfile(t *testing.T) {
	// gRPC 层负责把 request 字段和服务端 meta 收敛成 auth service 的注册快照。
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedCountry(t, repository, "CN")
	seedCountry(t, repository, "US")
	authSvc := authservice.New(authservice.Config{
		Issuer:             "hyapp-test",
		AccessTokenTTLSec:  testAccessTokenTTLSec,
		RefreshTokenTTLSec: 2592000,
		SigningAlg:         "HS256",
		SigningSecret:      "test-secret",
	}, authservice.WithAuthRepository(repository),
		authservice.WithUserRepository(repository),
		authservice.WithIdentityRepository(repository),
		authservice.WithCountryRegionRepository(repository), authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier([]string{"wechat"})))
	server := grpcserver.NewServer(authSvc, newUserService(repository))
	if err := repository.CreateUserWithIdentity(ctx, userdomain.User{
		AppCode:                  "lalu",
		UserID:                   800001,
		DefaultDisplayUserID:     "188001",
		CurrentDisplayUserID:     "188001",
		CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
		Status:                   userdomain.StatusActive,
		OnboardingStatus:         userdomain.OnboardingStatusProfileRequired,
		CreatedAtMs:              1000,
		UpdatedAtMs:              1000,
	}, userdomain.Identity{
		AppCode:              "lalu",
		UserID:               800001,
		DisplayUserID:        "188001",
		DefaultDisplayUserID: "188001",
		DisplayUserIDKind:    userdomain.DisplayUserIDKindDefault,
		Status:               userdomain.DisplayUserIDStatusActive,
	}); err != nil {
		t.Fatalf("seed inviter failed: %v", err)
	}
	inviter, err := repository.GetUser(ctx, 800001)
	if err != nil {
		t.Fatalf("get inviter failed: %v", err)
	}

	resp, err := server.LoginThirdParty(ctx, &userv1.LoginThirdPartyRequest{
		Meta:           &userv1.RequestMeta{RequestId: "req-third", DeviceId: "meta-device", ClientIp: "203.0.113.10", UserAgent: "hyapp-test/1.0", CountryByIp: "US"},
		Provider:       "wechat",
		Credential:     "openid-grpc",
		Username:       "hy",
		Gender:         "male",
		Country:        "CN",
		InviteCode:     inviter.InviteOverview.MyInviteCode,
		DeviceId:       "req-device",
		Device:         "iPhone 15",
		OsVersion:      "iOS 18.1",
		Avatar:         "https://cdn.example/avatar.png",
		Birth:          "2000-01-02",
		AppVersion:     "1.2.3",
		BuildNumber:    "123",
		Source:         "campaign-a",
		InstallChannel: "app_store",
		Campaign:       "spring_launch",
		Platform:       "ios",
		Language:       "zh-CN",
		Timezone:       "America/Los_Angeles",
	})
	if err != nil {
		t.Fatalf("LoginThirdParty failed: %v", err)
	}
	if resp.GetToken().GetExpiresInSec() != testAccessTokenTTLSec {
		t.Fatalf("login third-party access token TTL mismatch: %+v", resp.GetToken())
	}

	user, err := repository.GetUser(ctx, resp.GetToken().GetUserId())
	if err != nil {
		t.Fatalf("GetUser failed: %v", err)
	}
	if user.Username != "hy" || user.InviteCode != inviter.InviteOverview.MyInviteCode || user.RegisterIP != "203.0.113.10" || user.RegisterUserAgent != "hyapp-test/1.0" || user.CountryByIP != "US" || user.RegisterDeviceID != "req-device" {
		t.Fatalf("registration fields were not mapped: %+v", user)
	}
	if user.RegisterDevice != "iPhone 15" || user.RegisterOSVersion != "iOS 18.1" || user.BirthDate != "2000-01-02" || user.RegisterAppVersion != "1.2.3" || user.RegisterBuildNumber != "123" || user.RegisterInstallChannel != "app_store" || user.RegisterCampaign != "spring_launch" || user.RegisterPlatform != "ios" || user.Timezone != "America/Los_Angeles" {
		t.Fatalf("profile fields were not mapped: %+v", user)
	}
	var auditedAppVersion string
	var auditedBuildNumber string
	if err := repository.RawDB().QueryRowContext(ctx, `
		SELECT app_version, build_number
		FROM login_audit
		WHERE app_code = 'lalu' AND request_id = 'req-third' AND user_id = ?
		  AND login_type = 'third_party' AND result = 'success' AND blocked = 0
	`, resp.GetToken().GetUserId()).Scan(&auditedAppVersion, &auditedBuildNumber); err != nil {
		t.Fatalf("query third-party login client version audit failed: %v", err)
	}
	if auditedAppVersion != "1.2.3" || auditedBuildNumber != "123" {
		t.Fatalf("third-party login client version audit mismatch: app_version=%q build_number=%q", auditedAppVersion, auditedBuildNumber)
	}
}

func TestCompleteOnboardingReturnsReplacementAccessToken(t *testing.T) {
	// gRPC 完成注册响应必须直接带新 access token，避免 gateway profile gate 继续读取旧 JWT 快照。
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedCountry(t, repository, "SG")
	authSvc := authservice.New(authservice.Config{
		Issuer:             "hyapp-test",
		AccessTokenTTLSec:  testAccessTokenTTLSec,
		RefreshTokenTTLSec: 2592000,
		SigningAlg:         "HS256",
		SigningSecret:      "test-secret",
	}, authservice.WithAuthRepository(repository),
		authservice.WithUserRepository(repository),
		authservice.WithIdentityRepository(repository),
		authservice.WithCountryRegionRepository(repository),
		authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier([]string{"wechat"})))
	server := grpcserver.NewServer(authSvc, newUserService(repository))

	loginResp, err := server.LoginThirdParty(ctx, &userv1.LoginThirdPartyRequest{
		Meta:       &userv1.RequestMeta{RequestId: "req-login", DeviceId: "ios"},
		Provider:   "wechat",
		Credential: "openid-complete-token",
		Username:   "hy",
		Avatar:     "https://cdn.example/a.png",
		Gender:     "male",
		Country:    "SG",
		DeviceId:   "ios",
		Platform:   "ios",
	})
	if err != nil {
		t.Fatalf("LoginThirdParty failed: %v", err)
	}
	resp, err := server.CompleteOnboarding(ctx, &userv1.CompleteOnboardingRequest{
		Meta:     &userv1.RequestMeta{RequestId: "req-complete", SessionId: loginResp.GetToken().GetSessionId()},
		UserId:   loginResp.GetToken().GetUserId(),
		Username: "hy",
		Avatar:   "https://cdn.example/a.png",
		Gender:   "male",
		Country:  "SG",
	})
	if err != nil {
		t.Fatalf("CompleteOnboarding failed: %v", err)
	}
	if !resp.GetProfileCompleted() || resp.GetToken().GetAccessToken() == "" || !resp.GetToken().GetProfileCompleted() {
		t.Fatalf("complete onboarding must return completed replacement token: %+v", resp)
	}
	if resp.GetToken().GetExpiresInSec() != testAccessTokenTTLSec {
		t.Fatalf("complete onboarding access token TTL mismatch: %+v", resp.GetToken())
	}
	if resp.GetToken().GetSessionId() != loginResp.GetToken().GetSessionId() || resp.GetToken().GetRefreshToken() != "" {
		t.Fatalf("complete onboarding must reuse session without rotating refresh token: %+v", resp.GetToken())
	}
}

func TestUpdateUserProfileAndCountry(t *testing.T) {
	// UserService 新增资料写接口时，gRPC 层必须保留 optional 字段的“未传/清空”语义。
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	seedCountry(t, repository, "CN")
	seedCountry(t, repository, "US")
	repository.PutUser(userdomain.User{
		UserID:               10001,
		CurrentDisplayUserID: "100001",
		Country:              "CN",
		ProfileCompleted:     true,
		ProfileCompletedAtMs: 1,
		OnboardingStatus:     userdomain.OnboardingStatusCompleted,
		Status:               userdomain.StatusActive,
	})
	server := grpcserver.NewServer(authservice.New(authservice.Config{}), newUserService(repository, userservice.WithCountryChangeCooldown(0)))
	username := "hy"
	avatar := "https://cdn.example/a.png"
	gender := "female"
	birth := "2000-01-02"

	profileResp, err := server.UpdateUserProfile(ctx, &userv1.UpdateUserProfileRequest{
		UserId:   10001,
		Username: &username,
		Avatar:   &avatar,
		Gender:   &gender,
		Birth:    &birth,
	})
	if err != nil {
		t.Fatalf("UpdateUserProfile failed: %v", err)
	}
	if profileResp.GetUser().GetUsername() != "hy" || profileResp.GetUser().GetAvatar() != avatar || profileResp.GetUser().GetGender() != gender || profileResp.GetUser().GetBirth() != birth {
		t.Fatalf("profile response mismatch: %+v", profileResp.GetUser())
	}
	backgroundResp, err := server.UpdateUserProfileBackground(ctx, &userv1.UpdateUserProfileBackgroundRequest{
		UserId:       10001,
		ProfileBgImg: "https://cdn.example/profile-bg.png",
	})
	if err != nil {
		t.Fatalf("UpdateUserProfileBackground failed: %v", err)
	}
	if backgroundResp.GetUser().GetProfileBgImg() != "https://cdn.example/profile-bg.png" {
		t.Fatalf("profile background response mismatch: %+v", backgroundResp.GetUser())
	}

	countryResp, err := server.ChangeUserCountry(ctx, &userv1.ChangeUserCountryRequest{
		Meta:    &userv1.RequestMeta{RequestId: "req-country"},
		UserId:  10001,
		Country: "US",
	})
	if err != nil {
		t.Fatalf("ChangeUserCountry failed: %v", err)
	}
	if countryResp.GetUser().GetCountry() != "US" {
		t.Fatalf("country response mismatch: %+v", countryResp.GetUser())
	}
}

func TestCountryAndRegionAdminRPC(t *testing.T) {
	// user-service 只保留国家查询/更新和区域更新 RPC；国家创建/启停、区域创建由 admin-server 处理。
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	now := time.UnixMilli(1000)
	userSvc := newUserService(repository, userservice.WithClock(func() time.Time { return now }))
	server := grpcserver.NewServer(authservice.New(authservice.Config{}), userSvc)

	country := repository.PutCountry(userdomain.Country{
		CountryName:        "Example Area",
		CountryCode:        "XA",
		CountryDisplayName: "测试区域",
		Enabled:            true,
	})
	countryResp, err := server.UpdateCountry(ctx, &userv1.UpdateCountryRequest{
		Meta:               &userv1.RequestMeta{RequestId: "req-country"},
		CountryId:          country.CountryID,
		CountryName:        "Example Area",
		IsoAlpha3:          "xaa",
		IsoNumeric:         "901",
		CountryDisplayName: "测试区域",
		PhoneCountryCode:   "+901",
		Flag:               "XA",
		SortOrder:          10,
		OperatorUserId:     1,
	})
	if err != nil {
		t.Fatalf("UpdateCountry RPC failed: %v", err)
	}
	if countryResp.GetCountry().GetCountryCode() != "XA" || countryResp.GetCountry().GetIsoAlpha3() != "XAA" || countryResp.GetCountry().GetIsoNumeric() != "901" || countryResp.GetCountry().GetPhoneCountryCode() != "+901" || !countryResp.GetCountry().GetEnabled() || countryResp.GetCountry().GetUpdatedAtMs() != 1000 {
		t.Fatalf("country response mismatch: %+v", countryResp.GetCountry())
	}

	region := repository.PutRegion(userdomain.Region{
		RegionCode:      "APAC",
		Name:            "Asia Pacific",
		Status:          userdomain.RegionStatusActive,
		Countries:       []string{"XA"},
		CreatedByUserID: 1,
		UpdatedByUserID: 1,
		CreatedAtMs:     900,
		UpdatedAtMs:     900,
	})
	regionResp, err := server.UpdateRegion(ctx, &userv1.UpdateRegionRequest{
		Meta:           &userv1.RequestMeta{RequestId: "req-region"},
		RegionId:       region.RegionID,
		RegionCode:     "sea",
		Name:           "Southeast Asia",
		OperatorUserId: 1,
	})
	if err != nil {
		t.Fatalf("UpdateRegion RPC failed: %v", err)
	}
	if regionResp.GetRegion().GetRegionCode() != "SEA" || len(regionResp.GetRegion().GetCountries()) != 1 || regionResp.GetRegion().GetCountries()[0] != "XA" {
		t.Fatalf("region response mismatch: %+v", regionResp.GetRegion())
	}
}

// TestResolveDisplayUserIDErrorCarriesReason 验证短号接口也使用 gRPC ErrorInfo.reason。
func TestResolveDisplayUserIDErrorCarriesReason(t *testing.T) {
	// 短号格式错误也必须通过同一 gRPC 错误映射路径暴露稳定 reason。
	server := grpcserver.NewServer(authservice.New(authservice.Config{}), newUserService(mysqltest.NewRepository(t)))

	_, err := server.ResolveDisplayUserID(context.Background(), &userv1.ResolveDisplayUserIDRequest{
		DisplayUserId: "bad!",
	})
	if got := xerr.ReasonFromGRPC(err); got != xerr.DisplayUserIDInvalid {
		t.Fatalf("reason mismatch: got %s", got)
	}
}

func TestResolveDisplayUserIDAllowsPrettyDisplayID(t *testing.T) {
	// 真实解析链路必须允许当前 active 靓号作为入参，后台和 H5 不应该要求调用方先知道内部 user_id。
	repository := mysqltest.NewRepository(t)
	repository.PutUser(userdomain.User{
		AppCode:                  "lalu",
		UserID:                   123456,
		DefaultDisplayUserID:     "123456",
		CurrentDisplayUserID:     "123456",
		CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
		Status:                   userdomain.StatusActive,
		CreatedAtMs:              1000,
		UpdatedAtMs:              1000,
	})
	if _, err := repository.RawDB().ExecContext(context.Background(), `
		UPDATE user_display_user_ids
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND display_user_id = ?
	`, string(userdomain.DisplayUserIDStatusHeld), int64(1001), "lalu", int64(123456), "123456"); err != nil {
		t.Fatalf("mark default display_user_id held failed: %v", err)
	}
	if _, err := repository.RawDB().ExecContext(context.Background(), `
		INSERT INTO user_display_user_ids (app_code, display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, "lalu", "1111cc1111", int64(123456), string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive), int64(1001), int64(1001), int64(1001), int64(1001)); err != nil {
		t.Fatalf("seed active pretty display_user_id failed: %v", err)
	}
	server := grpcserver.NewServer(authservice.New(authservice.Config{}), newUserService(repository))

	resp, err := server.ResolveDisplayUserID(context.Background(), &userv1.ResolveDisplayUserIDRequest{
		Meta:          &userv1.RequestMeta{AppCode: "lalu", RequestId: "req-pretty-resolve"},
		DisplayUserId: "1111cc1111",
	})
	if err != nil {
		t.Fatalf("ResolveDisplayUserID should accept pretty display_user_id: %v", err)
	}
	if resp.GetIdentity().GetUserId() != 123456 || resp.GetIdentity().GetDisplayUserId() != "1111cc1111" || resp.GetIdentity().GetDisplayUserIdKind() != string(userdomain.DisplayUserIDKindPretty) {
		t.Fatalf("pretty display_user_id identity mismatch: %+v", resp.GetIdentity())
	}
	user, err := repository.GetUser(context.Background(), 123456)
	if err != nil {
		t.Fatalf("GetUser after pretty resolve failed: %v", err)
	}
	if user.CurrentDisplayUserID != "1111cc1111" || user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty {
		t.Fatalf("users current display snapshot was not repaired: %+v", user)
	}
}
