package user_test

import (
	"context"
	"testing"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	userservice "hyapp/services/user-service/internal/service/user"
	"hyapp/services/user-service/internal/testutil/mysqltest"
)

func TestAdminUserBanOverlapRestoresOnlyAfterLastManagedFact(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18001, "188001", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()

	first, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "admin-ban-first", TargetUserID: 18001, ExpiresAtMs: 2_000,
		OperatorAdminID: 9001, Reason: "first", RequestID: "req-first", NowMs: 1_000,
	})
	if err != nil {
		t.Fatalf("create first admin ban failed: %v", err)
	}
	second, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "admin-ban-second", TargetUserID: 18001, ExpiresAtMs: 0,
		OperatorAdminID: 9002, Reason: "second", RequestID: "req-second", NowMs: 1_100,
	})
	if err != nil {
		t.Fatalf("create overlapping admin ban failed: %v", err)
	}
	if !first.StatusChanged || second.StatusChanged {
		t.Fatalf("only the first ban may transition user status: first=%+v second=%+v", first, second)
	}
	if _, err := storage.SetUserStatus(ctx, userservice.UserStatusCommand{
		AppCode: "lalu", TargetUserID: 18001, Status: userdomain.StatusActive,
		OperatorType: userservice.OperatorTypeAdmin, OperatorUserID: 9003, RequestID: "bypass", NowMs: 1_150,
	}); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("generic active status must not bypass active ban facts, err=%v", err)
	}

	firstRelease, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		BanID: first.Ban.BanID, TargetUserID: 18001, OperatorAdminID: 9001,
		Status: userservice.AdminUserBanStatusReleased, Reason: "release-first", RequestID: "release-first", NowMs: 1_200,
	})
	if err != nil {
		t.Fatalf("release first admin ban failed: %v", err)
	}
	if firstRelease.Restored || firstRelease.User.Status != userdomain.StatusBanned {
		t.Fatalf("remaining admin ban must keep user banned: %+v", firstRelease)
	}

	secondRelease, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		BanID: second.Ban.BanID, TargetUserID: 18001, OperatorAdminID: 9002,
		Status: userservice.AdminUserBanStatusReleased, Reason: "release-second", RequestID: "release-second", NowMs: 1_300,
	})
	if err != nil {
		t.Fatalf("release second admin ban failed: %v", err)
	}
	if !secondRelease.Restored || secondRelease.User.Status != userdomain.StatusActive {
		t.Fatalf("last managed ban release must restore active user: %+v", secondRelease)
	}
}

func TestAdminUserBanReleaseWithoutIDSelectsLatestAdminFact(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18007, "188007", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()
	first, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "admin-ban-select-first", TargetUserID: 18007, ExpiresAtMs: 8_000,
		OperatorAdminID: 9001, Reason: "first", RequestID: "select-first", NowMs: 6_000,
	})
	if err != nil {
		t.Fatalf("create first admin ban failed: %v", err)
	}
	second, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "admin-ban-select-second", TargetUserID: 18007, ExpiresAtMs: 0,
		OperatorAdminID: 9002, Reason: "second", RequestID: "select-second", NowMs: 6_100,
	})
	if err != nil {
		t.Fatalf("create second admin ban failed: %v", err)
	}
	release, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		TargetUserID: 18007, OperatorAdminID: 9003, Status: userservice.AdminUserBanStatusReleased,
		Reason: "release-latest", RequestID: "release-latest", NowMs: 6_200,
	})
	if err != nil {
		t.Fatalf("release latest admin ban failed: %v", err)
	}
	if release.Ban.BanID != second.Ban.BanID || release.Restored || release.User.Status != userdomain.StatusBanned {
		t.Fatalf("latest fact must be released while older fact keeps ban: release=%+v first=%+v", release, first.Ban)
	}
	// 同一 request_id 代表同一次解封命令；重试必须返回原结果，不能继续释放第一条重叠封禁。
	retry, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		TargetUserID: 18007, OperatorAdminID: 9003, Status: userservice.AdminUserBanStatusReleased,
		Reason: "release-latest", RequestID: "release-latest", NowMs: 6_300,
	})
	if err != nil {
		t.Fatalf("retry latest admin ban release failed: %v", err)
	}
	if retry.Ban.BanID != second.Ban.BanID || retry.Restored || retry.User.Status != userdomain.StatusBanned {
		t.Fatalf("retry must reuse resolved ban and leave older fact active: retry=%+v first=%+v", retry, first.Ban)
	}
	finalRelease, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		TargetUserID: 18007, OperatorAdminID: 9003, Status: userservice.AdminUserBanStatusReleased,
		Reason: "release-remaining", RequestID: "release-remaining", NowMs: 6_400,
	})
	if err != nil {
		t.Fatalf("release remaining admin ban failed: %v", err)
	}
	if finalRelease.Ban.BanID != first.Ban.BanID || !finalRelease.Restored || finalRelease.User.Status != userdomain.StatusActive {
		t.Fatalf("new command must release remaining fact and restore user: %+v", finalRelease)
	}
}

func TestAdminUserBanDoesNotRestoreLegacyBannedUser(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18002, "188002", userdomain.StatusBanned)
	storage := repo.Repository.UserRepository()

	persisted, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "legacy-overlay", TargetUserID: 18002, OperatorAdminID: 9001,
		Reason: "legacy-overlay", RequestID: "legacy-overlay", NowMs: 2_000,
	})
	if err != nil {
		t.Fatalf("create admin fact over legacy ban failed: %v", err)
	}
	if persisted.StatusChanged {
		t.Fatalf("legacy banned user must not get a synthetic banned->banned transition: %+v", persisted)
	}

	release, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		BanID: persisted.Ban.BanID, TargetUserID: 18002, OperatorAdminID: 9001,
		Status: userservice.AdminUserBanStatusReleased, Reason: "manual-release", RequestID: "manual-release", NowMs: 2_100,
	})
	if err != nil {
		t.Fatalf("release legacy overlay failed: %v", err)
	}
	if release.Restored || release.User.Status != userdomain.StatusBanned {
		t.Fatalf("legacy ban must remain permanent after overlay release: %+v", release)
	}
}

func TestManagerAndAdminBanOverlapRestoresOnlyAfterBothSourcesRelease(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18004, "188004", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()
	target, err := storage.GetUser(ctx, 18004)
	if err != nil {
		t.Fatalf("get manager block target failed: %v", err)
	}
	managerBlock, err := storage.CreateManagerUserBlock(ctx, userservice.ManagerUserBlockCommand{
		CommandID: "manager-block", ManagerUserID: 7001, TargetUserID: 18004,
		BlockedUntilMS: 8_000, Reason: "manager", RequestID: "manager-block", NowMS: 3_000,
	}, target)
	if err != nil {
		t.Fatalf("create manager block fact failed: %v", err)
	}
	if _, err := storage.SetUserStatus(ctx, userservice.UserStatusCommand{
		AppCode: "lalu", TargetUserID: 18004, Status: userdomain.StatusBanned,
		OperatorType: userservice.OperatorTypeAppUser, OperatorUserID: 7001,
		Reason: "manager_center_block:" + managerBlock.BlockID, RequestID: "manager-block", NowMs: 3_000,
	}); err != nil {
		t.Fatalf("apply manager block status failed: %v", err)
	}
	adminBan, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "admin-over-manager", TargetUserID: 18004, ExpiresAtMs: 9_000,
		OperatorAdminID: 9001, Reason: "admin", RequestID: "admin-over-manager", NowMs: 3_100,
	})
	if err != nil {
		t.Fatalf("create admin overlap failed: %v", err)
	}

	managerRelease, err := storage.ReleaseManagerUserBlock(ctx, userservice.ReleaseManagerUserBlockCommand{
		ManagerUserID: 7001, BlockID: managerBlock.BlockID, Status: userservice.ManagerUserBlockStatusReleased,
		Reason: "manager-release", RequestID: "manager-release", NowMS: 3_200,
	})
	if err != nil {
		t.Fatalf("release manager block failed: %v", err)
	}
	if managerRelease.ShouldRestore || managerRelease.User.Status != userdomain.StatusBanned {
		t.Fatalf("active admin ban must prevent manager release from restoring user: %+v", managerRelease)
	}

	adminRelease, err := storage.ReleaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
		BanID: adminBan.Ban.BanID, TargetUserID: 18004, OperatorAdminID: 9001,
		Status: userservice.AdminUserBanStatusReleased, Reason: "admin-release", RequestID: "admin-release", NowMs: 3_300,
	})
	if err != nil {
		t.Fatalf("release admin block failed: %v", err)
	}
	if !adminRelease.Restored || adminRelease.User.Status != userdomain.StatusActive {
		t.Fatalf("last manager/admin source release must restore user: %+v", adminRelease)
	}
}

func TestAdminForceUnbanReleasesAllManagedSourcesAndRestoresUser(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18008, "188008", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()
	target, err := storage.GetUser(ctx, 18008)
	if err != nil {
		t.Fatalf("get force unban target failed: %v", err)
	}
	managerBlock, err := storage.CreateManagerUserBlock(ctx, userservice.ManagerUserBlockCommand{
		CommandID: "force-manager", ManagerUserID: 7001, TargetUserID: 18008,
		BlockedUntilMS: 20_000, Reason: "manager", RequestID: "force-manager", NowMS: 10_000,
	}, target)
	if err != nil {
		t.Fatalf("create manager block failed: %v", err)
	}
	if _, err := storage.SetUserStatus(ctx, userservice.UserStatusCommand{
		AppCode: "lalu", TargetUserID: 18008, Status: userdomain.StatusBanned,
		OperatorType: userservice.OperatorTypeAppUser, OperatorUserID: 7001,
		Reason: "manager_center_block:" + managerBlock.BlockID, RequestID: "force-manager", NowMs: 10_000,
	}); err != nil {
		t.Fatalf("apply manager block status failed: %v", err)
	}
	if _, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "force-admin-first", TargetUserID: 18008, ExpiresAtMs: 0,
		OperatorAdminID: 9001, Reason: "admin-first", RequestID: "force-admin-first", NowMs: 10_100,
	}); err != nil {
		t.Fatalf("create first admin ban failed: %v", err)
	}
	if _, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "force-admin-second", TargetUserID: 18008, ExpiresAtMs: 30_000,
		OperatorAdminID: 9002, Reason: "admin-second", RequestID: "force-admin-second", NowMs: 10_200,
	}); err != nil {
		t.Fatalf("create second admin ban failed: %v", err)
	}

	release, err := storage.ForceReleaseUserBans(ctx, userservice.AdminForceUnbanCommand{
		TargetUserID: 18008, OperatorAdminID: 9900, Reason: "manual override",
		RequestID: "force-unban-all", NowMs: 10_300,
	})
	if err != nil {
		t.Fatalf("force unban failed: %v", err)
	}
	if release.ReleasedAdminBanCount != 2 || release.ReleasedManagerBlockCount != 1 || !release.StatusRestored || release.User.Status != userdomain.StatusActive {
		t.Fatalf("force unban must release every source and restore user: %+v", release)
	}

	var activeAdminCount, activeManagerCount int64
	if err := repo.RawDB().QueryRow(`SELECT COUNT(*) FROM admin_user_bans WHERE app_code = 'lalu' AND target_user_id = 18008 AND status = 'active'`).Scan(&activeAdminCount); err != nil {
		t.Fatalf("count active admin bans failed: %v", err)
	}
	if err := repo.RawDB().QueryRow(`SELECT COUNT(*) FROM manager_user_blocks WHERE app_code = 'lalu' AND target_user_id = 18008 AND status = 'active'`).Scan(&activeManagerCount); err != nil {
		t.Fatalf("count active manager blocks failed: %v", err)
	}
	if activeAdminCount != 0 || activeManagerCount != 0 {
		t.Fatalf("force unban left active facts: admin=%d manager=%d", activeAdminCount, activeManagerCount)
	}
}

func TestAdminForceUnbanRetryDoesNotReleaseNewBan(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18009, "188009", userdomain.StatusBanned)
	storage := repo.Repository.UserRepository()
	command := userservice.AdminForceUnbanCommand{
		TargetUserID: 18009, OperatorAdminID: 9900, Reason: "legacy override",
		RequestID: "force-unban-idempotent", NowMs: 11_000,
	}
	first, err := storage.ForceReleaseUserBans(ctx, command)
	if err != nil {
		t.Fatalf("force legacy unban failed: %v", err)
	}
	if first.ReleasedAdminBanCount != 0 || first.ReleasedManagerBlockCount != 0 || !first.StatusRestored || first.User.Status != userdomain.StatusActive {
		t.Fatalf("legacy force unban mismatch: %+v", first)
	}
	newBan, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "ban-after-force", TargetUserID: 18009, ExpiresAtMs: 0,
		OperatorAdminID: 9001, Reason: "new independent ban", RequestID: "ban-after-force", NowMs: 11_100,
	})
	if err != nil {
		t.Fatalf("create new ban after force unban failed: %v", err)
	}

	retry, err := storage.ForceReleaseUserBans(ctx, command)
	if err != nil {
		t.Fatalf("retry force unban failed: %v", err)
	}
	if !retry.StatusRestored || retry.User.Status != userdomain.StatusBanned {
		t.Fatalf("retry must return first command result without restoring newly banned user: %+v", retry)
	}
	var newBanStatus string
	if err := repo.RawDB().QueryRow(`SELECT status FROM admin_user_bans WHERE app_code = 'lalu' AND ban_id = ?`, newBan.Ban.BanID).Scan(&newBanStatus); err != nil {
		t.Fatalf("read new ban status failed: %v", err)
	}
	if newBanStatus != userservice.AdminUserBanStatusActive {
		t.Fatalf("retry released a ban created after the original command: status=%s", newBanStatus)
	}
}

func TestReleasedManagerBlockCannotReapplyBannedStatus(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18010, "188010", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()
	target, err := storage.GetUser(ctx, 18010)
	if err != nil {
		t.Fatalf("get manager race target failed: %v", err)
	}
	block, err := storage.CreateManagerUserBlock(ctx, userservice.ManagerUserBlockCommand{
		CommandID: "manager-race", ManagerUserID: 7001, TargetUserID: 18010,
		BlockedUntilMS: 30_000, Reason: "race", RequestID: "manager-race", NowMS: 12_000,
	}, target)
	if err != nil {
		t.Fatalf("create manager race block failed: %v", err)
	}
	if _, err := storage.ForceReleaseUserBans(ctx, userservice.AdminForceUnbanCommand{
		TargetUserID: 18010, OperatorAdminID: 9900, Reason: "override pending block",
		RequestID: "force-before-manager-status", NowMs: 12_100,
	}); err != nil {
		t.Fatalf("force release pending manager block failed: %v", err)
	}
	if _, err := storage.SetUserStatus(ctx, userservice.UserStatusCommand{
		AppCode: "lalu", TargetUserID: 18010, Status: userdomain.StatusBanned,
		OperatorType: userservice.OperatorTypeAppUser, OperatorUserID: 7001,
		Reason: "manager_center_block:" + block.BlockID, RequestID: "manager-race", NowMs: 12_200,
	}); xerr.CodeOf(err) != xerr.Conflict {
		t.Fatalf("released manager block must not reapply banned status, err=%v", err)
	}
	user, err := storage.GetUser(ctx, 18010)
	if err != nil || user.Status != userdomain.StatusActive {
		t.Fatalf("released manager race must leave user active: user=%+v err=%v", user, err)
	}
}

func TestExpireAdminUserBansSkipsPermanentAndRestoresTimedBan(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18005, "188005", userdomain.StatusActive)
	createAdminBanTestUser(t, repo, 18006, "188006", userdomain.StatusActive)
	storage := repo.Repository.UserRepository()
	if _, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "permanent-ban", TargetUserID: 18005, ExpiresAtMs: 0,
		OperatorAdminID: 9001, RequestID: "permanent-ban", NowMs: 4_000,
	}); err != nil {
		t.Fatalf("create permanent ban failed: %v", err)
	}
	if _, err := storage.CreateAdminUserBan(ctx, userservice.AdminUserBanCommand{
		CommandID: "timed-ban", TargetUserID: 18006, ExpiresAtMs: 4_500,
		OperatorAdminID: 9001, RequestID: "timed-ban", NowMs: 4_000,
	}); err != nil {
		t.Fatalf("create timed ban failed: %v", err)
	}

	releases, err := storage.ExpireAdminUserBans(ctx, 5_000, 100)
	if err != nil {
		t.Fatalf("expire timed bans failed: %v", err)
	}
	if len(releases) != 1 || releases[0].Ban.TargetUserID != 18006 || !releases[0].Restored {
		t.Fatalf("expiry must release only timed ban: %+v", releases)
	}
	permanentUser, err := storage.GetUser(ctx, 18005)
	if err != nil || permanentUser.Status != userdomain.StatusBanned {
		t.Fatalf("permanent ban must remain banned: user=%+v err=%v", permanentUser, err)
	}
}

func TestBatchGetUserAdminProfilesUsesSuccessfulLoginAndActiveRoles(t *testing.T) {
	repo := mysqltest.NewRepository(t)
	ctx := appcode.WithContext(context.Background(), "lalu")
	createAdminBanTestUser(t, repo, 18003, "188003", userdomain.StatusActive)
	db := repo.RawDB()
	if _, err := db.Exec(`UPDATE users SET birth_date = '2000-02-03', register_device = 'Pixel 9' WHERE app_code = 'lalu' AND user_id = 18003`); err != nil {
		t.Fatalf("seed admin profile user fields failed: %v", err)
	}
	if _, err := db.Exec(`
		INSERT INTO login_audit (app_code, request_id, user_id, login_type, channel, platform, result, blocked, block_reason, created_at_ms)
		VALUES ('lalu', 'success-login', 18003, 'password', 'account', 'android', 'success', 0, '', 3000),
		       ('lalu', 'failed-login', 18003, 'password', 'account', 'android', 'failed', 0, '', 4000)`); err != nil {
		t.Fatalf("seed admin profile login facts failed: %v", err)
	}
	if _, err := db.Exec(`
		INSERT INTO host_profiles (app_code, user_id, status, current_agency_id, current_membership_id, source, first_became_host_at_ms, created_at_ms, updated_at_ms)
		VALUES ('lalu', 18003, 'active', NULL, NULL, 'admin_add_host', 3000, 3000, 3000)`); err != nil {
		t.Fatalf("seed admin profile host role failed: %v", err)
	}
	if _, err := db.Exec(`
		INSERT INTO manager_profiles (app_code, user_id, status, contact_info, created_by_admin_id, created_at_ms, updated_at_ms)
		VALUES ('lalu', 18003, 'active', '', 9001, 3000, 3000)`); err != nil {
		t.Fatalf("seed admin profile manager role failed: %v", err)
	}

	profiles, err := repo.Repository.UserRepository().BatchGetUserAdminProfiles(ctx, []int64{18003}, 5_000)
	if err != nil {
		t.Fatalf("batch admin profile failed: %v", err)
	}
	profile := profiles[18003]
	if profile.BirthDate != "2000-02-03" || profile.RegisterDevice != "Pixel 9" || profile.LastSuccessLoginAtMs != 3_000 {
		t.Fatalf("admin profile base/login facts mismatch: %+v", profile)
	}
	if len(profile.Roles) != 2 || profile.Roles[0] != userdomain.AdminRoleHost || profile.Roles[1] != userdomain.AdminRoleManager {
		t.Fatalf("admin profile roles mismatch: %+v", profile.Roles)
	}
}

func createAdminBanTestUser(t *testing.T, repo *mysqltest.Repository, userID int64, displayUserID string, status userdomain.Status) {
	t.Helper()
	err := repo.CreateUserWithIdentity(appcode.WithContext(context.Background(), "lalu"), userdomain.User{
		AppCode:                  "lalu",
		UserID:                   userID,
		DefaultDisplayUserID:     displayUserID,
		CurrentDisplayUserID:     displayUserID,
		CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
		Username:                 "admin-ban-user",
		Status:                   status,
		CreatedAtMs:              100,
		UpdatedAtMs:              100,
	}, userdomain.Identity{
		AppCode:              "lalu",
		UserID:               userID,
		DisplayUserID:        displayUserID,
		DefaultDisplayUserID: displayUserID,
		DisplayUserIDKind:    userdomain.DisplayUserIDKindDefault,
		Status:               userdomain.DisplayUserIDStatusActive,
	})
	if err != nil {
		t.Fatalf("create admin ban test user failed: %v", err)
	}
}
