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 TestReplayRoomHeartbeatAfterLeaveIsIgnored(t *testing.T) {
	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("stale heartbeat after leave must not break recovery: %v", err)
	}
	if len(current.OnlineUsers) != 0 {
		t.Fatalf("stale heartbeat must not recreate presence: %+v", current.OnlineUsers)
	}
	if current.Version != 0 {
		t.Fatalf("ignored heartbeat must not advance room version, 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 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)
	}
}
