package luckygift

import (
	"context"
	"errors"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
	"hyapp/pkg/luckygiftmq"
	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

type captureLuckyGiftEventPublisher struct {
	envelope *luckygifteventsv1.EventEnvelope
	err      error
}

func (p *captureLuckyGiftEventPublisher) PublishLuckyGiftEvent(_ context.Context, envelope *luckygifteventsv1.EventEnvelope) error {
	p.envelope = proto.Clone(envelope).(*luckygifteventsv1.EventEnvelope)
	return p.err
}

// settlementEventRepository embeds the full port only to keep this test focused on the methods used by the granted branch.
// Any unexpected repository call still panics through the nil embedded interface and exposes a widened code path immediately.
type settlementEventRepository struct {
	Repository
	rewardState      domain.DrawRewardState
	deliveredOutbox  string
	retryableOutbox  string
	failedOutbox     string
	failedDrawUpdate bool
}

func (r *settlementEventRepository) GetLuckyGiftDrawRewardState(context.Context, string, []string) (domain.DrawRewardState, error) {
	return r.rewardState, nil
}

func (r *settlementEventRepository) MarkLuckyGiftOutboxDelivered(_ context.Context, event domain.DrawOutbox, _ int64) error {
	r.deliveredOutbox = event.OutboxID
	return nil
}

func (r *settlementEventRepository) MarkLuckyGiftOutboxRetryable(_ context.Context, event domain.DrawOutbox, _ int, _ int64, _ string, _ int64) error {
	r.retryableOutbox = event.OutboxID
	return nil
}

func (r *settlementEventRepository) MarkLuckyGiftOutboxFailed(_ context.Context, event domain.DrawOutbox, _ int, _ string, _ int64) error {
	r.failedOutbox = event.OutboxID
	return nil
}

func (r *settlementEventRepository) MarkLuckyGiftDrawsFailed(context.Context, string, []string, string, int64) error {
	r.failedDrawUpdate = true
	return nil
}

func TestPublishLuckyGiftDrawnFactCarriesGrantedSnapshot(t *testing.T) {
	publisher := &captureLuckyGiftEventPublisher{}
	service := New(nil, WithEventPublisher(publisher))
	service.SetClock(func() time.Time { return time.UnixMilli(1783940398374).UTC() })
	payload := testLuckyGiftDrawnPayload()

	if err := service.publishLuckyGiftDrawnFact(context.Background(), payload, luckyGiftRewardReceipt{WalletTransactionID: "wallet-tx-1", RewardGrantedAtMS: 1783940398320}); err != nil {
		t.Fatalf("publish lucky gift fact: %v", err)
	}
	if publisher.envelope == nil || publisher.envelope.GetEventType() != luckygiftmq.EventTypeLuckyGiftDrawn || publisher.envelope.GetOccurredAtMs() != payload.CreatedAtMS {
		t.Fatalf("unexpected owner envelope: %+v", publisher.envelope)
	}
	var fact luckygifteventsv1.LuckyGiftDrawn
	if err := proto.Unmarshal(publisher.envelope.GetBody(), &fact); err != nil {
		t.Fatalf("decode owner fact: %v", err)
	}
	if fact.GetRewardStatus() != domain.StatusGranted || fact.GetWalletTransactionId() != "wallet-tx-1" || fact.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || fact.GetSenderName() != "Lucky Sender" || !fact.GetRoomLocked() || fact.GetOwnerUserId() != 323411852102995968 || fact.GetRewardGrantedAtMs() != 1783940398320 {
		t.Fatalf("unexpected owner fact: %+v", &fact)
	}
}

func TestPublishLuckyGiftDrawnFactIsStableAcrossBrokerRetries(t *testing.T) {
	publisher := &captureLuckyGiftEventPublisher{}
	service := New(nil, WithEventPublisher(publisher))
	payload := testLuckyGiftDrawnPayload()
	receipt := luckyGiftRewardReceipt{WalletTransactionID: "wallet-tx-stable", RewardGrantedAtMS: 1783940398320}

	service.SetClock(func() time.Time { return time.UnixMilli(1783940400000).UTC() })
	if err := service.publishLuckyGiftDrawnFact(context.Background(), payload, receipt); err != nil {
		t.Fatalf("first publish: %v", err)
	}
	first := proto.Clone(publisher.envelope).(*luckygifteventsv1.EventEnvelope)

	service.SetClock(func() time.Time { return time.UnixMilli(1783940500000).UTC() })
	if err := service.publishLuckyGiftDrawnFact(context.Background(), payload, receipt); err != nil {
		t.Fatalf("retry publish: %v", err)
	}
	if !proto.Equal(first, publisher.envelope) {
		t.Fatalf("same event_id changed across retry:\nfirst=%+v\nretry=%+v", first, publisher.envelope)
	}
}

func TestGrantedDrawKeepsSettlementRetryableUntilBrokerAcceptsFact(t *testing.T) {
	repository := &settlementEventRepository{rewardState: domain.DrawRewardState{AllGranted: true, WalletTransactionID: "wallet-tx-existing", RewardGrantedAtMS: 1783940398320}}
	publisher := &captureLuckyGiftEventPublisher{err: errors.New("broker unavailable")}
	service := New(repository, WithEventPublisher(publisher))
	service.SetClock(func() time.Time { return time.UnixMilli(1783940398374).UTC() })
	event := domain.DrawOutbox{AppCode: "lalu", OutboxID: "lucky_reward_draw-10x", EventType: domain.EventTypeLuckyGiftRewardSettlement}

	err := service.processRewardSettlementOutbox(context.Background(), event, testLuckyGiftDrawnPayload(), WorkerOptions{MaxRetry: 8})
	if err != nil {
		t.Fatalf("retry transition should be persisted without returning a second error: %v", err)
	}
	if repository.retryableOutbox != event.OutboxID || repository.deliveredOutbox != "" || repository.failedOutbox != "" {
		t.Fatalf("unexpected outbox transition: retryable=%q delivered=%q failed=%q", repository.retryableOutbox, repository.deliveredOutbox, repository.failedOutbox)
	}
	if repository.failedDrawUpdate {
		t.Fatal("broker failure must never overwrite an already granted draw")
	}
	if publisher.envelope == nil {
		t.Fatal("granted retry must still publish the owner fact")
	}
}

func TestGrantedDrawBrokerRetryExhaustionNeverMarksCreditedDrawFailed(t *testing.T) {
	repository := &settlementEventRepository{rewardState: domain.DrawRewardState{AllGranted: true, WalletTransactionID: "wallet-tx-existing", RewardGrantedAtMS: 1783940398320}}
	publisher := &captureLuckyGiftEventPublisher{err: errors.New("broker outage exceeded realtime retry window")}
	service := New(repository, WithEventPublisher(publisher))
	service.SetClock(func() time.Time { return time.UnixMilli(1783940398374).UTC() })
	event := domain.DrawOutbox{
		AppCode:     "lalu",
		OutboxID:    "lucky_reward_draw-10x",
		EventType:   domain.EventTypeLuckyGiftRewardSettlement,
		PayloadJSON: `{"draw_id":"draw-10x"}`,
		RetryCount:  7,
	}

	if err := service.processRewardSettlementOutbox(context.Background(), event, testLuckyGiftDrawnPayload(), WorkerOptions{MaxRetry: 8}); err != nil {
		t.Fatalf("terminal outbox transition: %v", err)
	}
	if repository.failedOutbox != event.OutboxID || repository.retryableOutbox != "" || repository.deliveredOutbox != "" {
		t.Fatalf("unexpected terminal transition: failed=%q retryable=%q delivered=%q", repository.failedOutbox, repository.retryableOutbox, repository.deliveredOutbox)
	}
	if repository.failedDrawUpdate {
		t.Fatal("credited draw must remain granted when realtime fact exhausts retries")
	}
}

func testLuckyGiftDrawnPayload() luckyGiftDrawnPayload {
	return luckyGiftDrawnPayload{
		EventType:            "lucky_gift_drawn",
		EventID:              "lucky_gift_drawn:draw-10x",
		AppCode:              "lalu",
		DrawID:               "draw-10x",
		DrawIDs:              []string{"draw-10x"},
		CommandID:            "cmd-10x",
		PoolID:               "super_lucky",
		RoomID:               "room-1",
		GiftID:               "43",
		GiftCount:            1,
		UserID:               123456,
		SenderUserID:         123456,
		TargetUserID:         654321,
		SenderName:           "Lucky Sender",
		SenderAvatar:         "https://cdn.example/sender.png",
		SenderDisplayUserID:  "123456",
		VisibleRegionID:      1,
		RoomLocked:           true,
		OwnerUserID:          323411852102995968,
		CountryID:            86,
		CoinSpent:            100,
		MultiplierPPM:        luckygiftmq.LuckyGiftDisplayMinMultiplierPPM,
		BaseRewardCoins:      1000,
		EffectiveRewardCoins: 1000,
		CreatedAtMS:          1783940398274,
	}
}
