package broadcast

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/tencentim"
	broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
)

func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
	repository := newFakeRepository()
	service := New(Config{NodeID: "node-a", SuperGiftMinValue: 100}, repository, nil, nil)
	service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_000_000) })
	envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
		SenderUserId:    42,
		TargetUserId:    43,
		GiftId:          "rocket",
		GiftCount:       2,
		GiftValue:       999,
		VisibleRegionId: 1001,
		CommandId:       "cmd-send-gift-1",
	})

	result, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
	if err != nil {
		t.Fatalf("HandleRoomEvent failed: %v", err)
	}
	if result.Status != broadcastdomain.StatusPending || !result.BroadcastCreated || result.BroadcastEventID != "gift_broadcast:room-1001:cmd-send-gift-1" {
		t.Fatalf("unexpected result: %+v", result)
	}
	record := repository.records[result.BroadcastEventID]
	if record.GroupID != "hy_lalu_bc_r_1001" || record.BroadcastType != broadcastdomain.TypeSuperGift {
		t.Fatalf("unexpected broadcast record: %+v", record)
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
		t.Fatalf("payload is not json: %v", err)
	}
	if payload["room_id"] != "room-1001" || payload["gift_id"] != "rocket" || payload["scope"] != "region" {
		t.Fatalf("payload mismatch: %+v", payload)
	}

	again, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
	if err != nil {
		t.Fatalf("second HandleRoomEvent failed: %v", err)
	}
	if again.BroadcastCreated {
		t.Fatalf("second consume must be idempotent: %+v", again)
	}
}

func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
	service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
	belowThreshold := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
	result, err := service.HandleRoomEvent(context.Background(), belowThreshold)
	if err != nil {
		t.Fatalf("below threshold failed: %v", err)
	}
	if result.Status != broadcastdomain.StatusSkipped || result.BroadcastEventID != "" {
		t.Fatalf("below threshold should skip: %+v", result)
	}

	globalRegion := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 1000, VisibleRegionId: 0})
	result, err = service.HandleRoomEvent(context.Background(), globalRegion)
	if err != nil {
		t.Fatalf("global region failed: %v", err)
	}
	if result.Status != broadcastdomain.StatusSkipped || result.BroadcastEventID != "" {
		t.Fatalf("global region should skip: %+v", result)
	}
}

func TestProcessPendingBroadcastsPublishesCustomMessage(t *testing.T) {
	repository := newFakeRepository()
	publisher := &fakePublisher{}
	service := New(Config{NodeID: "node-a"}, repository, publisher, nil)
	repository.records["evt-1"] = broadcastdomain.OutboxRecord{
		AppCode:       "lalu",
		EventID:       "evt-1",
		GroupID:       "hy_lalu_bc_g",
		BroadcastType: broadcastdomain.TypeRedPacket,
		PayloadJSON:   `{"event_id":"evt-1","broadcast_type":"red_packet"}`,
		Status:        broadcastdomain.StatusPending,
	}

	result, err := service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
	if err != nil {
		t.Fatalf("ProcessPendingBroadcasts failed: %v", err)
	}
	if result.SuccessCount != 1 || len(publisher.messages) != 1 {
		t.Fatalf("publish result mismatch: result=%+v messages=%d", result, len(publisher.messages))
	}
	if repository.records["evt-1"].Status != broadcastdomain.StatusDelivered {
		t.Fatalf("record should be delivered: %+v", repository.records["evt-1"])
	}
	if publisher.messages[0].GroupID != "hy_lalu_bc_g" || publisher.messages[0].Desc != broadcastdomain.TypeRedPacket {
		t.Fatalf("message mismatch: %+v", publisher.messages[0])
	}
}

func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) {
	publisher := &fakePublisher{}
	service := New(Config{NodeID: "node-a"}, newFakeRepository(), publisher, nil)

	groupID, removed, err := service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 1001)
	if err != nil {
		t.Fatalf("RemoveRegionBroadcastMember failed: %v", err)
	}
	if !removed || groupID != "hy_lalu_bc_r_1001" {
		t.Fatalf("unexpected remove result: group_id=%s removed=%t", groupID, removed)
	}
	if publisher.deletedGroupID != "hy_lalu_bc_r_1001" || publisher.deletedUserID != 42 {
		t.Fatalf("unexpected deleted member: group_id=%s user_id=%d", publisher.deletedGroupID, publisher.deletedUserID)
	}

	groupID, removed, err = service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 0)
	if err != nil {
		t.Fatalf("zero region should be a no-op, got: %v", err)
	}
	if removed || groupID != "" {
		t.Fatalf("zero region should not delete any group member: group_id=%s removed=%t", groupID, removed)
	}
}

func mustGiftEnvelope(t *testing.T, gift *roomeventsv1.RoomGiftSent) *roomeventsv1.EventEnvelope {
	t.Helper()
	body, err := proto.Marshal(gift)
	if err != nil {
		t.Fatalf("marshal gift failed: %v", err)
	}
	return &roomeventsv1.EventEnvelope{
		EventId:      "evt-room-gift-1",
		RoomId:       "room-1001",
		EventType:    "RoomGiftSent",
		RoomVersion:  7,
		OccurredAtMs: 1_700_000_000_000,
		AppCode:      "lalu",
		Body:         body,
	}
}

type fakeRepository struct {
	records map[string]broadcastdomain.OutboxRecord
}

func newFakeRepository() *fakeRepository {
	return &fakeRepository{records: map[string]broadcastdomain.OutboxRecord{}}
}

func (r *fakeRepository) SaveBroadcastOutbox(_ context.Context, record broadcastdomain.OutboxRecord) (bool, broadcastdomain.OutboxRecord, error) {
	if existing, ok := r.records[record.EventID]; ok {
		return false, existing, nil
	}
	r.records[record.EventID] = record
	return true, record, nil
}

func (r *fakeRepository) ClaimPendingBroadcastOutbox(_ context.Context, workerID string, _ int64, lockUntilMS int64, limit int, _ int) ([]broadcastdomain.OutboxRecord, error) {
	records := make([]broadcastdomain.OutboxRecord, 0, limit)
	for eventID, record := range r.records {
		if len(records) >= limit {
			break
		}
		if record.Status != broadcastdomain.StatusPending && record.Status != broadcastdomain.StatusRetryable {
			continue
		}
		record.Status = broadcastdomain.StatusDelivering
		record.LockedBy = workerID
		record.LockedUntilMS = lockUntilMS
		r.records[eventID] = record
		records = append(records, record)
	}
	return records, nil
}

func (r *fakeRepository) MarkBroadcastOutboxDelivered(_ context.Context, eventID string, nowMS int64) error {
	record, ok := r.records[eventID]
	if !ok {
		return errors.New("record not found")
	}
	record.Status = broadcastdomain.StatusDelivered
	record.UpdatedAtMS = nowMS
	r.records[eventID] = record
	return nil
}

func (r *fakeRepository) MarkBroadcastOutboxFailed(_ context.Context, eventID string, _ string, nowMS int64, nextRetryAtMS int64, _ int, lastError string) error {
	record, ok := r.records[eventID]
	if !ok {
		return errors.New("record not found")
	}
	record.Status = broadcastdomain.StatusRetryable
	record.AttemptCount++
	record.NextRetryAtMS = nextRetryAtMS
	record.LastError = lastError
	record.UpdatedAtMS = nowMS
	r.records[eventID] = record
	return nil
}

type fakePublisher struct {
	messages       []tencentim.CustomGroupMessage
	deletedGroupID string
	deletedUserID  int64
}

func (p *fakePublisher) EnsureGroup(context.Context, tencentim.GroupSpec) error {
	return nil
}

func (p *fakePublisher) PublishGroupCustomMessage(_ context.Context, message tencentim.CustomGroupMessage) error {
	p.messages = append(p.messages, message)
	return nil
}

func (p *fakePublisher) DeleteGroupMember(_ context.Context, groupID string, userID int64) error {
	p.deletedGroupID = groupID
	p.deletedUserID = userID
	return nil
}
