package roomapi

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

	activityv1 "hyapp.local/api/proto/activity/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/auth"
	gatewayclient "hyapp/services/gateway-service/internal/client"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type giftPanelTestRoomQueryClient struct {
	gatewayclient.RoomQueryClient
	getSnapshot func(context.Context, *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error)
}

func (f *giftPanelTestRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
	return f.getSnapshot(ctx, req)
}

type giftPanelTestWalletClient struct {
	gatewayclient.WalletClient
	getBalances          func(context.Context, *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error)
	getCatalogVersion    func(context.Context, *walletv1.GetGiftCatalogVersionRequest) (*walletv1.GetGiftCatalogVersionResponse, error)
	listUserResources    func(context.Context, *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error)
	listGiftTypes        func(context.Context, *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error)
	listGiftConfigs      func(context.Context, *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error)
	unusedEnrichRPCCalls atomic.Int32
}

func (f *giftPanelTestWalletClient) GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
	return f.getBalances(ctx, req)
}

func (f *giftPanelTestWalletClient) GetGiftCatalogVersion(ctx context.Context, req *walletv1.GetGiftCatalogVersionRequest) (*walletv1.GetGiftCatalogVersionResponse, error) {
	return f.getCatalogVersion(ctx, req)
}

func (f *giftPanelTestWalletClient) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
	return f.listUserResources(ctx, req)
}

func (f *giftPanelTestWalletClient) ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
	return f.listGiftTypes(ctx, req)
}

func (f *giftPanelTestWalletClient) ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
	return f.listGiftConfigs(ctx, req)
}

func (f *giftPanelTestWalletClient) BatchGetUserEquippedResources(context.Context, *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
	f.unusedEnrichRPCCalls.Add(1)
	return &walletv1.BatchGetUserEquippedResourcesResponse{}, nil
}

func (f *giftPanelTestWalletClient) GetResource(context.Context, *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
	f.unusedEnrichRPCCalls.Add(1)
	return &walletv1.GetResourceResponse{}, nil
}

type giftPanelTestUserClient struct {
	gatewayclient.UserProfileClient
	batchGetUsers func(context.Context, *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
	calls         atomic.Int32
}

func (f *giftPanelTestUserClient) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
	f.calls.Add(1)
	return f.batchGetUsers(ctx, req)
}

type giftPanelUnusedAchievementClient struct {
	gatewayclient.AchievementClient
	calls atomic.Int32
}

func (f *giftPanelUnusedAchievementClient) ListMyBadges(context.Context, *activityv1.ListMyBadgesRequest) (*activityv1.ListMyBadgesResponse, error) {
	f.calls.Add(1)
	return &activityv1.ListMyBadgesResponse{}, nil
}

type giftPanelUnusedGrowthClient struct {
	gatewayclient.GrowthLevelClient
	calls atomic.Int32
}

func (f *giftPanelUnusedGrowthClient) BatchGetUserLevelDisplayProfiles(context.Context, *activityv1.BatchGetUserLevelDisplayProfilesRequest) (*activityv1.BatchGetUserLevelDisplayProfilesResponse, error) {
	f.calls.Add(1)
	return &activityv1.BatchGetUserLevelDisplayProfilesResponse{}, nil
}

type giftPanelStageGate struct {
	started chan string
	release chan struct{}
}

func newGiftPanelStageGate() *giftPanelStageGate {
	return &giftPanelStageGate{started: make(chan string, 16), release: make(chan struct{})}
}

func (g *giftPanelStageGate) wait(ctx context.Context, stage string) error {
	select {
	case g.started <- stage:
	case <-ctx.Done():
		return ctx.Err()
	}
	select {
	case <-g.release:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func awaitGiftPanelStages(t *testing.T, started <-chan string, expected ...string) {
	t.Helper()
	want := make(map[string]bool, len(expected))
	for _, stage := range expected {
		want[stage] = true
	}
	deadline := time.NewTimer(time.Second)
	defer deadline.Stop()
	for len(want) > 0 {
		select {
		case stage := <-started:
			delete(want, stage)
		case <-deadline.C:
			t.Fatalf("gift-panel stages did not start concurrently; missing=%v", want)
		}
	}
}

type giftPanelBlockingTracker struct {
	active   atomic.Int32
	canceled atomic.Int32
}

func (t *giftPanelBlockingTracker) wait(ctx context.Context) error {
	t.active.Add(1)
	defer t.active.Add(-1)
	<-ctx.Done()
	t.canceled.Add(1)
	return ctx.Err()
}

func TestRoomGiftPanelProductionBudgetsStayBelowFlutterTimeout(t *testing.T) {
	handler := New(Config{})
	if got := handler.roomGiftPanelTotalTimeout(); got != 10*time.Second || got >= 15*time.Second {
		t.Fatalf("gift-panel total budget must leave client transport headroom: %s", got)
	}
	if got := handler.roomGiftPanelRecipientCoreTimeout(); got != 1500*time.Millisecond || got >= handler.roomGiftPanelTotalTimeout() {
		t.Fatalf("recipient core budget must remain a short degradable stage: %s", got)
	}
}

func TestRoomGiftPanelStageCanceledRecognizesLocalAndGRPCCancellation(t *testing.T) {
	for _, err := range []error{
		context.Canceled,
		fmt.Errorf("query interrupted: %w", context.Canceled),
		status.Error(codes.Canceled, "context canceled"),
	} {
		if !roomGiftPanelStageCanceled(err) {
			t.Fatalf("expected cancellation for %v", err)
		}
	}
	if roomGiftPanelStageCanceled(status.Error(codes.DeadlineExceeded, "deadline exceeded")) {
		t.Fatal("deadline exhaustion is a real latency failure and must remain error-level")
	}
}

func TestRoomGiftPanelRunsIndependentStagesConcurrentlyAndSkipsUnusedEnrichment(t *testing.T) {
	phaseOne := newGiftPanelStageGate()
	phaseTwo := newGiftPanelStageGate()
	roomID := "lalu_room_gift_parallel"
	queryClient := &giftPanelTestRoomQueryClient{getSnapshot: func(ctx context.Context, _ *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
		if err := phaseOne.wait(ctx, "snapshot"); err != nil {
			return nil, err
		}
		return giftPanelTestSnapshot(roomID), nil
	}}
	walletClient := giftPanelWorkingWalletClient()
	walletClient.getBalances = func(ctx context.Context, _ *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
		if err := phaseOne.wait(ctx, "balance"); err != nil {
			return nil, err
		}
		return &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{{AssetType: "COIN", AvailableAmount: 1000}}}, nil
	}
	walletClient.getCatalogVersion = func(ctx context.Context, _ *walletv1.GetGiftCatalogVersionRequest) (*walletv1.GetGiftCatalogVersionResponse, error) {
		if err := phaseOne.wait(ctx, "catalog"); err != nil {
			return nil, err
		}
		return &walletv1.GetGiftCatalogVersionResponse{Version: 1}, nil
	}
	walletClient.listUserResources = func(ctx context.Context, _ *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
		if err := phaseOne.wait(ctx, "bag"); err != nil {
			return nil, err
		}
		return &walletv1.ListUserResourcesResponse{}, nil
	}
	walletClient.listGiftTypes = func(ctx context.Context, _ *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
		if err := phaseTwo.wait(ctx, "gift_types"); err != nil {
			return nil, err
		}
		return &walletv1.ListGiftTypeConfigsResponse{GiftTypes: []*walletv1.GiftTypeConfig{{TypeCode: "normal", TabKey: "Gift", Status: "active"}}}, nil
	}
	walletClient.listGiftConfigs = func(ctx context.Context, _ *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
		if err := phaseTwo.wait(ctx, "gifts"); err != nil {
			return nil, err
		}
		return giftPanelTestGiftConfigs(), nil
	}
	userClient := &giftPanelTestUserClient{batchGetUsers: func(ctx context.Context, _ *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
		if err := phaseTwo.wait(ctx, "recipient_core"); err != nil {
			return nil, err
		}
		return giftPanelTestUsers(), nil
	}}
	achievementClient := &giftPanelUnusedAchievementClient{}
	growthClient := &giftPanelUnusedGrowthClient{}
	handler := New(Config{
		RoomQueryClient:   queryClient,
		WalletClient:      walletClient,
		UserProfileClient: userClient,
		AchievementClient: achievementClient,
		GrowthLevelClient: growthClient,
	})
	handler.giftPanelRecipientCoreBudget = 3 * time.Second

	responseCh := make(chan *httptest.ResponseRecorder, 1)
	go func() { responseCh <- serveGiftPanelForTest(handler, roomID) }()
	awaitGiftPanelStages(t, phaseOne.started, "snapshot", "balance", "catalog", "bag")
	close(phaseOne.release)
	awaitGiftPanelStages(t, phaseTwo.started, "gift_types", "gifts", "recipient_core")
	close(phaseTwo.release)

	recorder := <-responseCh
	if recorder.Code != http.StatusOK {
		t.Fatalf("gift-panel parallel response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if userClient.calls.Load() != 1 {
		t.Fatalf("recipient core must use one BatchGetUsers call, got=%d", userClient.calls.Load())
	}
	if achievementClient.calls.Load() != 0 || growthClient.calls.Load() != 0 || walletClient.unusedEnrichRPCCalls.Load() != 0 {
		t.Fatalf("gift-panel must not call unused enrich RPCs: badge=%d level=%d wallet=%d", achievementClient.calls.Load(), growthClient.calls.Load(), walletClient.unusedEnrichRPCCalls.Load())
	}
	var envelope struct {
		Code string `json:"code"`
		Data struct {
			Recipients []roomGiftRecipientData `json:"recipients"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode gift-panel response: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || len(envelope.Data.Recipients) != 3 {
		t.Fatalf("gift-panel recipients mismatch: %+v", envelope)
	}
	profile := envelope.Data.Recipients[1].Profile
	if profile == nil || profile.Username != "Host 101" || profile.Avatar != "https://cdn.example/101.png" {
		t.Fatalf("gift-panel core profile must survive without enrich: %+v", profile)
	}
}

func TestRoomGiftPanelTotalBudgetReturnsTraceableUpstreamErrorAndStopsWorkers(t *testing.T) {
	tracker := &giftPanelBlockingTracker{}
	queryClient := &giftPanelTestRoomQueryClient{getSnapshot: func(ctx context.Context, _ *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
		return nil, tracker.wait(ctx)
	}}
	walletClient := giftPanelWorkingWalletClient()
	walletClient.getBalances = func(ctx context.Context, _ *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
		return nil, tracker.wait(ctx)
	}
	walletClient.getCatalogVersion = func(ctx context.Context, _ *walletv1.GetGiftCatalogVersionRequest) (*walletv1.GetGiftCatalogVersionResponse, error) {
		return nil, tracker.wait(ctx)
	}
	walletClient.listUserResources = func(ctx context.Context, _ *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
		return nil, tracker.wait(ctx)
	}
	handler := New(Config{RoomQueryClient: queryClient, WalletClient: walletClient})
	handler.giftPanelTotalBudget = 40 * time.Millisecond

	startedAt := time.Now()
	recorder := serveGiftPanelForTest(handler, "lalu_room_gift_timeout")
	if elapsed := time.Since(startedAt); elapsed > 500*time.Millisecond {
		t.Fatalf("gift-panel budget did not stop response promptly: elapsed=%s", elapsed)
	}
	if recorder.Code != http.StatusBadGateway {
		t.Fatalf("gift-panel timeout status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode timeout envelope: %v", err)
	}
	if envelope.Code != httpkit.CodeUpstreamError || envelope.RequestID == "" || envelope.RequestID != recorder.Header().Get("X-Request-ID") {
		t.Fatalf("timeout envelope must expose server request_id: envelope=%+v header=%q", envelope, recorder.Header().Get("X-Request-ID"))
	}
	if tracker.canceled.Load() != 4 || tracker.active.Load() != 0 {
		t.Fatalf("all request workers must observe cancellation and exit: canceled=%d active=%d", tracker.canceled.Load(), tracker.active.Load())
	}
}

func TestRoomGiftPanelRecipientCoreTimeoutDegradesWithoutFailingPanel(t *testing.T) {
	tracker := &giftPanelBlockingTracker{}
	handler := New(Config{
		RoomQueryClient: &giftPanelTestRoomQueryClient{getSnapshot: func(context.Context, *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
			return giftPanelTestSnapshot("lalu_room_gift_profile_timeout"), nil
		}},
		WalletClient: giftPanelWorkingWalletClient(),
		UserProfileClient: &giftPanelTestUserClient{batchGetUsers: func(ctx context.Context, _ *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
			return nil, tracker.wait(ctx)
		}},
	})
	handler.giftPanelRecipientCoreBudget = 35 * time.Millisecond

	recorder := serveGiftPanelForTest(handler, "lalu_room_gift_profile_timeout")
	if recorder.Code != http.StatusOK {
		t.Fatalf("recipient core timeout must degrade, not fail panel: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if tracker.canceled.Load() != 1 || tracker.active.Load() != 0 {
		t.Fatalf("recipient core worker must stop at its child deadline: canceled=%d active=%d", tracker.canceled.Load(), tracker.active.Load())
	}
	var envelope struct {
		Code string `json:"code"`
		Data struct {
			Recipients []roomGiftRecipientData `json:"recipients"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode degraded panel: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || len(envelope.Data.Recipients) != 3 || envelope.Data.Recipients[1].UserID == "" || envelope.Data.Recipients[1].Profile != nil {
		t.Fatalf("degraded panel must preserve Room Cell recipient IDs only: %+v", envelope)
	}
}

func TestRoomGiftPanelConfigFailureCancelsSiblingRPCBeforeReturning(t *testing.T) {
	tracker := &giftPanelBlockingTracker{}
	giftStarted := make(chan struct{})
	walletClient := giftPanelWorkingWalletClient()
	walletClient.listGiftConfigs = func(ctx context.Context, _ *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
		close(giftStarted)
		return nil, tracker.wait(ctx)
	}
	walletClient.listGiftTypes = func(context.Context, *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
		<-giftStarted
		return nil, errors.New("gift type test failure")
	}
	handler := New(Config{WalletClient: walletClient})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/lalu_room_gift_config_failure/gift-panel", nil)
	request.SetPathValue("room_id", "lalu_room_gift_config_failure")
	request = request.WithContext(appcode.WithContext(request.Context(), "lalu"))

	_, err := handler.roomGiftPanelBaseConfigForVersion(request, "lalu", 1001, 1, false)
	if err == nil || err.Error() != "gift type test failure" {
		t.Fatalf("config error mismatch: %v", err)
	}
	if tracker.canceled.Load() != 1 || tracker.active.Load() != 0 {
		t.Fatalf("failed config sibling must observe cancellation before return: canceled=%d active=%d", tracker.canceled.Load(), tracker.active.Load())
	}
}

func serveGiftPanelForTest(handler *Handler, roomID string) *httptest.ResponseRecorder {
	request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/"+roomID+"/gift-panel", nil)
	request.SetPathValue("room_id", roomID)
	request = request.WithContext(auth.WithUserID(appcode.WithContext(request.Context(), "lalu"), 42))
	recorder := httptest.NewRecorder()
	httpkit.WithRequestID(http.HandlerFunc(handler.getRoomGiftPanel)).ServeHTTP(recorder, request)
	return recorder
}

func giftPanelTestSnapshot(roomID string) *roomv1.GetRoomSnapshotResponse {
	return &roomv1.GetRoomSnapshotResponse{Room: &roomv1.RoomSnapshot{
		RoomId:          roomID,
		OwnerUserId:     101,
		VisibleRegionId: 1001,
		MicSeats: []*roomv1.SeatState{
			{SeatNo: 1, UserId: 101},
			{SeatNo: 2, UserId: 102},
		},
	}}
}

func giftPanelWorkingWalletClient() *giftPanelTestWalletClient {
	return &giftPanelTestWalletClient{
		getBalances: func(context.Context, *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
			return &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{{AssetType: "COIN", AvailableAmount: 1000}}}, nil
		},
		getCatalogVersion: func(context.Context, *walletv1.GetGiftCatalogVersionRequest) (*walletv1.GetGiftCatalogVersionResponse, error) {
			return &walletv1.GetGiftCatalogVersionResponse{Version: 1}, nil
		},
		listUserResources: func(context.Context, *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
			return &walletv1.ListUserResourcesResponse{}, nil
		},
		listGiftTypes: func(context.Context, *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
			return &walletv1.ListGiftTypeConfigsResponse{GiftTypes: []*walletv1.GiftTypeConfig{{TypeCode: "normal", TabKey: "Gift", Status: "active"}}}, nil
		},
		listGiftConfigs: func(context.Context, *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
			return giftPanelTestGiftConfigs(), nil
		},
	}
}

func giftPanelTestGiftConfigs() *walletv1.ListGiftConfigsResponse {
	return &walletv1.ListGiftConfigsResponse{Gifts: []*walletv1.GiftConfig{{
		GiftId:       "rose",
		ResourceId:   11,
		Resource:     &walletv1.Resource{ResourceId: 11, ResourceType: "gift", Status: "active", Name: "Rose"},
		Status:       "active",
		Name:         "Rose",
		GiftTypeCode: "normal",
		VipLevel:     0,
		CoinPrice:    100,
	}}}
}

func giftPanelTestUsers() *userv1.BatchGetUsersResponse {
	return &userv1.BatchGetUsersResponse{Users: map[int64]*userv1.User{
		101: {UserId: 101, Username: "Host 101", Avatar: "https://cdn.example/101.png", DisplayUserId: "100101"},
		102: {UserId: 102, Username: "Host 102", Avatar: "https://cdn.example/102.png", DisplayUserId: "100102"},
	}}
}
