package service_test

import (
	"context"
	"fmt"
	"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/pkg/walletmq"
	"hyapp/pkg/xerr"
	roomservice "hyapp/services/room-service/internal/room/service"
	"hyapp/services/room-service/internal/testutil/mysqltest"
)

const vipTestAppCode = "fami"

func TestCreateRoomProjectsOwnerVIPWithoutListWalletLookup(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	ownerID := int64(8191)
	vipResponse := tieredVIPResponse(ownerID, 5, "VIP5", "colored_room_name")
	vipResponse.GetState().GetEffectiveVip().ExpiresAtMs = time.Now().UTC().Add(time.Hour).UnixMilli()
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{ownerID: vipResponse}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 20, 8, 0, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())

	roomID := "fami-owner-vip-list-room"
	createVIPRoom(t, ctx, svc, roomID, ownerID)
	vipCallsAfterCreate := len(wallet.vipRequests)
	response, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta: &roomv1.RequestMeta{AppCode: vipTestAppCode}, ViewerUserId: ownerID, VisibleRegionId: 1001, Tab: "new", Limit: 20,
	})
	if err != nil {
		t.Fatalf("list owner VIP room failed: %v", err)
	}
	if len(response.GetRooms()) != 1 || response.GetRooms()[0].GetOwnerVipLevel() != 5 {
		t.Fatalf("room list must return projected owner VIP5: %+v", response.GetRooms())
	}
	if len(wallet.vipRequests) != vipCallsAfterCreate {
		t.Fatalf("room list must not query wallet: before=%d after=%d requests=%+v", vipCallsAfterCreate, len(wallet.vipRequests), wallet.vipRequests)
	}
}

func TestVIPActivatedEventRefreshesOwnerVIPProjectionOnce(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	ownerID := int64(8192)
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
		ownerID: tieredVIPResponse(ownerID, 0, ""),
	}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 20, 8, 0, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())
	createVIPRoom(t, ctx, svc, "fami-owner-vip-event-room", ownerID)

	activated := tieredVIPResponse(ownerID, 7, "VIP7", "colored_room_name")
	activated.GetState().GetEffectiveVip().ExpiresAtMs = time.Now().UTC().Add(time.Hour).UnixMilli()
	wallet.vipResponses[ownerID] = activated
	event := walletmq.WalletOutboxMessage{
		AppCode: vipTestAppCode, EventID: "vip-activated-owner-projection", EventType: walletmq.EventTypeVIPActivated, UserID: ownerID,
	}
	if err := svc.HandleWalletDecorationEvent(ctx, event); err != nil {
		t.Fatalf("refresh owner VIP from activation event: %v", err)
	}
	vipCallsAfterFirstEvent := len(wallet.vipRequests)
	if err := svc.HandleWalletDecorationEvent(ctx, event); err != nil {
		t.Fatalf("repeat activation event: %v", err)
	}
	if len(wallet.vipRequests) != vipCallsAfterFirstEvent {
		t.Fatalf("consumed activation event must not query wallet twice: before=%d after=%d", vipCallsAfterFirstEvent, len(wallet.vipRequests))
	}

	response, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta: &roomv1.RequestMeta{AppCode: vipTestAppCode}, ViewerUserId: ownerID, VisibleRegionId: 1001, Tab: "new", Limit: 20,
	})
	if err != nil {
		t.Fatalf("list room after activation event: %v", err)
	}
	if len(response.GetRooms()) != 1 || response.GetRooms()[0].GetOwnerVipLevel() != 7 {
		t.Fatalf("activation event must project owner VIP7 into room list: %+v", response.GetRooms())
	}
}

func TestJoinRoomCarriesEffectiveVIPAndQueriesWalletOnce(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
		8202: tieredVIPResponse(8202, 4, "VIP4", "room_entry_notice"),
	}}
	directIM := newRecordingRoomDirectIMPublisher()
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, directIM)

	roomID := "fami-vip-join-room"
	createVIPRoom(t, ctx, svc, roomID, 8201)
	response, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
		Meta: vipRoomMeta(roomID, 8202, "join"),
		Role: "audience",
	})
	if err != nil {
		t.Fatalf("join room failed: %v", err)
	}
	if response.GetEffectiveVip().GetProgramType() != "tiered_privilege_v1" || response.GetEffectiveVip().GetLevel() != 4 || response.GetEffectiveVip().GetName() != "VIP4" || !response.GetEffectiveVip().GetRoomEntryNoticeEnabled() {
		t.Fatalf("join effective vip mismatch: %+v", response.GetEffectiveVip())
	}
	if len(wallet.vipRequests) != 1 || wallet.vipRequests[0].GetAppCode() != vipTestAppCode || wallet.vipRequests[0].GetUserId() != 8202 {
		t.Fatalf("join must query exactly one scoped effective vip state: %+v", wallet.vipRequests)
	}

	waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomUserJoined": 1})
	records := directIM.recordsByType("RoomUserJoined")
	if len(records) != 1 {
		t.Fatalf("joined direct IM record mismatch: %+v", records)
	}
	if records[0].GetAppCode() != vipTestAppCode {
		t.Fatalf("joined direct IM must inherit request app: got=%q want=%q", records[0].GetAppCode(), vipTestAppCode)
	}
	var joined roomeventsv1.RoomUserJoined
	if err := proto.Unmarshal(records[0].GetBody(), &joined); err != nil {
		t.Fatalf("decode joined event failed: %v", err)
	}
	if joined.GetVipProgramType() != "tiered_privilege_v1" || joined.GetEffectiveVipLevel() != 4 || joined.GetEffectiveVipName() != "VIP4" || !joined.GetRoomEntryNoticeEnabled() {
		t.Fatalf("joined event vip mismatch: %+v", &joined)
	}
}

func TestRoomEntryNoticeRequiresBenefitAndUserSetting(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
		8502: tieredVIPResponseWithRoomNoticeSetting(8502, 8, "VIP8", true, true),
		8503: tieredVIPResponseWithRoomNoticeSetting(8503, 8, "VIP8", true, false),
		8504: tieredVIPResponseWithRoomNoticeSetting(8504, 8, "VIP8", false, true),
		// nil user_settings 模拟滚动升级期间旧 wallet，必须按默认开启兼容。
		8505: tieredVIPResponse(8505, 8, "VIP8", "room_entry_notice"),
	}}
	directIM := newRecordingRoomDirectIMPublisher()
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 8, 30, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, directIM)
	roomID := "fami-vip-setting-room"
	createVIPRoom(t, ctx, svc, roomID, 8501)

	wants := map[int64]bool{8502: true, 8503: false, 8504: false, 8505: true}
	for userID, want := range wants {
		response, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
			Meta: vipRoomMeta(roomID, userID, fmt.Sprintf("settings-%d", userID)), Role: "audience",
		})
		if err != nil {
			t.Fatalf("join user %d failed: %v", userID, err)
		}
		if got := response.GetEffectiveVip().GetRoomEntryNoticeEnabled(); got != want {
			t.Fatalf("room snapshot setting mismatch user=%d got=%v want=%v snapshot=%+v", userID, got, want, response.GetEffectiveVip())
		}
	}

	waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomUserJoined": len(wants)})
	seen := make(map[int64]bool, len(wants))
	for _, record := range directIM.recordsByType("RoomUserJoined") {
		var joined roomeventsv1.RoomUserJoined
		if err := proto.Unmarshal(record.GetBody(), &joined); err != nil {
			t.Fatalf("decode joined event failed: %v", err)
		}
		seen[joined.GetUserId()] = joined.GetRoomEntryNoticeEnabled()
	}
	for userID, want := range wants {
		if got, exists := seen[userID]; !exists || got != want {
			t.Fatalf("joined event/IM setting mismatch user=%d exists=%v got=%v want=%v", userID, exists, got, want)
		}
	}
}

func TestTieredVIPCustomRoomBackgroundRequiresEffectiveBenefit(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	ownerID := int64(8301)
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
		ownerID: tieredVIPResponse(ownerID, 3, "VIP3"),
	}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())
	roomID := "fami-vip-background-room"
	createVIPRoom(t, ctx, svc, roomID, ownerID)

	if _, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
		Meta:     vipRoomMeta(roomID, ownerID, "save-denied"),
		RoomId:   roomID,
		ImageUrl: "https://cdn.example.com/fami-vip-denied.png",
	}); !xerr.IsCode(err, xerr.VIPBenefitRequired) {
		t.Fatalf("tiered app without custom_room_background must be denied: %v", err)
	}

	wallet.vipResponses[ownerID] = tieredVIPResponse(ownerID, 3, "VIP3", "custom_room_background")
	first := saveVIPRoomBackground(t, ctx, svc, roomID, ownerID, "first", "https://cdn.example.com/fami-vip-first.png")
	second := saveVIPRoomBackground(t, ctx, svc, roomID, ownerID, "second", "https://cdn.example.com/fami-vip-second.png")
	if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
		Meta:         vipRoomMeta(roomID, ownerID, "set-allowed"),
		BackgroundId: first.GetBackgroundId(),
	}); err != nil {
		t.Fatalf("effective custom_room_background benefit must allow set: %v", err)
	}

	wallet.vipResponses[ownerID] = tieredVIPResponse(ownerID, 3, "VIP3")
	if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
		Meta:         vipRoomMeta(roomID, ownerID, "set-denied"),
		BackgroundId: second.GetBackgroundId(),
	}); !xerr.IsCode(err, xerr.VIPBenefitRequired) {
		t.Fatalf("expired custom_room_background benefit must block later set: %v", err)
	}
}

func TestVIPModerationProtectionBlocksRoomManagersButNotSystemEvict(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	targetID := int64(8402)
	wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
		targetID: tieredVIPResponse(targetID, 9, "VIP9", "anti_kick", "anti_mute"),
	}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)}
	svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())
	roomID := "fami-vip-moderation-room"
	ownerID := int64(8401)
	createVIPRoom(t, ctx, svc, roomID, ownerID)
	if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{Meta: vipRoomMeta(roomID, targetID, "target-join"), Role: "audience"}); err != nil {
		t.Fatalf("join protected target failed: %v", err)
	}

	if _, err := svc.MuteUser(ctx, &roomv1.MuteUserRequest{
		Meta:         vipRoomMeta(roomID, ownerID, "mute"),
		TargetUserId: targetID,
		Muted:        true,
	}); !xerr.IsCode(err, xerr.VIPAntiMute) {
		t.Fatalf("anti_mute must block ordinary room owner: %v", err)
	}
	if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
		Meta:         vipRoomMeta(roomID, ownerID, "kick"),
		TargetUserId: targetID,
	}); !xerr.IsCode(err, xerr.VIPAntiKick) {
		t.Fatalf("anti_kick must block ordinary room owner: %v", err)
	}

	vipCallsBeforeSystemEvict := len(wallet.vipRequests)
	evict, err := svc.SystemEvictUser(ctx, &roomv1.SystemEvictUserRequest{
		Meta:           vipRoomMeta(roomID, 9901, "system-evict"),
		TargetUserId:   targetID,
		OperatorUserId: 9901,
		Reason:         "platform_moderation",
		BanFromRoom:    true,
	})
	if err != nil {
		t.Fatalf("system evict must bypass anti_kick: %v", err)
	}
	if !evict.GetResult().GetApplied() || findRoomUser(evict.GetRoom(), targetID) != nil || !containsInt64(evict.GetRoom().GetBanUserIds(), targetID) {
		t.Fatalf("system evict must remove and ban protected target: %+v", evict)
	}
	if len(wallet.vipRequests) != vipCallsBeforeSystemEvict {
		t.Fatalf("SystemEvictUser must not query or enforce ordinary VIP moderation benefits: before=%d after=%d", vipCallsBeforeSystemEvict, len(wallet.vipRequests))
	}
}

func tieredVIPResponse(userID int64, level int32, name string, benefitCodes ...string) *walletv1.GetMyVipResponse {
	benefits := make([]*walletv1.VipBenefit, 0, len(benefitCodes))
	for index, code := range benefitCodes {
		benefits = append(benefits, &walletv1.VipBenefit{
			BenefitCode: code,
			Name:        code,
			Status:      "active",
			SortOrder:   int32(index + 1),
		})
	}
	return &walletv1.GetMyVipResponse{State: &walletv1.VipState{
		ProgramConfig: &walletv1.VipProgramConfig{AppCode: vipTestAppCode, ProgramType: "tiered_privilege_v1", Status: "active", LevelCount: 9},
		EffectiveVip: &walletv1.UserVip{
			UserId:      userID,
			Level:       level,
			Name:        name,
			Active:      level > 0,
			ProgramType: "tiered_privilege_v1",
		},
		EffectiveBenefits: benefits,
	}}
}

func tieredVIPResponseWithRoomNoticeSetting(userID int64, level int32, name string, hasBenefit bool, enabled bool) *walletv1.GetMyVipResponse {
	codes := []string{}
	if hasBenefit {
		codes = append(codes, "room_entry_notice")
	}
	response := tieredVIPResponse(userID, level, name, codes...)
	response.GetState().UserSettings = &walletv1.VipUserSettings{
		AppCode: vipTestAppCode, UserId: userID,
		RoomEntryNoticeEnabled: enabled, OnlineGlobalNoticeEnabled: true,
	}
	return response
}

func createVIPRoom(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, ownerID int64) {
	t.Helper()
	if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
		Meta:            vipRoomMeta(roomID, ownerID, "create"),
		SeatCount:       10,
		Mode:            "voice",
		VisibleRegionId: 1001,
		RoomName:        roomID,
		RoomAvatar:      testRoomCoverURL,
		RoomShortId:     roomID,
	}); err != nil {
		t.Fatalf("create vip room failed: %v", err)
	}
}

func saveVIPRoomBackground(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, ownerID int64, suffix string, imageURL string) *roomv1.RoomBackgroundImage {
	t.Helper()
	response, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
		Meta:     vipRoomMeta(roomID, ownerID, "save-"+suffix),
		RoomId:   roomID,
		ImageUrl: imageURL,
	})
	if err != nil {
		t.Fatalf("save vip room background failed: %v", err)
	}
	return response.GetBackground()
}

func vipRoomMeta(roomID string, actorUserID int64, suffix string) *roomv1.RequestMeta {
	return &roomv1.RequestMeta{
		RequestId:   fmt.Sprintf("req-vip-%s", suffix),
		CommandId:   fmt.Sprintf("cmd-vip-%s", suffix),
		ActorUserId: actorUserID,
		RoomId:      roomID,
		AppCode:     vipTestAppCode,
		SentAtMs:    time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC).UnixMilli(),
	}
}
