package service

import (
	"context"
	"math"
	"sync"
	"testing"

	"google.golang.org/protobuf/proto"
	luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/luckygiftmq"
	"hyapp/services/room-service/internal/room/outbox"
)

func TestScaledRobotRewardCoinsUsesMultiplierAndSaturates(t *testing.T) {
	if got := scaledRobotRewardCoins(100, 20_000_000); got != 2_000 {
		t.Fatalf("20x robot reward = %d, want 2000", got)
	}
	if got := scaledRobotRewardCoins(math.MaxInt64, 20_000_000); got != math.MaxInt64 {
		t.Fatalf("overflowing robot reward = %d, want saturation", got)
	}
	if got := scaledRobotRewardCoins(100, 0); got != 0 {
		t.Fatalf("zero multiplier robot reward = %d, want 0", got)
	}
}

type luckyGiftDisplayRepository struct {
	Repository

	mu      sync.Mutex
	records map[string]outbox.Record
}

func (r *luckyGiftDisplayRepository) SaveOutbox(_ context.Context, records []outbox.Record) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.records == nil {
		r.records = make(map[string]outbox.Record)
	}
	for _, record := range records {
		key := record.AppCode + "\x00" + record.EventID
		if _, exists := r.records[key]; !exists {
			r.records[key] = record
		}
	}
	return nil
}

func (r *luckyGiftDisplayRepository) snapshot() []outbox.Record {
	r.mu.Lock()
	defer r.mu.Unlock()
	result := make([]outbox.Record, 0, len(r.records))
	for _, record := range r.records {
		result = append(result, record)
	}
	return result
}

func TestHandleLuckyGiftOutboxEventPersistsOneDerivedRoomDisplayOutbox(t *testing.T) {
	repository := &luckyGiftDisplayRepository{}
	svc := &Service{repository: repository, outboxWake: make(chan struct{}, 4)}
	envelope := luckyGiftDisplayEnvelope(t, luckygiftmq.LuckyGiftDisplayMinMultiplierPPM)
	ctx := appcode.WithContext(context.Background(), "lalu")

	if err := svc.HandleLuckyGiftOutboxEvent(ctx, envelope); err != nil {
		t.Fatalf("HandleLuckyGiftOutboxEvent failed: %v", err)
	}
	if err := svc.HandleLuckyGiftOutboxEvent(ctx, envelope); err != nil {
		t.Fatalf("replayed HandleLuckyGiftOutboxEvent failed: %v", err)
	}

	records := repository.snapshot()
	if len(records) != 1 {
		t.Fatalf("owner event replay must keep one room outbox row, got %d", len(records))
	}
	record := records[0]
	if record.AppCode != "lalu" || record.EventID != "lucky_gift_drawn:lucky_draw_room_1" || record.EventType != roomLuckyGiftDrawnEventType || record.RoomID != "room-lucky-display" || record.Envelope.GetRoomVersion() != 0 {
		t.Fatalf("derived room outbox identity mismatch: %+v envelope=%+v", record, record.Envelope)
	}
	var body roomeventsv1.RoomLuckyGiftDrawn
	if err := proto.Unmarshal(record.Envelope.GetBody(), &body); err != nil {
		t.Fatalf("decode derived RoomLuckyGiftDrawn failed: %v", err)
	}
	if body.GetDrawId() != "lucky_draw_room_1" || body.GetSenderName() != "Sender" || body.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || body.GetRewardStatus() != "granted" || !body.GetRoomLocked() || body.GetOwnerUserId() != 323411852102995968 {
		t.Fatalf("derived RoomLuckyGiftDrawn mismatch: %+v", &body)
	}
}

func TestHandleLuckyGiftOutboxEventSkipsRewardBelowDisplayThreshold(t *testing.T) {
	repository := &luckyGiftDisplayRepository{}
	svc := &Service{repository: repository, outboxWake: make(chan struct{}, 1)}
	envelope := luckyGiftDisplayEnvelope(t, luckygiftmq.LuckyGiftDisplayMinMultiplierPPM-1)

	if err := svc.HandleLuckyGiftOutboxEvent(context.Background(), envelope); err != nil {
		t.Fatalf("below-threshold owner fact should be acknowledged: %v", err)
	}
	if records := repository.snapshot(); len(records) != 0 {
		t.Fatalf("below-threshold owner fact must have no room display side effect: %+v", records)
	}
}

func TestHandleLuckyGiftOutboxEventRejectsUnstableEventID(t *testing.T) {
	repository := &luckyGiftDisplayRepository{}
	svc := &Service{repository: repository, outboxWake: make(chan struct{}, 1)}
	envelope := luckyGiftDisplayEnvelope(t, luckygiftmq.LuckyGiftDisplayMinMultiplierPPM)
	envelope.EventId = "evt_random"

	if err := svc.HandleLuckyGiftOutboxEvent(context.Background(), envelope); err == nil {
		t.Fatal("owner fact with random event_id must be rejected")
	}
	if records := repository.snapshot(); len(records) != 0 {
		t.Fatalf("invalid owner fact must not persist room display outbox: %+v", records)
	}
}

func luckyGiftDisplayEnvelope(t *testing.T, multiplierPPM int64) *luckygifteventsv1.EventEnvelope {
	t.Helper()
	drawn := &luckygifteventsv1.LuckyGiftDrawn{
		DrawId:                    "lucky_draw_room_1",
		CommandId:                 "cmd_lucky_room_1",
		RoomId:                    "room-lucky-display",
		PoolId:                    "advanced",
		GiftId:                    "43",
		GiftCount:                 1,
		SenderUserId:              123456,
		TargetUserId:              654321,
		SenderName:                "Sender",
		SenderAvatar:              "https://cdn.example.test/sender.png",
		SenderDisplayUserId:       "123456",
		SenderPrettyDisplayUserId: "VIP123456",
		VisibleRegionId:           1,
		RoomLocked:                true,
		OwnerUserId:               323411852102995968,
		CountryId:                 86,
		CoinSpent:                 100,
		RuleVersion:               3,
		ExperiencePool:            "normal",
		SelectedTierId:            "10x",
		MultiplierPpm:             multiplierPPM,
		BaseRewardCoins:           1000,
		EffectiveRewardCoins:      1000,
		HighMultiplier:            true,
		RewardStatus:              "granted",
		WalletTransactionId:       "wallet-lucky-room-1",
		DrawCreatedAtMs:           1_783_940_398_274,
		RewardGrantedAtMs:         1_783_940_398_300,
	}
	body, err := proto.Marshal(drawn)
	if err != nil {
		t.Fatalf("marshal LuckyGiftDrawn failed: %v", err)
	}
	return &luckygifteventsv1.EventEnvelope{
		EventId:      "lucky_gift_drawn:lucky_draw_room_1",
		EventType:    luckygiftmq.EventTypeLuckyGiftDrawn,
		AppCode:      "lalu",
		DrawId:       drawn.GetDrawId(),
		OccurredAtMs: drawn.GetDrawCreatedAtMs(),
		Body:         body,
	}
}
