package service

import (
	"math/rand"
	"testing"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/state"
)

func TestRobotRoomRuntimeClearUsesToken(t *testing.T) {
	cancelled := false
	svc := &Service{
		robotRuntimes: map[string]robotRoomRuntime{
			"robot-runtime-token-test": {
				cancel: func() { cancelled = true },
				token:  "new-token",
			},
		},
	}

	svc.clearRobotRoomRuntime("robot-runtime-token-test", "old-token")
	if len(svc.robotRuntimes) != 1 {
		t.Fatalf("old runtime token must not clear newer runtime")
	}

	svc.clearRobotRoomRuntime("robot-runtime-token-test", "new-token")
	if len(svc.robotRuntimes) != 0 {
		t.Fatalf("matching runtime token should clear runtime, got %d", len(svc.robotRuntimes))
	}
	if cancelled {
		t.Fatalf("clearRobotRoomRuntime must only remove map entry, not invoke cancel")
	}
}

func TestRobotRoomStalePresenceKeepsConfiguredRobots(t *testing.T) {
	current := &state.RoomState{
		RoomExt: map[string]string{
			roomExtRobotRoomKey:    "true",
			roomExtRobotUserIDsKey: "1001,1002",
		},
		OnlineUsers: map[int64]*state.RoomUserState{
			1001: {UserID: 1001, LastSeenAtMS: 10},
			1002: {UserID: 1002, LastSeenAtMS: 10},
			2001: {UserID: 2001, LastSeenAtMS: 10},
			2002: {UserID: 2002, LastSeenAtMS: 200},
		},
	}

	stale := stalePresenceUserIDs(current, 100)
	if len(stale) != 1 || stale[0] != 2001 {
		t.Fatalf("stale sweep should keep robot users and evict only stale real users, got %+v", stale)
	}
}

func TestReplayRoomHeartbeatWithoutPresenceRecreatesIt(t *testing.T) {
	// 线上心跳会对丢失的 presence 静默恢复后才提交命令日志；回放遇到"用户不存在"的
	// 已提交心跳必须重现恢复（含 Version++），并且绝不能让整个 Room Cell 恢复失败。
	// 旧语义的"忽略跳过"会让恢复出的 OnlineUsers/版本与快照谱系分叉。
	current := &state.RoomState{
		OnlineUsers: map[int64]*state.RoomUserState{},
	}

	err := replay(current, &command.RoomHeartbeat{
		Base: command.Base{
			ActorID:  123456,
			Room:     "robot-stale-heartbeat",
			SentAtMS: 1781675912713,
		},
	}, 1781675913212)
	if err != nil {
		t.Fatalf("heartbeat without presence must not break recovery: %v", err)
	}
	recovered, exists := current.OnlineUsers[123456]
	if !exists || recovered.Role != "audience" || recovered.LastSeenAtMS != 1781675912713 {
		t.Fatalf("replayed heartbeat heal must recreate presence from SentAtMS: %+v", current.OnlineUsers)
	}
	if current.Version != 1 {
		t.Fatalf("replayed heartbeat heal must advance room version exactly once, got %d", current.Version)
	}
}

func TestNormalizeRobotRoomGiftRuleClampsLuckyCombo(t *testing.T) {
	rule, err := normalizeRobotRoomGiftRule(&roomv1.AdminRobotRoomGiftRule{
		GiftIds:              []string{"normal-gift"},
		LuckyGiftIds:         []string{"lucky-gift"},
		NormalGiftIntervalMs: 10000,
		LuckyComboMin:        100,
		LuckyComboMax:        10000,
		LuckyPauseMinMs:      5000,
		LuckyPauseMaxMs:      20000,
	})
	if err != nil {
		t.Fatalf("normalize robot room gift rule failed: %v", err)
	}
	if rule.LuckyComboMin != maxRobotLuckyComboPerCycle || rule.LuckyComboMax != maxRobotLuckyComboPerCycle {
		t.Fatalf("lucky combo should be clamped to %d, got %d-%d", maxRobotLuckyComboPerCycle, rule.LuckyComboMin, rule.LuckyComboMax)
	}
}

func TestNormalizeRobotRoomGiftRuleAppliesBehaviorDefaults(t *testing.T) {
	rule, err := normalizeRobotRoomGiftRule(&roomv1.AdminRobotRoomGiftRule{
		GiftIds:              []string{"normal-gift"},
		LuckyGiftIds:         []string{"lucky-gift"},
		NormalGiftIntervalMs: 10000,
		LuckyComboMin:        1,
		LuckyComboMax:        1,
		LuckyPauseMinMs:      5000,
		LuckyPauseMaxMs:      20000,
	})
	if err != nil {
		t.Fatalf("normalize robot room gift rule failed: %v", err)
	}
	if rule.RobotStayMinMS != defaultRobotStayMinMS || rule.RobotStayMaxMS != defaultRobotStayMaxMS {
		t.Fatalf("robot stay defaults mismatch: %d-%d", rule.RobotStayMinMS, rule.RobotStayMaxMS)
	}
	if rule.RobotReplaceMinMS != defaultRobotReplaceMinMS || rule.RobotReplaceMaxMS != defaultRobotReplaceMaxMS {
		t.Fatalf("robot replace defaults mismatch: %d-%d", rule.RobotReplaceMinMS, rule.RobotReplaceMaxMS)
	}
	if rule.MaxGiftSenders != defaultMaxGiftSenders {
		t.Fatalf("max gift senders default mismatch: %d", rule.MaxGiftSenders)
	}
}

func TestRobotRestoreSeatNoFallsBackWhenPreferredExceedsSnapshot(t *testing.T) {
	robots := make([]int64, 12)
	for index := range robots {
		robots[index] = int64(1001 + index)
	}
	seats := make([]*roomv1.SeatState, 10)
	for index := range seats {
		seats[index] = &roomv1.SeatState{SeatNo: int32(index + 1)}
	}
	seats[0].UserId = robots[0]

	// 候选池第 12 个机器人历史 preferred=12，但当前房间只有 10 个麦位；
	// 恢复必须选择真实存在的首个空麦 2，而不是把 seat=12 交给 Room Cell。
	got := robotRestoreSeatNo(
		RobotRoomConfig{RobotUserIDs: robots, SeatCount: 10},
		robots[11],
		seats,
		map[int32]bool{1: true},
	)
	if got != 2 {
		t.Fatalf("out-of-range preferred seat must fall back to first free snapshot seat: got %d want 2", got)
	}

	got = robotRestoreSeatNo(
		RobotRoomConfig{RobotUserIDs: robots, SeatCount: 10},
		robots[11],
		seats,
		map[int32]bool{1: true, 2: true},
	)
	if got != 3 {
		t.Fatalf("same-pass occupied seat must not be selected twice: got %d want 3", got)
	}

	seats[2].Locked = true
	got = robotRestoreSeatNo(
		RobotRoomConfig{RobotUserIDs: robots, SeatCount: 10},
		robots[11],
		seats,
		map[int32]bool{1: true, 2: true},
	)
	if got != 4 {
		t.Fatalf("locked snapshot seat is not available for restore: got %d want 4", got)
	}
}

func TestRobotRoomActivityPrefersOtherReplacement(t *testing.T) {
	activity := newRobotRoomActivity([]int64{1001, 1002, 1003})
	activity.markActive(1001)
	activity.markActive(1002)
	if !activity.markInactive(1002) {
		t.Fatalf("expected active robot to become inactive")
	}
	rng := rand.New(rand.NewSource(1))
	if got := activity.pickReplacement(rng, 1002); got != 1003 {
		t.Fatalf("replacement should prefer another offline robot, got %d", got)
	}
}
