package grpc

import (
	"context"
	"net"
	"os"
	"testing"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/integration"
	roomservice "hyapp/services/room-service/internal/room/service"
	"hyapp/services/room-service/internal/router"
	"hyapp/services/room-service/internal/testutil/mysqltest"

	grpcpkg "google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	grpcstatus "google.golang.org/grpc/status"
)

// fakeWallet 只满足 room-service 构造依赖；本测试走参数校验失败分支，不触发扣费。
type fakeWallet struct{}

// DebitGift 返回固定结果，避免 transport 测试依赖 wallet-service 进程。
func (fakeWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
	return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}, nil
}

// BatchDebitGift 返回固定结果，避免 transport 测试依赖 wallet-service 批量扣费实现。
func (fakeWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
	return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}}, nil
}

// DebitRobotGift 返回固定结果，避免 transport 测试依赖 wallet-service 机器人金币实现。
func (fakeWallet) DebitRobotGift(context.Context, *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
	return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-test"}, nil
}

// GrantResourceGroup 返回固定发放结果，避免 transport 测试依赖 wallet-service 资源组实现。
func (fakeWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
	return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-test"}}, nil
}

// GetMyVip 返回显式 legacy program，避免 transport 测试把 VIP 配置缺失误判成房间协议错误。
func (fakeWallet) GetMyVip(_ context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) {
	return &walletv1.GetMyVipResponse{State: &walletv1.VipState{
		ProgramConfig: &walletv1.VipProgramConfig{AppCode: appcode.Default, ProgramType: "legacy_timed", Status: "active"},
		EffectiveVip:  &walletv1.UserVip{UserId: req.GetUserId()},
	}}, nil
}

// newTransportTestServer 构造完整领域服务，确保测试覆盖真实 service -> transport 错误边界。
func newTransportTestServer(t *testing.T) *Server {
	t.Helper()

	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-transport-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
	}, router.NewMemoryDirectory(), mysqltest.NewRepository(t), fakeWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())

	return NewServer(svc)
}

func TestCreateRoomMapsDomainErrorToGRPCReason(t *testing.T) {
	server := newTransportTestServer(t)

	_, err := server.CreateRoom(context.Background(), &roomv1.CreateRoomRequest{
		// room_id 缺失属于客户端参数错误，不能在 gRPC 边界丢失 reason 后变成 INTERNAL_ERROR。
		SeatCount: 10,
		Mode:      "voice",
	})
	if err == nil {
		t.Fatalf("CreateRoom without room_id should fail")
	}

	if got := grpcstatus.Code(err); got != codes.InvalidArgument {
		t.Fatalf("gRPC code mismatch: got %s", got)
	}
	if got := xerr.ReasonFromGRPC(err); got != xerr.InvalidArgument {
		t.Fatalf("reason mismatch: got %s", got)
	}
	if message := grpcstatus.Convert(err).Message(); message != "room_id is required" {
		t.Fatalf("message mismatch: got %q", message)
	}
}

func TestOwnerForwardingUsesRedisRouteAndNodeRegistry(t *testing.T) {
	redisAddr := os.Getenv("ROOM_SERVICE_REDIS_TEST_ADDR")
	if redisAddr == "" {
		t.Skip("set ROOM_SERVICE_REDIS_TEST_ADDR to run Redis-backed owner forwarding test")
	}
	ctx := context.Background()
	redisClient, err := router.NewRedisClient(ctx, redisAddr, os.Getenv("ROOM_SERVICE_REDIS_TEST_PASSWORD"), 0)
	if err != nil {
		t.Fatalf("open redis directory failed: %v", err)
	}
	t.Cleanup(func() { _ = redisClient.Close() })
	directory := router.NewRedisDirectory(redisClient)
	repository := mysqltest.NewRepository(t)

	addrA, stopA := startForwardingRoomServer(t, "node-forward-a", directory, repository)
	defer stopA()
	addrB, stopB := startForwardingRoomServer(t, "node-forward-b", directory, repository)
	defer stopB()

	if err := directory.RegisterNode(ctx, router.NodeRegistration{NodeID: "node-forward-a", GRPCAddr: addrA}, time.Minute); err != nil {
		t.Fatalf("register node a failed: %v", err)
	}
	if err := directory.RegisterNode(ctx, router.NodeRegistration{NodeID: "node-forward-b", GRPCAddr: addrB}, time.Minute); err != nil {
		t.Fatalf("register node b failed: %v", err)
	}
	t.Cleanup(func() {
		_ = directory.UnregisterNode(context.Background(), "node-forward-a")
		_ = directory.UnregisterNode(context.Background(), "node-forward-b")
	})

	connA, err := grpcpkg.Dial(addrA, grpcpkg.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		t.Fatalf("dial node a failed: %v", err)
	}
	defer connA.Close()
	connB, err := grpcpkg.Dial(addrB, grpcpkg.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		t.Fatalf("dial node b failed: %v", err)
	}
	defer connB.Close()

	roomID := "room-forward-test"
	directory.ForceExpire(roomservice.RuntimeRoomKey(appcode.Default, roomID))
	t.Cleanup(func() {
		directory.ForceExpire(roomservice.RuntimeRoomKey(appcode.Default, roomID))
	})
	createResp, err := roomv1.NewRoomCommandServiceClient(connA).CreateRoom(ctx, &roomv1.CreateRoomRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-forward-create",
			CommandId:   "cmd-forward-create",
			ActorUserId: 1001,
			RoomId:      roomID,
			SentAtMs:    time.Now().UTC().UnixMilli(),
			AppCode:     appcode.Default,
		},
		SeatCount:   10,
		Mode:        "voice",
		RoomName:    "Forward Room",
		RoomAvatar:  "https://cdn.example.com/room-cover.png",
		RoomShortId: "forward-short",
	})
	if err != nil {
		t.Fatalf("create room on node a failed: %v", err)
	}
	if createResp.GetRoom().GetRoomId() != roomID {
		t.Fatalf("create room id mismatch: %+v", createResp.GetRoom())
	}

	lease, exists, err := directory.Lookup(ctx, roomservice.RuntimeRoomKey(appcode.Default, roomID))
	if err != nil || !exists {
		t.Fatalf("lookup owner lease failed: exists=%v err=%v", exists, err)
	}
	if lease.NodeID != "node-forward-a" {
		t.Fatalf("room should be owned by node a before forwarding, got %+v", lease)
	}

	joinResp, err := roomv1.NewRoomCommandServiceClient(connB).JoinRoom(ctx, &roomv1.JoinRoomRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-forward-join",
			CommandId:   "cmd-forward-join",
			ActorUserId: 1002,
			RoomId:      roomID,
			SentAtMs:    time.Now().UTC().UnixMilli(),
			AppCode:     appcode.Default,
		},
		Role: "audience",
	})
	if err != nil {
		t.Fatalf("join room through node b should forward to owner: %v", err)
	}
	if findUser(joinResp.GetRoom(), 1002) == nil {
		t.Fatalf("forwarded join did not update owner room snapshot: %+v", joinResp.GetRoom().GetOnlineUsers())
	}

	guardResp, err := roomv1.NewRoomGuardServiceClient(connB).CheckSpeakPermission(ctx, &roomv1.CheckSpeakPermissionRequest{
		RoomId:  roomID,
		UserId:  1002,
		AppCode: appcode.Default,
	})
	if err != nil {
		t.Fatalf("guard through node b should forward to owner: %v", err)
	}
	if !guardResp.GetAllowed() {
		t.Fatalf("forwarded guard should allow joined user, got %+v", guardResp)
	}

	snapshotResp, err := roomv1.NewRoomQueryServiceClient(connB).GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-forward-snapshot",
			ActorUserId: 1002,
			RoomId:      roomID,
			AppCode:     appcode.Default,
		},
		RoomId:       roomID,
		ViewerUserId: 1002,
	})
	if err != nil {
		t.Fatalf("snapshot through node b should forward to owner: %v", err)
	}
	if snapshotResp.GetRoom().GetVersion() != joinResp.GetRoom().GetVersion() {
		t.Fatalf("forwarded snapshot version mismatch: got %d want %d", snapshotResp.GetRoom().GetVersion(), joinResp.GetRoom().GetVersion())
	}
}

func startForwardingRoomServer(t *testing.T, nodeID string, directory *router.RedisDirectory, repository roomservice.Repository) (string, func()) {
	t.Helper()

	svc := roomservice.New(roomservice.Config{
		NodeID:         nodeID,
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
	}, directory, repository, fakeWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen forwarding test server failed: %v", err)
	}
	grpcServer := grpcpkg.NewServer()
	roomServer := NewServer(svc, WithOwnerForwarding(nodeID, directory, directory, time.Second))
	roomv1.RegisterRoomCommandServiceServer(grpcServer, roomServer)
	roomv1.RegisterRoomGuardServiceServer(grpcServer, roomServer)
	roomv1.RegisterRoomQueryServiceServer(grpcServer, roomServer)
	errCh := make(chan error, 1)
	go func() {
		errCh <- grpcServer.Serve(listener)
	}()
	stop := func() {
		roomServer.ownerForwarder.close()
		grpcServer.GracefulStop()
		select {
		case err := <-errCh:
			if err != nil {
				t.Fatalf("forwarding test server stopped with error: %v", err)
			}
		case <-time.After(2 * time.Second):
			t.Fatal("forwarding test server did not stop")
		}
	}
	return listener.Addr().String(), stop
}

func findUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
	for _, user := range snapshot.GetOnlineUsers() {
		if user.GetUserId() == userID {
			return user
		}
	}
	return nil
}
