package user

import (
	"context"
	"errors"
	"testing"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	invitedomain "hyapp/services/user-service/internal/domain/invite"
	userdomain "hyapp/services/user-service/internal/domain/user"
	hostservice "hyapp/services/user-service/internal/service/host"
)

func TestSetUserStatusBanRevokesSessionsAndKicksRealtimeSurfaces(t *testing.T) {
	repository := &fakeModerationRepository{
		user: userdomain.User{
			AppCode: "lalu",
			UserID:  10001,
			Status:  userdomain.StatusBanned,
		},
		sessionIDs: []string{"sess_a", "sess_b"},
	}
	denylist := &fakeSessionDenylist{}
	im := &fakeIMLoginKicker{}
	room := &fakeRoomEvictor{resp: &roomv1.SystemEvictUserResponse{
		HadCurrentRoom: true,
		Result:         &roomv1.CommandResult{Applied: true, RoomVersion: 7},
		RoomId:         "room_1001",
		RtcKicked:      true,
	}}
	svc := New(repository,
		WithModerationRepository(repository),
		WithSessionDenylist(denylist, time.Minute),
		WithIMLoginKicker(im),
		WithRoomEvictor(room),
		WithClock(func() time.Time { return time.UnixMilli(1_700_000_000_000).UTC() }),
	)

	result, err := svc.SetUserStatus(appcode.WithContext(context.Background(), "lalu"), UserStatusCommand{
		AppCode:        "lalu",
		TargetUserID:   10001,
		Status:         userdomain.StatusBanned,
		OperatorType:   OperatorTypeAdmin,
		OperatorUserID: 9001,
		RequestID:      "req-ban",
	})
	if err != nil {
		t.Fatalf("SetUserStatus failed: %v", err)
	}

	if repository.lastCommand.Status != userdomain.StatusBanned || repository.lastCommand.OperatorType != OperatorTypeAdmin || repository.lastCommand.NowMs != 1_700_000_000_000 {
		t.Fatalf("unexpected repository command: %+v", repository.lastCommand)
	}
	if len(denylist.sessions) != 2 || denylist.sessions[0] != "sess_a" || denylist.sessions[1] != "sess_b" {
		t.Fatalf("denylist sessions mismatch: %+v", denylist.sessions)
	}
	if im.userID != 10001 {
		t.Fatalf("im kick target mismatch: %d", im.userID)
	}
	if room.req.GetTargetUserId() != 10001 || !room.req.GetBanFromRoom() || room.req.GetMeta().GetAppCode() != "lalu" {
		t.Fatalf("room eviction request mismatch: %+v", room.req)
	}
	if !result.AccessTokenRevoked || result.AccessTokenError != "" {
		t.Fatalf("access token revoke result mismatch: %+v", result)
	}
	if !result.IMKicked || !result.RoomEvicted || !result.RTCKicked || result.RoomID != "room_1001" {
		t.Fatalf("unexpected moderation result: %+v", result)
	}
}

func TestSetUserStatusSurfacesAccessDenylistError(t *testing.T) {
	repository := &fakeModerationRepository{
		user: userdomain.User{
			AppCode: "lalu",
			UserID:  10001,
			Status:  userdomain.StatusBanned,
		},
		sessionIDs: []string{"sess_a"},
	}
	denylist := &fakeSessionDenylist{err: errors.New("redis unavailable")}
	svc := New(repository,
		WithModerationRepository(repository),
		WithSessionDenylist(denylist, time.Minute),
	)

	result, err := svc.SetUserStatus(appcode.WithContext(context.Background(), "lalu"), UserStatusCommand{
		AppCode:      "lalu",
		TargetUserID: 10001,
		Status:       userdomain.StatusBanned,
		RequestID:    "req-ban",
	})
	if err != nil {
		t.Fatalf("SetUserStatus failed: %v", err)
	}

	if result.AccessTokenRevoked || result.AccessTokenError != "redis unavailable" {
		t.Fatalf("denylist error must be surfaced for retry: %+v", result)
	}
	if result.RevokedSessionCount != 1 {
		t.Fatalf("database revocation fact mismatch: %+v", result)
	}
}

func TestCreateManagerUserBlockUsesGeneralCrossRegionScope(t *testing.T) {
	for _, testCase := range []struct {
		name         string
		policy       hostservice.RoleScopePolicy
		crossRegion  bool
		targetRegion int64
		wantAllowed  bool
	}{
		{name: "country scope denied", policy: managerRoleScopePolicy(hostservice.RoleScopeCountry, hostservice.RoleScopeRegion, true), targetRegion: 1, wantAllowed: false},
		{name: "expanded region allows same region", policy: managerRoleScopePolicy(hostservice.RoleScopeCountry, hostservice.RoleScopeRegion, true), crossRegion: true, targetRegion: 1, wantAllowed: true},
		{name: "expanded region denies another region", policy: managerRoleScopePolicy(hostservice.RoleScopeCountry, hostservice.RoleScopeRegion, true), crossRegion: true, targetRegion: 2, wantAllowed: false},
		{name: "global owner policy allows all regions", policy: managerRoleScopePolicy(hostservice.RoleScopeGlobal, hostservice.RoleScopeGlobal, false), targetRegion: 2, wantAllowed: true},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			repository := &fakeModerationRepository{
				managerCrossRegionEnabled: testCase.crossRegion,
				users: map[int64]userdomain.User{
					100: {UserID: 100, Country: "US", RegionID: 1, Status: userdomain.StatusActive},
					200: {UserID: 200, Country: "CA", RegionID: testCase.targetRegion, Status: userdomain.StatusActive},
				},
			}
			svc := New(repository,
				WithModerationRepository(repository),
				WithRoleScopePolicyResolver(staticRoleScopePolicyResolver{policy: testCase.policy}),
				WithClock(func() time.Time { return time.UnixMilli(1_000).UTC() }),
			)
			_, _, err := svc.CreateManagerUserBlock(appcode.WithContext(context.Background(), "tenant-a"), ManagerUserBlockCommand{
				CommandID: "manager-block", ManagerUserID: 100, TargetUserID: 200, BlockedUntilMS: 2_000,
			})
			if testCase.wantAllowed && err != nil {
				t.Fatalf("cross-region block should be allowed: %v", err)
			}
			if !testCase.wantAllowed && !xerr.IsCode(err, xerr.PermissionDenied) {
				t.Fatalf("out-of-scope block should be denied: %v", err)
			}
		})
	}
}

type staticRoleScopePolicyResolver struct {
	policy hostservice.RoleScopePolicy
	err    error
}

func (r staticRoleScopePolicyResolver) ResolveRoleScopePolicy(context.Context, string) (hostservice.RoleScopePolicy, error) {
	return r.policy, r.err
}

func managerRoleScopePolicy(base hostservice.RoleScope, expanded hostservice.RoleScope, configurable bool) hostservice.RoleScopePolicy {
	return hostservice.RoleScopePolicy{
		Scene:                       hostservice.RoleScopeSceneManagerOperations,
		BaseScope:                   base,
		ExpandedScope:               expanded,
		RegionExpansionConfigurable: configurable,
	}
}

func TestAdminBanUserAcceptsPermanentBanAndRunsRealtimeSideEffectsOnce(t *testing.T) {
	repository := &fakeModerationRepository{
		user:       userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned},
		sessionIDs: []string{"sess_admin"},
	}
	denylist := &fakeSessionDenylist{}
	im := &fakeIMLoginKicker{}
	room := &fakeRoomEvictor{resp: &roomv1.SystemEvictUserResponse{Result: &roomv1.CommandResult{Applied: true}}}
	svc := New(repository,
		WithModerationRepository(repository),
		WithSessionDenylist(denylist, time.Minute),
		WithIMLoginKicker(im),
		WithRoomEvictor(room),
		WithClock(func() time.Time { return time.UnixMilli(1_700_000_000_000).UTC() }),
	)

	ban, result, err := svc.AdminBanUser(appcode.WithContext(context.Background(), "lalu"), AdminUserBanCommand{
		CommandID:       "admin-ban-command",
		TargetUserID:    10001,
		ExpiresAtMs:     0,
		OperatorAdminID: 9001,
		RequestID:       "req-admin-ban",
		NowMs:           1,
	})
	if err != nil {
		t.Fatalf("AdminBanUser failed: %v", err)
	}
	if ban.ExpiresAtMs != 0 || ban.Status != AdminUserBanStatusActive {
		t.Fatalf("permanent admin ban mismatch: %+v", ban)
	}
	if len(denylist.sessions) != 1 || denylist.sessions[0] != "sess_admin" || im.userID != 10001 || room.req.GetTargetUserId() != 10001 {
		t.Fatalf("admin ban side effects mismatch: denylist=%+v im=%d room=%+v", denylist.sessions, im.userID, room.req)
	}
	if !result.AccessTokenRevoked || result.RevokedSessionCount != 1 {
		t.Fatalf("admin ban status result mismatch: %+v", result)
	}
	if repository.lastAdminBanCommand.NowMs != 1_700_000_000_000 {
		t.Fatalf("user-service owner clock must override caller sent_at_ms: %+v", repository.lastAdminBanCommand)
	}
}

func TestAdminUnbanUserUsesOwnerClock(t *testing.T) {
	repository := &fakeModerationRepository{adminForceUnbanResult: AdminForceUnbanResult{
		User:                      userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusActive},
		ReleasedManagerBlockCount: 1,
		StatusRestored:            true,
	}}
	svc := New(repository,
		WithModerationRepository(repository),
		WithClock(func() time.Time { return time.UnixMilli(1_700_000_000_000).UTC() }),
	)
	release, _, err := svc.AdminUnbanUser(context.Background(), AdminForceUnbanCommand{
		TargetUserID: 10001, OperatorAdminID: 9001, Reason: "manual",
		RequestID: "unban-owner-clock", NowMs: 1,
	})
	if err != nil {
		t.Fatalf("AdminUnbanUser failed: %v", err)
	}
	if repository.lastAdminForceUnbanCommand.NowMs != 1_700_000_000_000 {
		t.Fatalf("unban must use user-service owner clock: %+v", repository.lastAdminForceUnbanCommand)
	}
	if release.ReleasedManagerBlockCount != 1 || !release.StatusRestored {
		t.Fatalf("force unban result mismatch: %+v", release)
	}
}

func TestAdminBanUserDoesNotRepeatSideEffectsForOverlappingBan(t *testing.T) {
	repository := &fakeModerationRepository{
		user: userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned},
		adminBanPersistence: AdminUserBanPersistenceResult{
			Ban:           AdminUserBan{BanID: "admin-ban-2", TargetUserID: 10001, Status: AdminUserBanStatusActive},
			Status:        UserStatusPersistenceResult{User: userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned}},
			StatusChanged: false,
		},
	}
	im := &fakeIMLoginKicker{}
	room := &fakeRoomEvictor{}
	svc := New(repository, WithModerationRepository(repository), WithIMLoginKicker(im), WithRoomEvictor(room))

	_, result, err := svc.AdminBanUser(appcode.WithContext(context.Background(), "lalu"), AdminUserBanCommand{
		CommandID:       "overlap",
		TargetUserID:    10001,
		OperatorAdminID: 9002,
	})
	if err != nil {
		t.Fatalf("AdminBanUser overlap failed: %v", err)
	}
	if im.userID != 0 || room.req != nil {
		t.Fatalf("overlapping ban must not repeat realtime side effects: im=%d room=%+v", im.userID, room.req)
	}
	if result.User.Status != userdomain.StatusBanned {
		t.Fatalf("overlapping ban must preserve banned projection: %+v", result)
	}
}

func TestAdminBanUserRejectsExpiredDeadline(t *testing.T) {
	repository := &fakeModerationRepository{}
	svc := New(repository, WithModerationRepository(repository), WithClock(func() time.Time {
		return time.UnixMilli(1_700_000_000_000).UTC()
	}))

	_, _, err := svc.AdminBanUser(context.Background(), AdminUserBanCommand{
		CommandID:       "expired",
		TargetUserID:    10001,
		OperatorAdminID: 9001,
		ExpiresAtMs:     1_700_000_000_000,
	})
	if xerr.CodeOf(err) != xerr.InvalidArgument {
		t.Fatalf("expired deadline must be rejected, err=%v", err)
	}
}

func TestBatchGetUserAdminProfilesDeduplicatesAndLimitsInput(t *testing.T) {
	repository := &fakeModerationRepository{adminProfiles: map[int64]userdomain.AdminProfile{
		10001: {User: userdomain.User{UserID: 10001}},
	}}
	svc := New(repository)

	profiles, err := svc.BatchGetUserAdminProfiles(context.Background(), []int64{10001, 10001})
	if err != nil || profiles[10001].User.UserID != 10001 {
		t.Fatalf("batch admin profile failed: profiles=%+v err=%v", profiles, err)
	}
	tooMany := make([]int64, maxAdminProfileBatchSize+1)
	for index := range tooMany {
		tooMany[index] = int64(index + 1)
	}
	if _, err := svc.BatchGetUserAdminProfiles(context.Background(), tooMany); xerr.CodeOf(err) != xerr.InvalidArgument {
		t.Fatalf("oversized admin profile batch must be rejected, err=%v", err)
	}
}

type fakeModerationRepository struct {
	user                       userdomain.User
	users                      map[int64]userdomain.User
	inviteReferrer             userdomain.User
	sessionIDs                 []string
	countrySessionIDs          []string
	lastCommand                UserStatusCommand
	lastAdminBanCommand        AdminUserBanCommand
	lastAdminBanReleaseCommand ReleaseAdminUserBanCommand
	lastAdminForceUnbanCommand AdminForceUnbanCommand
	lastProfileCommand         userdomain.ProfileUpdateCommand
	lastWithdrawAddressCommand userdomain.WithdrawAddressUpdateCommand
	lastCountryCommand         userdomain.CountryChangeCommand
	lastFindAppCode            string
	lastFindInviteCode         string
	lastBindInvite             userdomain.BindInviteReferrerCommand
	countryChangeCalls         int
	lastReport                 userdomain.Report
	adminProfiles              map[int64]userdomain.AdminProfile
	adminBanPersistence        AdminUserBanPersistenceResult
	adminBanRelease            AdminUserBanRelease
	adminForceUnbanResult      AdminForceUnbanResult
	adminBanExpirations        []AdminUserBanRelease
	managerCrossRegionEnabled  bool
}

func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error) {
	r.lastCommand = command
	r.user.Status = command.Status
	return UserStatusPersistenceResult{User: r.user, RevokedSessionIDs: append([]string{}, r.sessionIDs...)}, nil
}

func (r *fakeModerationRepository) ManagerCrossRegionEnabled(context.Context, int64) (bool, error) {
	return r.managerCrossRegionEnabled, nil
}

func (r *fakeModerationRepository) GetInviteAttribution(context.Context, int64) (invitedomain.Attribution, error) {
	return invitedomain.Attribution{}, nil
}

func (r *fakeModerationRepository) CreateManagerUserBlock(_ context.Context, command ManagerUserBlockCommand, target userdomain.User) (ManagerUserBlock, error) {
	return ManagerUserBlock{
		BlockID:             "block-1",
		ManagerUserID:       command.ManagerUserID,
		TargetUserID:        command.TargetUserID,
		TargetDisplayUserID: target.CurrentDisplayUserID,
		TargetUsername:      target.Username,
		TargetAvatar:        target.Avatar,
		Country:             target.Country,
		BlockedUntilMS:      command.BlockedUntilMS,
		Status:              ManagerUserBlockStatusActive,
		Reason:              command.Reason,
	}, nil
}

func (r *fakeModerationRepository) ListManagerUserBlocks(context.Context, int64, string, int32) ([]ManagerUserBlock, error) {
	return nil, nil
}

func (r *fakeModerationRepository) ReleaseManagerUserBlock(context.Context, ReleaseManagerUserBlockCommand) (ManagerUserBlockRelease, error) {
	return ManagerUserBlockRelease{}, nil
}

func (r *fakeModerationRepository) ExpireManagerUserBlocks(context.Context, int64, int32) ([]ManagerUserBlockRelease, error) {
	return nil, nil
}

func (r *fakeModerationRepository) CreateAdminUserBan(_ context.Context, command AdminUserBanCommand) (AdminUserBanPersistenceResult, error) {
	r.lastAdminBanCommand = command
	if r.adminBanPersistence.Ban.BanID == "" {
		r.adminBanPersistence = AdminUserBanPersistenceResult{
			Ban: AdminUserBan{
				BanID:           "admin-ban-1",
				CommandID:       command.CommandID,
				TargetUserID:    command.TargetUserID,
				ExpiresAtMs:     command.ExpiresAtMs,
				Status:          AdminUserBanStatusActive,
				OperatorAdminID: command.OperatorAdminID,
			},
			Status:        UserStatusPersistenceResult{User: r.user, RevokedSessionIDs: append([]string{}, r.sessionIDs...)},
			StatusChanged: true,
		}
	}
	return r.adminBanPersistence, nil
}

func (r *fakeModerationRepository) ReleaseAdminUserBan(_ context.Context, command ReleaseAdminUserBanCommand) (AdminUserBanRelease, error) {
	r.lastAdminBanReleaseCommand = command
	return r.adminBanRelease, nil
}

func (r *fakeModerationRepository) ForceReleaseUserBans(_ context.Context, command AdminForceUnbanCommand) (AdminForceUnbanResult, error) {
	r.lastAdminForceUnbanCommand = command
	return r.adminForceUnbanResult, nil
}

func (r *fakeModerationRepository) ExpireAdminUserBans(context.Context, int64, int32) ([]AdminUserBanRelease, error) {
	return r.adminBanExpirations, nil
}

func (r *fakeModerationRepository) GetUser(_ context.Context, userID int64) (userdomain.User, error) {
	if user, ok := r.users[userID]; ok {
		return user, nil
	}
	return r.user, nil
}

func (r *fakeModerationRepository) GetMyProfileStats(context.Context, int64) (userdomain.ProfileStats, error) {
	return userdomain.ProfileStats{}, nil
}

func (r *fakeModerationRepository) RecordProfileVisit(context.Context, int64, int64, int64) (bool, userdomain.ProfileStats, error) {
	return false, userdomain.ProfileStats{}, nil
}

func (r *fakeModerationRepository) ListProfileVisitors(context.Context, int64, int32, int32) ([]userdomain.ProfileVisitRecord, int64, error) {
	return nil, 0, nil
}

func (r *fakeModerationRepository) FollowUser(context.Context, int64, int64, int64) (userdomain.ProfileStats, error) {
	return userdomain.ProfileStats{}, nil
}

func (r *fakeModerationRepository) UnfollowUser(context.Context, int64, int64, int64) (userdomain.ProfileStats, error) {
	return userdomain.ProfileStats{}, nil
}

func (r *fakeModerationRepository) ListFollowing(context.Context, int64, int32, int32, int64, int64) ([]userdomain.FollowRecord, int64, error) {
	return nil, 0, nil
}

func (r *fakeModerationRepository) ApplyFriend(context.Context, int64, int64, int64) (userdomain.FriendApplication, bool, error) {
	return userdomain.FriendApplication{}, false, nil
}

func (r *fakeModerationRepository) AcceptFriendApplication(context.Context, int64, int64, int64) (userdomain.FriendRecord, error) {
	return userdomain.FriendRecord{}, nil
}

func (r *fakeModerationRepository) DeleteFriend(context.Context, int64, int64, int64) (bool, error) {
	return false, nil
}

func (r *fakeModerationRepository) ListFriends(context.Context, int64, int32, int32, int64, int64) ([]userdomain.FriendRecord, int64, error) {
	return nil, 0, nil
}

func (r *fakeModerationRepository) ListFriendApplications(context.Context, int64, string, int32, int32) ([]userdomain.FriendApplication, int64, error) {
	return nil, 0, nil
}

func (r *fakeModerationRepository) SubmitReport(_ context.Context, report userdomain.Report) (userdomain.Report, error) {
	r.lastReport = report
	return report, nil
}

func (r *fakeModerationRepository) BusinessUserLookup(context.Context, string, bool, int32) ([]userdomain.BusinessUserLookupItem, error) {
	return nil, nil
}

func (r *fakeModerationRepository) BatchGetUsers(context.Context, []int64) (map[int64]userdomain.User, error) {
	return nil, nil
}

func (r *fakeModerationRepository) BatchGetUserAdminProfiles(context.Context, []int64, int64) (map[int64]userdomain.AdminProfile, error) {
	if r.adminProfiles == nil {
		return map[int64]userdomain.AdminProfile{}, nil
	}
	return r.adminProfiles, nil
}

func (r *fakeModerationRepository) BatchGetRoomBasicUsers(context.Context, []int64) (map[int64]userdomain.RoomBasicUser, error) {
	return nil, nil
}

func (r *fakeModerationRepository) FindInviteReferrerByCode(_ context.Context, appCode string, inviteCode string) (userdomain.User, error) {
	r.lastFindAppCode = appCode
	r.lastFindInviteCode = inviteCode
	if r.inviteReferrer.UserID > 0 {
		return r.inviteReferrer, nil
	}
	return r.user, nil
}

func (r *fakeModerationRepository) ListUserIDs(context.Context, userdomain.UserIDPageFilter) ([]int64, error) {
	return nil, nil
}

func (r *fakeModerationRepository) CreateUserWithIdentity(context.Context, userdomain.User, userdomain.Identity) error {
	return nil
}

func (r *fakeModerationRepository) UpdateUserProfile(_ context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error) {
	r.lastProfileCommand = command
	return r.user, nil
}

func (r *fakeModerationRepository) UpdateUserProfileBackground(_ context.Context, command userdomain.ProfileBackgroundUpdateCommand) (userdomain.User, error) {
	r.user.ProfileBgImg = command.ProfileBgImg
	return r.user, nil
}

func (r *fakeModerationRepository) UpdateUserContactInfo(_ context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error) {
	r.user.ContactInfo = command.ContactInfo
	return r.user, nil
}

func (r *fakeModerationRepository) UpdateUserWithdrawAddress(_ context.Context, command userdomain.WithdrawAddressUpdateCommand) (userdomain.User, error) {
	r.lastWithdrawAddressCommand = command
	r.user.WithdrawUSDTTRC20Address = command.USDTTRC20Address
	return r.user, nil
}

func (r *fakeModerationRepository) CompleteOnboarding(context.Context, userdomain.CompleteOnboardingCommand) (userdomain.User, error) {
	return r.user, nil
}

func (r *fakeModerationRepository) BindInviteReferrer(_ context.Context, command userdomain.BindInviteReferrerCommand) (userdomain.User, error) {
	r.lastBindInvite = command
	return r.user, nil
}

func (r *fakeModerationRepository) ChangeUserCountry(_ context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, []string, error) {
	r.lastCountryCommand = command
	r.countryChangeCalls++
	r.user.Country = command.NewCountry
	r.user.RegionID = command.NewRegionID
	r.user.UpdatedAtMs = command.ChangedAtMs
	return r.user, 0, append([]string{}, r.countrySessionIDs...), nil
}

type fakeSessionDenylist struct {
	sessions []string
	reasons  []string
	err      error
}

func (d *fakeSessionDenylist) SetRevokedSession(_ context.Context, _ string, sessionID string, reason string, _ time.Duration) error {
	d.sessions = append(d.sessions, sessionID)
	d.reasons = append(d.reasons, reason)
	return d.err
}

type fakeIMLoginKicker struct {
	userID int64
}

func (k *fakeIMLoginKicker) KickUser(_ context.Context, userID int64) error {
	k.userID = userID
	return nil
}

type fakeRoomEvictor struct {
	req  *roomv1.SystemEvictUserRequest
	resp *roomv1.SystemEvictUserResponse
}

func (e *fakeRoomEvictor) SystemEvictUser(_ context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error) {
	e.req = req
	return e.resp, nil
}
