package roomturnoverreward

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

	"google.golang.org/grpc"
	"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/appcode"
	domain "hyapp/services/activity-service/internal/domain/roomturnoverreward"
	messageservice "hyapp/services/activity-service/internal/service/message"
)

func TestWeekWindowUsesUTCOneMondayBoundary(t *testing.T) {
	cases := []struct {
		name      string
		now       string
		wantStart string
		wantEnd   string
	}{
		{
			name:      "monday zero still belongs to previous week",
			now:       "2026-06-01T00:00:00Z",
			wantStart: "2026-05-25T01:00:00Z",
			wantEnd:   "2026-06-01T01:00:00Z",
		},
		{
			name:      "monday one starts new contribution week",
			now:       "2026-06-01T01:00:00Z",
			wantStart: "2026-06-01T01:00:00Z",
			wantEnd:   "2026-06-08T01:00:00Z",
		},
		{
			name:      "sunday belongs to current week",
			now:       "2026-06-07T23:59:59Z",
			wantStart: "2026-06-01T01:00:00Z",
			wantEnd:   "2026-06-08T01:00:00Z",
		},
		{
			name:      "cross month keeps utc week",
			now:       "2026-07-01T12:30:00Z",
			wantStart: "2026-06-29T01:00:00Z",
			wantEnd:   "2026-07-06T01:00:00Z",
		},
		{
			name:      "cross year keeps utc week",
			now:       "2027-01-01T08:00:00Z",
			wantStart: "2026-12-28T01:00:00Z",
			wantEnd:   "2027-01-04T01:00:00Z",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			now := mustParseTime(t, tc.now)
			start, end := WeekWindow(now)
			if got := start.Format(time.RFC3339); got != tc.wantStart {
				t.Fatalf("unexpected start: got %s want %s", got, tc.wantStart)
			}
			if got := end.Format(time.RFC3339); got != tc.wantEnd {
				t.Fatalf("unexpected end: got %s want %s", got, tc.wantEnd)
			}
		})
	}
}

func TestPreviousWeekWindowReturnsCompletedUTCWeek(t *testing.T) {
	start, end := PreviousWeekWindow(mustParseTime(t, "2026-06-08T01:00:00Z"))
	if got := start.Format(time.RFC3339); got != "2026-06-01T01:00:00Z" {
		t.Fatalf("unexpected previous start: %s", got)
	}
	if got := end.Format(time.RFC3339); got != "2026-06-08T01:00:00Z" {
		t.Fatalf("unexpected previous end: %s", got)
	}
}

func TestMatchHighestTierReturnsHighestThreshold(t *testing.T) {
	tiers := []domain.Tier{
		{TierID: 1, TierCode: "bronze", ThresholdCoinSpent: 100, RewardCoinAmount: 10, Status: domain.TierStatusActive},
		{TierID: 2, TierCode: "silver", ThresholdCoinSpent: 500, RewardCoinAmount: 60, Status: domain.TierStatusActive},
		{TierID: 3, TierCode: "gold", ThresholdCoinSpent: 1000, RewardCoinAmount: 160, Status: domain.TierStatusActive},
		{TierID: 4, TierCode: "hidden", ThresholdCoinSpent: 700, RewardCoinAmount: 90, Status: domain.TierStatusInactive},
	}

	tier, ok := MatchHighestTier(tiers, 850)
	if !ok {
		t.Fatal("expected a matched tier")
	}
	if tier.TierCode != "silver" {
		t.Fatalf("unexpected tier: got %s want silver", tier.TierCode)
	}
}

func TestRetrySettlementRejectsTierNotMatchedWithoutGrantSideEffects(t *testing.T) {
	repo := &retryBoundaryRepository{
		settlement: domain.Settlement{
			SettlementID:     "settlement-tier-not-matched",
			RoomID:           "room-1",
			Status:           domain.SettlementStatusFailed,
			FailureReason:    "tier_not_matched",
			WalletCommandID:  "room_turnover_reward:lalu:1:room-1",
			RewardCoinAmount: 0,
		},
	}
	room := &retryBoundaryRoom{ownerID: 10001}
	wallet := &retryBoundaryWallet{}
	svc := New(repo, wallet, room)

	got, err := svc.RetrySettlement(context.Background(), repo.settlement.SettlementID)
	if err == nil {
		t.Fatal("expected retry to reject tier_not_matched settlement")
	}
	if got.Status != domain.SettlementStatusFailed || got.FailureReason != "tier_not_matched" {
		t.Fatalf("retry should return original failed settlement, got %+v", got)
	}
	if repo.resetCalls != 0 || repo.grantCalls != 0 || repo.failCalls != 0 {
		t.Fatalf("tier_not_matched retry must not mutate settlement: reset=%d grant=%d fail=%d", repo.resetCalls, repo.grantCalls, repo.failCalls)
	}
	if room.calls != 0 {
		t.Fatalf("tier_not_matched retry must not fetch room owner, calls=%d", room.calls)
	}
	if wallet.calls != 0 {
		t.Fatalf("tier_not_matched retry must not call wallet, calls=%d", wallet.calls)
	}
}

func TestHandleRoomEventConsumesGiftValueAsHeatValue(t *testing.T) {
	repo := &roomGiftEventRepository{}
	svc := New(repo, nil, nil)
	svc.SetClock(func() time.Time { return mustParseTime(t, "2026-06-03T12:00:00Z") })
	body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{
		GiftValue:       900,
		CoinSpent:       1200,
		VisibleRegionId: 12,
	})
	if err != nil {
		t.Fatalf("marshal gift: %v", err)
	}
	result, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
		EventId:      "room-gift-heat-value",
		EventType:    "RoomGiftSent",
		RoomId:       "room-1",
		AppCode:      appcode.Default,
		OccurredAtMs: mustParseTime(t, "2026-06-03T10:00:00Z").UnixMilli(),
		Body:         body,
	})
	if err != nil {
		t.Fatalf("HandleRoomEvent returned error: %v", err)
	}
	if result.Status != domain.EventStatusConsumed {
		t.Fatalf("unexpected result: %+v", result)
	}
	if repo.event.CoinSpent != 900 || repo.event.RegionID != 12 {
		t.Fatalf("room turnover reward must consume gift_value/HeatValue, got %+v", repo.event)
	}
	if got := time.UnixMilli(repo.periodStartMS).UTC().Format(time.RFC3339); got != "2026-06-01T01:00:00Z" {
		t.Fatalf("unexpected period start: %s", got)
	}
}

func TestGetStatusFallsBackToGlobalConfigUnlessRegionOverrides(t *testing.T) {
	repo := &roomGiftEventRepository{configs: map[int64]domain.Config{
		0: {RegionID: 0, Enabled: true, Tiers: []domain.Tier{{TierID: 1, Status: domain.TierStatusActive, ThresholdCoinSpent: 100, RewardCoinAmount: 10}}},
	}}
	svc := New(repo, nil, nil)
	svc.SetClock(func() time.Time { return mustParseTime(t, "2026-06-03T12:00:00Z") })

	status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42, 12, "", 42)
	if err != nil {
		t.Fatalf("GetStatus returned error: %v", err)
	}
	if status.Config.RegionID != 0 || !status.Config.Enabled {
		t.Fatalf("region without override must use global config, got %+v", status.Config)
	}
	if len(repo.configLookups) != 2 || repo.configLookups[0] != 12 || repo.configLookups[1] != 0 {
		t.Fatalf("expected exact-region then global lookup, got %v", repo.configLookups)
	}

	repo.configs[12] = domain.Config{RegionID: 12, Enabled: false}
	repo.configLookups = nil
	status, err = svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42, 12, "", 42)
	if err != nil {
		t.Fatalf("GetStatus with override returned error: %v", err)
	}
	if status.Config.RegionID != 12 || status.Config.Enabled {
		t.Fatalf("explicit disabled regional config must block global fallback, got %+v", status.Config)
	}
	if len(repo.configLookups) != 1 || repo.configLookups[0] != 12 {
		t.Fatalf("regional override should stop fallback lookup, got %v", repo.configLookups)
	}
}

func TestGrantSettlementCreatesSystemNoticeBeforeMarkGranted(t *testing.T) {
	repo := &retryBoundaryRepository{
		settlement: roomTurnoverRewardGrantableSettlement(),
	}
	wallet := &retryBoundaryWallet{}
	notices := &fakeRewardNoticeService{}
	svc := New(repo, wallet, nil, WithNoticeService(notices))
	svc.SetClock(func() time.Time { return mustParseTime(t, "2026-06-09T01:00:00Z") })

	granted, err := svc.grantSettlement(appcode.WithContext(context.Background(), "lalu"), repo.settlement)
	if err != nil {
		t.Fatalf("grantSettlement returned error: %v", err)
	}
	if wallet.calls != 1 || repo.grantCalls != 1 || repo.failCalls != 0 {
		t.Fatalf("grant path mismatch: wallet=%d grant=%d fail=%d", wallet.calls, repo.grantCalls, repo.failCalls)
	}
	if notices.calls != 1 {
		t.Fatalf("expected one system notice, got %d", notices.calls)
	}
	if granted.Status != domain.SettlementStatusGranted || granted.WalletTransactionID != "wallet-tx-1" {
		t.Fatalf("settlement should be granted with wallet transaction, got %+v", granted)
	}

	cmd := notices.command
	if cmd.TargetUserID != repo.settlement.OwnerUserID || cmd.Producer != rewardNoticeProducer || cmd.ProducerEventID != repo.settlement.SettlementID {
		t.Fatalf("notice idempotency or target mismatch: %+v", cmd)
	}
	if cmd.ProducerEventType != rewardNoticeEventType || cmd.AggregateType != rewardNoticeAggregateType || cmd.AggregateID != repo.settlement.SettlementID {
		t.Fatalf("notice aggregate mismatch: %+v", cmd)
	}
	wantSummary := "Congratulations! You earned 123 coins from this week's room turnover reward."
	if cmd.Title != "Room turnover reward" || cmd.Summary != wantSummary || cmd.Body != wantSummary {
		t.Fatalf("notice content mismatch: title=%q summary=%q body=%q", cmd.Title, cmd.Summary, cmd.Body)
	}
	if cmd.SentAtMS != 1234 {
		t.Fatalf("notice sent_at_ms should use wallet grant time, got %d", cmd.SentAtMS)
	}
	if cmd.Metadata["settlement_id"] != repo.settlement.SettlementID ||
		cmd.Metadata["room_id"] != repo.settlement.RoomID ||
		cmd.Metadata["reward_coin_amount"] != repo.settlement.RewardCoinAmount ||
		cmd.Metadata["wallet_transaction_id"] != "wallet-tx-1" {
		t.Fatalf("notice metadata mismatch: %+v", cmd.Metadata)
	}
}

func TestGrantSettlementFailsWhenSystemNoticeCannotBeCreated(t *testing.T) {
	repo := &retryBoundaryRepository{
		settlement: roomTurnoverRewardGrantableSettlement(),
	}
	wallet := &retryBoundaryWallet{}
	notices := &fakeRewardNoticeService{err: errors.New("message unavailable")}
	svc := New(repo, wallet, nil, WithNoticeService(notices))

	if _, err := svc.grantSettlement(appcode.WithContext(context.Background(), "lalu"), repo.settlement); err == nil {
		t.Fatal("expected grantSettlement to fail when notice creation fails")
	}
	if wallet.calls != 1 || notices.calls != 1 {
		t.Fatalf("wallet should be credited once and notice attempted once, wallet=%d notices=%d", wallet.calls, notices.calls)
	}
	if repo.grantCalls != 0 || repo.failCalls != 1 || repo.settlement.Status != domain.SettlementStatusFailed {
		t.Fatalf("notice failure must not mark granted: grant=%d fail=%d settlement=%+v", repo.grantCalls, repo.failCalls, repo.settlement)
	}
}

func roomTurnoverRewardGrantableSettlement() domain.Settlement {
	return domain.Settlement{
		SettlementID:       "settlement-room-turnover-1",
		AppCode:            "lalu",
		RegionID:           12,
		RoomID:             "room-1",
		OwnerUserID:        42,
		PeriodStartMS:      mustUnixMilli("2026-06-01T01:00:00Z"),
		PeriodEndMS:        mustUnixMilli("2026-06-08T01:00:00Z"),
		CoinSpent:          9000,
		TierID:             2,
		TierCode:           "silver",
		ThresholdCoinSpent: 5000,
		RewardCoinAmount:   123,
		Status:             domain.SettlementStatusPending,
		WalletCommandID:    "room_turnover_reward:lalu:20260601:room-1",
	}
}

func mustUnixMilli(value string) int64 {
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		panic(err)
	}
	return parsed.UnixMilli()
}

func mustParseTime(t *testing.T, value string) time.Time {
	t.Helper()
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		t.Fatalf("parse time %q: %v", value, err)
	}
	return parsed
}

type retryBoundaryRepository struct {
	settlement domain.Settlement
	resetCalls int
	grantCalls int
	failCalls  int
}

type roomGiftEventRepository struct {
	event         domain.RoomGiftEvent
	periodStartMS int64
	periodEndMS   int64
	configs       map[int64]domain.Config
	configLookups []int64
}

func (r *roomGiftEventRepository) GetRoomTurnoverRewardConfig(_ context.Context, regionID int64) (domain.Config, bool, error) {
	r.configLookups = append(r.configLookups, regionID)
	config, exists := r.configs[regionID]
	return config, exists, nil
}

func (r *roomGiftEventRepository) UpdateRoomTurnoverRewardConfig(context.Context, domain.Config, int64) (domain.Config, error) {
	return domain.Config{}, nil
}

func (r *roomGiftEventRepository) ConsumeRoomTurnoverGiftEvent(_ context.Context, event domain.RoomGiftEvent, periodStartMS int64, periodEndMS int64, _ int64) (domain.EventResult, error) {
	r.event = event
	r.periodStartMS = periodStartMS
	r.periodEndMS = periodEndMS
	return domain.EventResult{EventID: event.EventID, Status: domain.EventStatusConsumed}, nil
}

func (r *roomGiftEventRepository) GetRoomTurnoverRewardProgress(context.Context, int64, string, int64) (domain.Progress, bool, error) {
	return domain.Progress{}, false, nil
}

func (r *roomGiftEventRepository) GetLatestRoomTurnoverRewardSettlement(context.Context, int64, string) (domain.Settlement, bool, error) {
	return domain.Settlement{}, false, nil
}

func (r *roomGiftEventRepository) ListRoomTurnoverRewardSettlements(context.Context, domain.SettlementQuery) ([]domain.Settlement, int64, error) {
	return nil, 0, nil
}

func (r *roomGiftEventRepository) ListUnsettledRoomTurnoverRewardProgress(context.Context, int64, int) ([]domain.Progress, error) {
	return nil, nil
}

func (r *roomGiftEventRepository) InsertRoomTurnoverRewardSettlement(context.Context, domain.Settlement, int64) (domain.Settlement, bool, error) {
	return domain.Settlement{}, false, nil
}

func (r *roomGiftEventRepository) ListPendingRoomTurnoverRewardSettlements(context.Context, int) ([]domain.Settlement, error) {
	return nil, nil
}

func (r *roomGiftEventRepository) GetRoomTurnoverRewardSettlement(context.Context, string) (domain.Settlement, bool, error) {
	return domain.Settlement{}, false, nil
}

func (r *roomGiftEventRepository) ResetRoomTurnoverRewardSettlementForRetry(context.Context, string, int64, int64) (domain.Settlement, error) {
	return domain.Settlement{}, nil
}

func (r *roomGiftEventRepository) MarkRoomTurnoverRewardSettlementGranted(context.Context, string, string, int64) (domain.Settlement, error) {
	return domain.Settlement{}, nil
}

func (r *roomGiftEventRepository) MarkRoomTurnoverRewardSettlementFailed(context.Context, string, string, int64) error {
	return nil
}

func (r *retryBoundaryRepository) GetRoomTurnoverRewardConfig(context.Context, int64) (domain.Config, bool, error) {
	panic("unexpected GetRoomTurnoverRewardConfig")
}

func (r *retryBoundaryRepository) UpdateRoomTurnoverRewardConfig(context.Context, domain.Config, int64) (domain.Config, error) {
	panic("unexpected UpdateRoomTurnoverRewardConfig")
}

func (r *retryBoundaryRepository) ConsumeRoomTurnoverGiftEvent(context.Context, domain.RoomGiftEvent, int64, int64, int64) (domain.EventResult, error) {
	panic("unexpected ConsumeRoomTurnoverGiftEvent")
}

func (r *retryBoundaryRepository) GetRoomTurnoverRewardProgress(context.Context, int64, string, int64) (domain.Progress, bool, error) {
	panic("unexpected GetRoomTurnoverRewardProgress")
}

func (r *retryBoundaryRepository) GetLatestRoomTurnoverRewardSettlement(context.Context, int64, string) (domain.Settlement, bool, error) {
	panic("unexpected GetLatestRoomTurnoverRewardSettlement")
}

func (r *retryBoundaryRepository) ListRoomTurnoverRewardSettlements(context.Context, domain.SettlementQuery) ([]domain.Settlement, int64, error) {
	panic("unexpected ListRoomTurnoverRewardSettlements")
}

func (r *retryBoundaryRepository) ListUnsettledRoomTurnoverRewardProgress(context.Context, int64, int) ([]domain.Progress, error) {
	panic("unexpected ListUnsettledRoomTurnoverRewardProgress")
}

func (r *retryBoundaryRepository) InsertRoomTurnoverRewardSettlement(context.Context, domain.Settlement, int64) (domain.Settlement, bool, error) {
	panic("unexpected InsertRoomTurnoverRewardSettlement")
}

func (r *retryBoundaryRepository) ListPendingRoomTurnoverRewardSettlements(context.Context, int) ([]domain.Settlement, error) {
	panic("unexpected ListPendingRoomTurnoverRewardSettlements")
}

func (r *retryBoundaryRepository) GetRoomTurnoverRewardSettlement(_ context.Context, settlementID string) (domain.Settlement, bool, error) {
	if settlementID != r.settlement.SettlementID {
		return domain.Settlement{}, false, nil
	}
	return r.settlement, true, nil
}

func (r *retryBoundaryRepository) ResetRoomTurnoverRewardSettlementForRetry(_ context.Context, _ string, ownerUserID int64, _ int64) (domain.Settlement, error) {
	r.resetCalls++
	r.settlement.Status = domain.SettlementStatusPending
	r.settlement.OwnerUserID = ownerUserID
	r.settlement.FailureReason = ""
	return r.settlement, nil
}

func (r *retryBoundaryRepository) MarkRoomTurnoverRewardSettlementGranted(_ context.Context, _ string, walletTransactionID string, _ int64) (domain.Settlement, error) {
	r.grantCalls++
	r.settlement.Status = domain.SettlementStatusGranted
	r.settlement.WalletTransactionID = walletTransactionID
	return r.settlement, nil
}

func (r *retryBoundaryRepository) MarkRoomTurnoverRewardSettlementFailed(_ context.Context, _ string, reason string, _ int64) error {
	r.failCalls++
	r.settlement.Status = domain.SettlementStatusFailed
	r.settlement.FailureReason = reason
	return nil
}

type retryBoundaryRoom struct {
	ownerID int64
	calls   int
}

func (r *retryBoundaryRoom) AdminGetRoom(context.Context, *roomv1.AdminGetRoomRequest, ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error) {
	r.calls++
	return &roomv1.AdminGetRoomResponse{Room: &roomv1.AdminRoomListItem{OwnerUserId: r.ownerID}}, nil
}

type retryBoundaryWallet struct {
	calls int
	req   *walletv1.CreditRoomTurnoverRewardRequest
}

func (w *retryBoundaryWallet) CreditRoomTurnoverReward(_ context.Context, req *walletv1.CreditRoomTurnoverRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditRoomTurnoverRewardResponse, error) {
	w.calls++
	w.req = req
	return &walletv1.CreditRoomTurnoverRewardResponse{TransactionId: "wallet-tx-1", GrantedAtMs: 1234}, nil
}

type fakeRewardNoticeService struct {
	calls   int
	command messageservice.NoticeCommand
	err     error
}

func (s *fakeRewardNoticeService) CreateSystemNotice(_ context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error) {
	s.calls++
	s.command = cmd
	if s.err != nil {
		return messageservice.NoticeResult{}, s.err
	}
	return messageservice.NoticeResult{Created: true}, nil
}
