package service

import (
	"context"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/services/room-service/internal/router"
)

type robotRoomScanRepository struct {
	Repository
	configs []RobotRoomConfig
}

func (r *robotRoomScanRepository) ListActiveRobotRooms(context.Context) ([]RobotRoomConfig, error) {
	return r.configs, nil
}

func activeRobotRoomConfig(roomID string) RobotRoomConfig {
	return RobotRoomConfig{
		AppCode:      appcode.Default,
		RoomID:       roomID,
		Status:       robotRoomStatusActive,
		RobotUserIDs: []int64{1001, 1002},
	}
}

func TestClaimRobotRoomOwnershipRespectsOtherValidOwner(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC)
	directory := router.NewMemoryDirectory()
	svc := &Service{
		nodeID:    "room-node-a",
		leaseTTL:  10 * time.Second,
		clock:     fixedRoomTestClock{now: now},
		directory: directory,
	}
	config := activeRobotRoomConfig("robot-claim-test")

	owned, ownerNodeID, err := svc.claimRobotRoomOwnership(ctx, config)
	if err != nil || !owned || ownerNodeID != "" {
		t.Fatalf("unowned robot room should be claimed by this node: owned=%v owner=%q err=%v", owned, ownerNodeID, err)
	}
	if owned, _, err := svc.claimRobotRoomOwnership(ctx, config); err != nil || !owned {
		t.Fatalf("self-owned robot room should renew: owned=%v err=%v", owned, err)
	}

	directory.ForceExpire(runtimeRoomKey(config.AppCode, config.RoomID))
	if _, err := directory.EnsureOwner(ctx, runtimeRoomKey(config.AppCode, config.RoomID), "room-node-b", now, 10*time.Second); err != nil {
		t.Fatalf("other node takeover: %v", err)
	}
	owned, ownerNodeID, err = svc.claimRobotRoomOwnership(ctx, config)
	if err != nil || owned || ownerNodeID != "room-node-b" {
		t.Fatalf("valid lease on another node must not be claimed: owned=%v owner=%q err=%v", owned, ownerNodeID, err)
	}
}

func TestStartRobotRoomRuntimeSkipsWhenOwnedElsewhere(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC)
	directory := router.NewMemoryDirectory()
	svc := &Service{
		nodeID:    "room-node-a",
		leaseTTL:  10 * time.Second,
		clock:     fixedRoomTestClock{now: now},
		directory: directory,
	}
	config := activeRobotRoomConfig("robot-owned-elsewhere")
	if _, err := directory.EnsureOwner(ctx, runtimeRoomKey(config.AppCode, config.RoomID), "room-node-b", now, 10*time.Second); err != nil {
		t.Fatalf("seed other-node lease: %v", err)
	}

	// repository 故意留空：非 owner 分支必须在触达任何房间命令/仓储调用前返回。
	svc.startRobotRoomRuntime(ctx, config)
	if len(svc.robotRuntimes) != 0 {
		t.Fatalf("non-owner node must not start robot room runtime, got %d", len(svc.robotRuntimes))
	}
}

func TestStartActiveRobotRoomsReconcilesOwnershipAndInactiveRooms(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC)
	directory := router.NewMemoryDirectory()
	ownedConfig := activeRobotRoomConfig("robot-owned")
	lostConfig := activeRobotRoomConfig("robot-lease-lost")

	lostCancelled := false
	staleCancelled := false
	ownedCancelled := false
	svc := &Service{
		nodeID:     "room-node-a",
		leaseTTL:   10 * time.Second,
		clock:      fixedRoomTestClock{now: now},
		directory:  directory,
		repository: &robotRoomScanRepository{configs: []RobotRoomConfig{ownedConfig, lostConfig}},
		robotRuntimes: map[string]robotRoomRuntime{
			ownedConfig.RoomID: {cancel: func() { ownedCancelled = true }, token: "owned-token"},
			lostConfig.RoomID:  {cancel: func() { lostCancelled = true }, token: "lost-token"},
			"robot-stale":      {cancel: func() { staleCancelled = true }, token: "stale-token"},
		},
	}
	if _, err := directory.EnsureOwner(ctx, runtimeRoomKey(ownedConfig.AppCode, ownedConfig.RoomID), svc.nodeID, now, svc.leaseTTL); err != nil {
		t.Fatalf("seed self lease: %v", err)
	}
	if _, err := directory.EnsureOwner(ctx, runtimeRoomKey(lostConfig.AppCode, lostConfig.RoomID), "room-node-b", now, svc.leaseTTL); err != nil {
		t.Fatalf("seed other-node lease: %v", err)
	}

	svc.startActiveRobotRooms(ctx)

	if _, exists := svc.robotRuntimes[ownedConfig.RoomID]; !exists || ownedCancelled {
		t.Fatalf("runtime owned by this node must keep running: exists=%v cancelled=%v", exists, ownedCancelled)
	}
	if _, exists := svc.robotRuntimes[lostConfig.RoomID]; exists || !lostCancelled {
		t.Fatalf("runtime whose lease moved to another node must stop: exists=%v cancelled=%v", exists, lostCancelled)
	}
	if _, exists := svc.robotRuntimes["robot-stale"]; exists || !staleCancelled {
		t.Fatalf("runtime without active config must stop: exists=%v cancelled=%v", exists, staleCancelled)
	}
}

func TestRobotRoomLeaseKeeperStopsRuntimeWhenOwnershipMoves(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC)
	directory := router.NewMemoryDirectory()
	svc := &Service{
		nodeID:    "room-node-a",
		leaseTTL:  30 * time.Millisecond,
		clock:     fixedRoomTestClock{now: now},
		directory: directory,
	}
	config := activeRobotRoomConfig("robot-keeper")
	key := runtimeRoomKey(config.AppCode, config.RoomID)
	if _, err := directory.EnsureOwner(ctx, key, svc.nodeID, now, svc.leaseTTL); err != nil {
		t.Fatalf("seed self lease: %v", err)
	}

	runtimeCancelled := make(chan struct{})
	svc.robotRuntimes = map[string]robotRoomRuntime{
		config.RoomID: {cancel: func() { close(runtimeCancelled) }, token: "keeper-token"},
	}
	go svc.runRobotRoomLeaseKeeper(ctx, config, "keeper-token")

	// 续租周期内 keeper 必须保持 owner 且不打断 runtime。
	time.Sleep(50 * time.Millisecond)
	select {
	case <-runtimeCancelled:
		t.Fatal("keeper must not stop runtime while this node still owns the lease")
	default:
	}
	if lease, exists, err := directory.Lookup(ctx, key); err != nil || !exists || lease.NodeID != svc.nodeID || !lease.ValidAt(now) {
		t.Fatalf("keeper should keep renewing the lease: lease=%+v exists=%v err=%v", lease, exists, err)
	}

	// keeper 会在 ForceExpire 和另一节点抢占之间继续续租，重试直到 node-b 真正拿到有效 lease。
	takeoverDeadline := time.Now().Add(2 * time.Second)
	for {
		directory.ForceExpire(key)
		if _, err := directory.EnsureOwner(ctx, key, "room-node-b", now, time.Minute); err != nil {
			t.Fatalf("other node takeover: %v", err)
		}
		if lease, exists, err := directory.Lookup(ctx, key); err == nil && exists && lease.NodeID == "room-node-b" {
			break
		}
		if time.Now().After(takeoverDeadline) {
			t.Fatal("other node could not take over the lease")
		}
	}

	select {
	case <-runtimeCancelled:
	case <-time.After(2 * time.Second):
		t.Fatal("keeper must stop the local runtime after ownership moved to another node")
	}
	svc.robotRuntimeMu.Lock()
	_, exists := svc.robotRuntimes[config.RoomID]
	svc.robotRuntimeMu.Unlock()
	if exists {
		t.Fatal("keeper must remove the runtime entry after ownership loss")
	}
}
