package wallet_test

import (
	"context"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
	walletservice "hyapp/services/wallet-service/internal/service/wallet"
	"hyapp/services/wallet-service/internal/testutil/mysqltest"
)

// TestVipUserSettingsDefaultPartialUpdateAndAppScope 验证设置不授予/删除权益，且无行默认、
// partial PATCH、付费/体验来源和 App 隔离都由 wallet owner 使用同一规则处理。
func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
	repository := mysqltest.NewRepository(t)
	service := walletservice.New(repository)
	famiCtx := appcode.WithContext(context.Background(), "fami")
	laluCtx := appcode.WithContext(context.Background(), "lalu")
	const paidUserID int64 = 940001

	defaultState, err := service.GetVipState(famiCtx, paidUserID)
	if err != nil {
		t.Fatalf("get default VIP settings failed: %v", err)
	}
	if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled ||
		defaultState.UserSettings.HideProfileDataEnabled || defaultState.UserSettings.AnonymousProfileVisitEnabled ||
		defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 {
		t.Fatalf("missing settings row must project notice-on privacy-off defaults: %+v", defaultState.UserSettings)
	}
	if got := repository.CountRows("user_vip_settings", "app_code = ? AND user_id = ?", "fami", paidUserID); got != 0 {
		t.Fatalf("read must not materialize default settings row, got %d", got)
	}

	roomOff := false
	settings, err := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: paidUserID, RoomEntryNoticeEnabled: &roomOff,
	})
	if err != nil || settings.RoomEntryNoticeEnabled || !settings.OnlineGlobalNoticeEnabled ||
		settings.HideProfileDataEnabled || settings.AnonymousProfileVisitEnabled || settings.LeaderboardInvisibleEnabled ||
		settings.UpdatedAtMS <= 0 {
		t.Fatalf("first partial settings update mismatch: settings=%+v err=%v", settings, err)
	}
	anonymousVisitOn := true
	settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: paidUserID, AnonymousProfileVisitEnabled: &anonymousVisitOn,
	})
	if err != nil || settings.HideProfileDataEnabled || !settings.AnonymousProfileVisitEnabled || settings.LeaderboardInvisibleEnabled {
		t.Fatalf("privacy partial update must preserve other switches: settings=%+v err=%v", settings, err)
	}
	onlineOff := false
	settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: paidUserID, OnlineGlobalNoticeEnabled: &onlineOff,
	})
	if err != nil || settings.RoomEntryNoticeEnabled || settings.OnlineGlobalNoticeEnabled {
		t.Fatalf("second partial update must preserve room switch: settings=%+v err=%v", settings, err)
	}
	laluState, err := service.GetVipState(laluCtx, paidUserID)
	if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled ||
		laluState.UserSettings.HideProfileDataEnabled || laluState.UserSettings.AnonymousProfileVisitEnabled ||
		laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 {
		t.Fatalf("same user settings must remain App scoped: state=%+v err=%v", laluState.UserSettings, err)
	}

	// 两个客户端并发修改不同字段时，partial upsert 必须串行保留双方结果。
	const concurrentUserID int64 = 940003
	startUpdates := make(chan struct{})
	updateErrors := make(chan error, 2)
	go func() {
		<-startUpdates
		_, updateErr := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
			AppCode: "fami", UserID: concurrentUserID, RoomEntryNoticeEnabled: &roomOff,
		})
		updateErrors <- updateErr
	}()
	go func() {
		<-startUpdates
		_, updateErr := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
			AppCode: "fami", UserID: concurrentUserID, OnlineGlobalNoticeEnabled: &onlineOff,
		})
		updateErrors <- updateErr
	}()
	close(startUpdates)
	if leftErr, rightErr := <-updateErrors, <-updateErrors; leftErr != nil || rightErr != nil {
		t.Fatalf("concurrent partial settings update failed: left=%v right=%v", leftErr, rightErr)
	}
	concurrentState, err := service.GetVipState(famiCtx, concurrentUserID)
	if err != nil || concurrentState.UserSettings.RoomEntryNoticeEnabled || concurrentState.UserSettings.OnlineGlobalNoticeEnabled ||
		concurrentState.UserSettings.HideProfileDataEnabled || concurrentState.UserSettings.AnonymousProfileVisitEnabled ||
		concurrentState.UserSettings.LeaderboardInvisibleEnabled {
		t.Fatalf("concurrent partial updates overwrote each other: settings=%+v err=%v", concurrentState.UserSettings, err)
	}

	// 设置可在无 VIP 时保存，但 CheckVipBenefit 仍优先返回 vip_inactive，不能让偏好变成授权。
	inactiveCheck, err := service.CheckVipBenefit(famiCtx, paidUserID, ledger.VipBenefitCodeRoomEntryNotice)
	if err != nil || inactiveCheck.Allowed || inactiveCheck.DenialReason != "vip_inactive" {
		t.Fatalf("settings must not grant VIP benefit: result=%+v err=%v", inactiveCheck, err)
	}

	now := time.Now().UTC()
	repository.SeedPaidVIPAtWindow("fami", paidUserID, 9, now.Add(-time.Hour).UnixMilli(), now.Add(time.Hour).UnixMilli())
	paidState, err := service.GetVipState(famiCtx, paidUserID)
	if err != nil || paidState.EffectiveSource != ledger.VipEffectiveSourcePaid {
		t.Fatalf("get paid VIP state failed: state=%+v err=%v", paidState, err)
	}
	if !containsVipBenefit(paidState.EffectiveBenefits, ledger.VipBenefitCodeRoomEntryNotice) || !containsVipBenefit(paidState.EffectiveBenefits, ledger.VipBenefitCodeOnlineGlobalNotice) {
		t.Fatalf("disabled user settings must not delete owned effective benefits: %+v", paidState.EffectiveBenefits)
	}
	for _, code := range []string{ledger.VipBenefitCodeRoomEntryNotice, ledger.VipBenefitCodeOnlineGlobalNotice} {
		result, checkErr := service.CheckVipBenefit(famiCtx, paidUserID, code)
		if checkErr != nil || result.Allowed || result.DenialReason != "user_setting_disabled" || result.Benefit.BenefitCode != code {
			t.Fatalf("paid VIP user setting gate mismatch for %s: result=%+v err=%v", code, result, checkErr)
		}
	}
	hideProfileCheck, err := service.CheckVipBenefit(famiCtx, paidUserID, ledger.VipBenefitCodeHideProfileData)
	if err != nil || hideProfileCheck.Allowed || hideProfileCheck.DenialReason != "user_setting_disabled" {
		t.Fatalf("hide profile setting gate mismatch: result=%+v err=%v", hideProfileCheck, err)
	}
	roomOn := true
	settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: paidUserID, RoomEntryNoticeEnabled: &roomOn,
	})
	if err != nil || !settings.RoomEntryNoticeEnabled || settings.OnlineGlobalNoticeEnabled {
		t.Fatalf("partial re-enable must preserve online=false: settings=%+v err=%v", settings, err)
	}
	roomCheck, err := service.CheckVipBenefit(famiCtx, paidUserID, ledger.VipBenefitCodeRoomEntryNotice)
	if err != nil || !roomCheck.Allowed {
		t.Fatalf("enabled paid room entry benefit must be allowed: result=%+v err=%v", roomCheck, err)
	}
	leaderboardInvisibleOn := true
	if _, err := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: paidUserID, LeaderboardInvisibleEnabled: &leaderboardInvisibleOn,
	}); err != nil {
		t.Fatalf("enable paid leaderboard privacy failed: %v", err)
	}
	presentation, err := service.BatchGetUserEquippedResources(famiCtx, resourcedomain.BatchGetUserEquippedResourcesQuery{
		AppCode: "fami", UserIDs: []int64{paidUserID, concurrentUserID},
		ResourceTypes: []string{resourcedomain.TypeBadge}, VIPBenefitCodes: []string{ledger.VipBenefitCodeLeaderboardInvisible},
	})
	if err != nil {
		t.Fatalf("batch evaluate leaderboard privacy failed: %v", err)
	}
	if len(presentation) != 2 || presentation[0].EffectiveVIPLevel != 9 || presentation[1].EffectiveVIPLevel != 0 ||
		len(presentation[0].AllowedVIPBenefitCodes) != 1 || presentation[0].AllowedVIPBenefitCodes[0] != ledger.VipBenefitCodeLeaderboardInvisible || len(presentation[1].AllowedVIPBenefitCodes) != 0 {
		t.Fatalf("batch presentation must grant only the active VIP with its enabled setting: %+v", presentation)
	}

	// 体验卡与付费 VIP 共用同一设置门禁；佩戴后 effective_benefits 仍保留资格，执行结果为 disabled。
	const trialUserID int64 = 940002
	repository.SetVIPLevelStatusForApp("fami", 8, ledger.VipStatusActive, 100)
	grant, err := service.GrantVipTrialCard(famiCtx, ledger.GrantVipTrialCardCommand{
		AppCode: "fami", CommandID: "settings-trial-grant", TargetUserID: trialUserID,
		Level: 8, DurationMS: int64(24 * time.Hour / time.Millisecond),
		GrantSource: ledger.VipGrantSourceAdmin, OperatorUserID: 9001, Reason: "settings test",
	})
	if err != nil {
		t.Fatalf("grant trial VIP for settings test failed: %v", err)
	}
	if _, _, err := service.EquipVipTrialCard(famiCtx, ledger.EquipVipTrialCardCommand{
		AppCode: "fami", RequestID: "settings-trial-equip", UserID: trialUserID,
		EntitlementID: grant.TrialCard.EntitlementID,
	}); err != nil {
		t.Fatalf("equip trial VIP for settings test failed: %v", err)
	}
	if _, err := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
		AppCode: "fami", UserID: trialUserID, RoomEntryNoticeEnabled: &roomOff,
	}); err != nil {
		t.Fatalf("disable trial user room entry setting failed: %v", err)
	}
	trialCheck, err := service.CheckVipBenefit(famiCtx, trialUserID, ledger.VipBenefitCodeRoomEntryNotice)
	if err != nil || trialCheck.Allowed || trialCheck.DenialReason != "user_setting_disabled" || trialCheck.State.EffectiveSource != ledger.VipEffectiveSourceTrial || !containsVipBenefit(trialCheck.State.EffectiveBenefits, ledger.VipBenefitCodeRoomEntryNotice) {
		t.Fatalf("trial VIP user setting gate mismatch: result=%+v err=%v", trialCheck, err)
	}
	trialPresentation, err := service.BatchGetUserEquippedResources(famiCtx, resourcedomain.BatchGetUserEquippedResourcesQuery{
		AppCode: "fami", UserIDs: []int64{trialUserID},
		VIPBenefitCodes: []string{ledger.VipBenefitCodeColoredNickname, ledger.VipBenefitCodeColoredID, ledger.VipBenefitCodeColoredRoomName},
	})
	if err != nil {
		t.Fatalf("batch evaluate trial VIP colorful identity failed: %v", err)
	}
	if len(trialPresentation) != 1 || trialPresentation[0].EffectiveVIPLevel != 8 ||
		len(trialPresentation[0].AllowedVIPBenefitCodes) != 3 {
		t.Fatalf("batch presentation must expose equipped trial VIP level and colorful identity benefits: %+v", trialPresentation)
	}
}

// TestVipEquippedResourceProjectsBenefitCode 锁定同为 badge 类型的资源仍携带精确 VIP 权益编码，
// gateway/Flutter 才能把 vip_badge_identity 放进身份行而不是普通长徽章行。
func TestVipEquippedResourceProjectsBenefitCode(t *testing.T) {
	repository := mysqltest.NewRepository(t)
	service := walletservice.New(repository)
	ctx := appcode.WithContext(context.Background(), "fami")
	const userID int64 = 940101

	badge, err := service.CreateResource(ctx, resourcedomain.ResourceCommand{
		ResourceCode:   "vip1_identity_projection",
		ResourceType:   resourcedomain.TypeBadge,
		Name:           "VIP1 Identity Projection",
		Status:         resourcedomain.StatusActive,
		Grantable:      true,
		GrantStrategy:  resourcedomain.GrantStrategySetActiveFlag,
		UsageScopes:    []string{"profile"},
		AssetURL:       "https://cdn.example/vip1-identity.png",
		MetadataJSON:   `{"badge_form":"strip","badge_kind":"normal"}`,
		OperatorUserID: 90001,
	})
	if err != nil {
		t.Fatalf("create VIP identity badge failed: %v", err)
	}
	repository.BindVIPBenefitResourceForApp(
		"fami", 1, ledger.VipBenefitCodeBadgeIdentity,
		badge.ResourceID, resourcedomain.TypeBadge, true,
	)
	repository.SetVIPLevelStatusForApp("fami", 1, ledger.VipStatusActive, 100)
	repository.SetBalanceForApp("fami", userID, 1000)
	if _, err := service.PurchaseVip(ctx, ledger.PurchaseVipCommand{
		CommandID: "vip-identity-projection", UserID: userID, Level: 1,
	}); err != nil {
		t.Fatalf("purchase VIP1 failed: %v", err)
	}

	items, err := service.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
		AppCode: "fami", UserIDs: []int64{userID}, ResourceTypes: []string{resourcedomain.TypeBadge},
	})
	if err != nil {
		t.Fatalf("batch get VIP identity equipment failed: %v", err)
	}
	if len(items) != 1 || len(items[0].Resources) != 1 ||
		len(items[0].Resources[0].VIPBenefitCodes) != 1 ||
		items[0].Resources[0].VIPBenefitCodes[0] != ledger.VipBenefitCodeBadgeIdentity {
		t.Fatalf("VIP identity resource benefit projection mismatch: %+v", items)
	}
}

func containsVipBenefit(items []ledger.VipBenefit, code string) bool {
	for _, item := range items {
		if item.BenefitCode == code {
			return true
		}
	}
	return false
}
