package service_test

import (
	"context"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"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"
)

func TestRoomUserGiftValueFollowsMicChangeAndResetsAfterLeave(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)}
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId: "receipt-target-heat",
		CoinSpent:        1000,
		ChargeAmount:     1000,
		HeatValue:        117,
		GiftTypeCode:     "normal",
	}}}
	svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-value")

	roomID := "room-user-gift-value"
	ownerID := int64(61001)
	targetID := int64(61002)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
		Meta:   rocketMeta(roomID, targetID, "target-heat-up"),
		SeatNo: 2,
	}); err != nil {
		t.Fatalf("target mic up failed: %v", err)
	}

	giftResp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:         rocketMeta(roomID, ownerID, "target-heat-gift"),
		TargetType:   "user",
		TargetUserId: targetID,
		GiftId:       "gift-target-heat",
		GiftCount:    1,
	})
	if err != nil {
		t.Fatalf("send gift failed: %v", err)
	}
	if user := onlineUserByID(giftResp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 117 {
		t.Fatalf("target online user gift_value mismatch: %+v", user)
	}
	if seat := seatByNo(giftResp.GetRoom(), 2); seat == nil || seat.GetUserId() != targetID || seat.GetGiftValue() != 117 {
		t.Fatalf("target seat gift_value mismatch after gift: %+v", seat)
	}
	giftEvents := roomGiftSentEvents(t, ctx, repository)
	if len(giftEvents) != 1 || giftEvents[0].GetTargetGiftValue() != 117 {
		t.Fatalf("RoomGiftSent must carry target cumulative gift value: %+v", giftEvents)
	}

	changeResp, err := svc.ChangeMicSeat(ctx, &roomv1.ChangeMicSeatRequest{
		Meta:         rocketMeta(roomID, targetID, "target-heat-change"),
		TargetUserId: targetID,
		SeatNo:       5,
	})
	if err != nil {
		t.Fatalf("change mic failed: %v", err)
	}
	if oldSeat := seatByNo(changeResp.GetRoom(), 2); oldSeat == nil || oldSeat.GetUserId() != 0 || oldSeat.GetGiftValue() != 0 {
		t.Fatalf("old seat must be empty after change: %+v", oldSeat)
	}
	if newSeat := seatByNo(changeResp.GetRoom(), 5); newSeat == nil || newSeat.GetUserId() != targetID || newSeat.GetGiftValue() != 117 {
		t.Fatalf("gift value must follow user to new seat: %+v", newSeat)
	}
	if changed := firstMicChangedEventByAction(t, ctx, repository, "change"); changed == nil || changed.GetTargetGiftValue() != 117 {
		t.Fatalf("RoomMicChanged/change must carry target gift value: %+v", changed)
	}

	if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, targetID, "target-heat-leave")}); err != nil {
		t.Fatalf("leave room failed: %v", err)
	}
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	reupResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
		Meta:   rocketMeta(roomID, targetID, "target-heat-reup"),
		SeatNo: 1,
	})
	if err != nil {
		t.Fatalf("target mic re-up failed: %v", err)
	}
	if user := onlineUserByID(reupResp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 0 {
		t.Fatalf("gift value must reset after leave and rejoin: %+v", user)
	}
	if seat := seatByNo(reupResp.GetRoom(), 1); seat == nil || seat.GetUserId() != targetID || seat.GetGiftValue() != 0 {
		t.Fatalf("rejoined user seat gift_value must be zero: %+v", seat)
	}
}

func TestRoomUserGiftValueRecoversFromCommandLog(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 10, 30, 0, 0, time.UTC)}
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId: "receipt-replay-target-heat",
		CoinSpent:        1500,
		ChargeAmount:     1500,
		HeatValue:        367,
		GiftTypeCode:     "lucky",
	}}}
	svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-replay-writer")

	roomID := "room-user-gift-replay"
	ownerID := int64(62001)
	targetID := int64(62002)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
		Meta:   rocketMeta(roomID, targetID, "replay-up"),
		SeatNo: 2,
	}); err != nil {
		t.Fatalf("target mic up failed: %v", err)
	}
	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:         rocketMeta(roomID, ownerID, "replay-gift"),
		TargetType:   "user",
		TargetUserId: targetID,
		GiftId:       "gift-replay-target-heat",
		GiftCount:    1,
	}); err != nil {
		t.Fatalf("send gift failed: %v", err)
	}
	if _, err := svc.ChangeMicSeat(ctx, &roomv1.ChangeMicSeatRequest{
		Meta:         rocketMeta(roomID, targetID, "replay-change"),
		TargetUserId: targetID,
		SeatNo:       4,
	}); err != nil {
		t.Fatalf("change mic failed: %v", err)
	}

	recoveredSvc := newUserGiftValueTestService(repository, &rocketTestWallet{}, now, "node-user-gift-replay-reader")
	resp, err := recoveredSvc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
		Meta:         rocketMeta(roomID, ownerID, "replay-snapshot"),
		RoomId:       roomID,
		ViewerUserId: ownerID,
	})
	if err != nil {
		t.Fatalf("recover room snapshot failed: %v", err)
	}
	if user := onlineUserByID(resp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 367 {
		t.Fatalf("recovered online user gift_value mismatch: %+v", user)
	}
	if oldSeat := seatByNo(resp.GetRoom(), 2); oldSeat == nil || oldSeat.GetUserId() != 0 || oldSeat.GetGiftValue() != 0 {
		t.Fatalf("recovered old seat must stay empty: %+v", oldSeat)
	}
	if newSeat := seatByNo(resp.GetRoom(), 4); newSeat == nil || newSeat.GetUserId() != targetID || newSeat.GetGiftValue() != 367 {
		t.Fatalf("recovered seat must keep target gift value after change: %+v", newSeat)
	}
}

func newUserGiftValueTestService(repository *mysqltest.Repository, wallet *rocketTestWallet, clock *fixedRoomRocketClock, nodeID string) *roomservice.Service {
	return roomservice.New(roomservice.Config{
		NodeID:         nodeID,
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 100,
		Clock:          clock,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
}

func firstMicChangedEventByAction(t *testing.T, ctx context.Context, repository *mysqltest.Repository, action string) *roomeventsv1.RoomMicChanged {
	t.Helper()

	records, err := repository.ListPendingOutbox(ctx, 100)
	if err != nil {
		t.Fatalf("list pending outbox failed: %v", err)
	}
	for _, record := range records {
		if record.EventType != "RoomMicChanged" {
			continue
		}
		var event roomeventsv1.RoomMicChanged
		if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
			t.Fatalf("decode mic changed event failed: %v", err)
		}
		if event.GetAction() == action {
			return &event
		}
	}
	return nil
}
