package walletnotice

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

	"hyapp/pkg/tencentim"
	"hyapp/pkg/walletmq"
)

func TestProcessWalletBalanceNoticesPublishesPrivateIM(t *testing.T) {
	repo := &fakeRepository{
		events: []WalletBalanceEvent{{
			AppCode:        "lalu",
			EventID:        "evt-1",
			EventType:      "WalletBalanceChanged",
			TransactionID:  "tx-1",
			CommandID:      "cmd-1",
			UserID:         123,
			AssetType:      "COIN",
			AvailableDelta: -10,
			FrozenDelta:    0,
			PayloadJSON:    `{"available_after":90,"frozen_after":0,"balance_version":7}`,
			CreatedAtMS:    1710000000000,
		}},
	}
	publisher := &fakePublisher{}
	service := New(Config{NodeID: "node-a"}, repo, publisher)

	processed, err := service.ProcessWalletBalanceNotices(context.Background(), WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletBalanceNotices failed: %v", err)
	}
	if processed != 1 || len(repo.delivered) != 1 || len(publisher.messages) != 1 {
		t.Fatalf("unexpected process result: processed=%d delivered=%d messages=%d", processed, len(repo.delivered), len(publisher.messages))
	}
	message := publisher.messages[0]
	if message.ToAccount != "123" || message.EventID != "evt-1" || message.Ext != "wallet_notice" {
		t.Fatalf("unexpected c2c message: %+v", message)
	}
	var payload map[string]any
	if err := json.Unmarshal(message.PayloadJSON, &payload); err != nil {
		t.Fatalf("decode payload: %v", err)
	}
	if payload["event_id"] != "evt-1" || payload["asset_type"] != "COIN" || payload["balance_version"].(float64) != 7 {
		t.Fatalf("unexpected wallet payload: %+v", payload)
	}
}

func TestProcessWalletBalanceNoticesPublishesVipActivatedIM(t *testing.T) {
	repo := &fakeRepository{
		events: []WalletBalanceEvent{{
			AppCode:       "lalu",
			EventID:       "vip-evt-1",
			EventType:     "VipActivated",
			TransactionID: "tx-vip-1",
			CommandID:     "cmd-vip-1",
			UserID:        456,
			AssetType:     "VIP",
			PayloadJSON:   `{"level":3,"vip_name":"VIP3","source":"admin_grant","expires_at_ms":1710003600000}`,
			CreatedAtMS:   1710000000000,
		}},
	}
	publisher := &fakePublisher{}
	service := New(Config{NodeID: "node-a"}, repo, publisher)

	processed, err := service.ProcessWalletBalanceNotices(context.Background(), WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletBalanceNotices failed: %v", err)
	}
	if processed != 1 || len(repo.delivered) != 1 || len(publisher.messages) != 1 {
		t.Fatalf("unexpected process result: processed=%d delivered=%d messages=%d", processed, len(repo.delivered), len(publisher.messages))
	}
	message := publisher.messages[0]
	if message.ToAccount != "456" || message.EventID != "vip-evt-1" || message.Desc != "VipActivated" || message.Ext != "vip_notice" {
		t.Fatalf("unexpected vip c2c message: %+v", message)
	}
	var payload map[string]any
	if err := json.Unmarshal(message.PayloadJSON, &payload); err != nil {
		t.Fatalf("decode payload: %v", err)
	}
	if payload["event_type"] != "VipActivated" || payload["asset_type"] != "VIP" || payload["level"].(float64) != 3 {
		t.Fatalf("unexpected vip payload: %+v", payload)
	}
}

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

	handled, err := service.ProcessWalletOutboxMessage(context.Background(), walletmq.WalletOutboxMessage{
		AppCode:        "lalu",
		EventID:        "evt-mq-1",
		EventType:      "WalletBalanceChanged",
		TransactionID:  "tx-mq-1",
		CommandID:      "cmd-mq-1",
		UserID:         789,
		AssetType:      "COIN",
		AvailableDelta: 30,
		PayloadJSON:    `{"available_after":130}`,
		OccurredAtMS:   1710000000000,
	}, WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletOutboxMessage failed: %v", err)
	}
	if !handled || len(repo.claimedMessages) != 1 || len(repo.delivered) != 1 || len(publisher.messages) != 1 {
		t.Fatalf("unexpected mq result: handled=%v claimed=%d delivered=%d messages=%d", handled, len(repo.claimedMessages), len(repo.delivered), len(publisher.messages))
	}
	if publisher.messages[0].ToAccount != "789" || publisher.messages[0].EventID != "evt-mq-1" {
		t.Fatalf("unexpected mq c2c message: %+v", publisher.messages[0])
	}
}

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

	handled, err := service.ProcessWalletOutboxMessage(context.Background(), walletmq.WalletOutboxMessage{
		AppCode:        "lalu",
		EventID:        "robot-wallet-1",
		EventType:      "WalletBalanceChanged",
		TransactionID:  "robot-tx-1",
		CommandID:      "robot-cmd-1",
		UserID:         100001,
		AssetType:      robotCoinAssetType,
		AvailableDelta: -1,
		PayloadJSON:    `{"available_after":999}`,
		OccurredAtMS:   1710000000000,
	}, WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletOutboxMessage failed: %v", err)
	}
	if !handled || len(repo.claimedMessages) != 0 || len(repo.delivered) != 0 || len(publisher.messages) != 0 {
		t.Fatalf("robot coin notice must be acked without c2c delivery: handled=%v claimed=%d delivered=%d messages=%d", handled, len(repo.claimedMessages), len(repo.delivered), len(publisher.messages))
	}
}

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

	handled, err := service.ProcessWalletOutboxMessage(context.Background(), walletmq.WalletOutboxMessage{
		AppCode:        "huwaa",
		EventID:        "suppressed-wallet-1",
		EventType:      "WalletBalanceChanged",
		TransactionID:  "suppressed-tx-1",
		CommandID:      "suppressed-cmd-1",
		UserID:         100002,
		AssetType:      "COIN",
		AvailableDelta: -1,
		PayloadJSON:    `{"available_after":999,"suppress_private_notice":true}`,
		OccurredAtMS:   1710000000000,
	}, WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletOutboxMessage failed: %v", err)
	}
	if !handled || len(repo.claimedMessages) != 0 || len(repo.delivered) != 0 || len(publisher.messages) != 0 {
		t.Fatalf("owner-suppressed notice must be acked before delivery claim: handled=%v claimed=%d delivered=%d messages=%d", handled, len(repo.claimedMessages), len(repo.delivered), len(publisher.messages))
	}
}

func TestProcessWalletBalanceNoticesTerminatesOwnerSuppressedFallback(t *testing.T) {
	repo := &fakeRepository{events: []WalletBalanceEvent{{
		AppCode:        "huwaa",
		EventID:        "suppressed-wallet-fallback-1",
		EventType:      "WalletBalanceChanged",
		TransactionID:  "suppressed-tx-fallback-1",
		CommandID:      "suppressed-cmd-fallback-1",
		UserID:         100003,
		AssetType:      "COIN",
		AvailableDelta: -1,
		PayloadJSON:    `{"available_after":999,"suppress_private_notice":true}`,
		CreatedAtMS:    1710000000000,
	}}}
	publisher := &fakePublisher{}
	service := New(Config{NodeID: "node-a"}, repo, publisher)

	processed, err := service.ProcessWalletBalanceNotices(context.Background(), WalletNoticeWorkerOptions{MaxRetryCount: 3})
	if err != nil {
		t.Fatalf("ProcessWalletBalanceNotices failed: %v", err)
	}
	if processed != 1 || len(repo.delivered) != 1 || len(publisher.messages) != 0 {
		t.Fatalf("fallback suppression must terminate locally without C2C: processed=%d delivered=%d messages=%d", processed, len(repo.delivered), len(publisher.messages))
	}
}

func TestProcessWalletBalanceNoticesMarksRetryable(t *testing.T) {
	repo := &fakeRepository{
		events: []WalletBalanceEvent{{
			AppCode:     "lalu",
			EventID:     "evt-2",
			EventType:   "WalletBalanceChanged",
			UserID:      123,
			AssetType:   "COIN",
			PayloadJSON: `{"available_after":90,"balance_version":8}`,
			RetryCount:  0,
		}},
	}
	service := New(Config{NodeID: "node-a"}, repo, &fakePublisher{err: errors.New("im timeout")})

	processed, err := service.ProcessWalletBalanceNotices(context.Background(), WalletNoticeWorkerOptions{
		MaxRetryCount:  3,
		InitialBackoff: time.Second,
		MaxBackoff:     time.Minute,
	})
	if err != nil {
		t.Fatalf("ProcessWalletBalanceNotices failed: %v", err)
	}
	if processed != 1 || len(repo.retryable) != 1 {
		t.Fatalf("unexpected retry result: processed=%d retryable=%d", processed, len(repo.retryable))
	}
	if repo.retryable[0].retryCount != 1 || repo.retryable[0].nextRetryAtMS <= 0 {
		t.Fatalf("unexpected retry marker: %+v", repo.retryable[0])
	}
}

type fakeRepository struct {
	events          []WalletBalanceEvent
	claimedMessages []walletmq.WalletOutboxMessage
	delivered       []WalletBalanceEvent
	retryable       []retryMarker
	failed          []WalletBalanceEvent
}

type retryMarker struct {
	event         WalletBalanceEvent
	retryCount    int
	nextRetryAtMS int64
	lastErr       string
}

func (r *fakeRepository) ClaimWalletBalanceEvents(context.Context, string, int, time.Duration) ([]WalletBalanceEvent, error) {
	return r.events, nil
}

func (r *fakeRepository) ClaimWalletBalanceMessage(_ context.Context, _ string, message walletmq.WalletOutboxMessage, _ time.Duration) (WalletBalanceEvent, bool, error) {
	r.claimedMessages = append(r.claimedMessages, message)
	return WalletBalanceEvent{
		AppCode:        message.AppCode,
		EventID:        message.EventID,
		EventType:      message.EventType,
		TransactionID:  message.TransactionID,
		CommandID:      message.CommandID,
		UserID:         message.UserID,
		AssetType:      message.AssetType,
		AvailableDelta: message.AvailableDelta,
		FrozenDelta:    message.FrozenDelta,
		PayloadJSON:    message.PayloadJSON,
		CreatedAtMS:    message.OccurredAtMS,
	}, true, nil
}

func (r *fakeRepository) MarkWalletBalanceDelivered(_ context.Context, event WalletBalanceEvent, _ json.RawMessage, _ int64) error {
	r.delivered = append(r.delivered, event)
	return nil
}

func (r *fakeRepository) MarkWalletBalanceRetryable(_ context.Context, event WalletBalanceEvent, retryCount int, nextRetryAtMS int64, lastErr string, _ int64) error {
	r.retryable = append(r.retryable, retryMarker{event: event, retryCount: retryCount, nextRetryAtMS: nextRetryAtMS, lastErr: lastErr})
	return nil
}

func (r *fakeRepository) MarkWalletBalanceFailed(_ context.Context, event WalletBalanceEvent, _ int, _ string, _ int64) error {
	r.failed = append(r.failed, event)
	return nil
}

type fakePublisher struct {
	messages []tencentim.CustomUserMessage
	err      error
}

func (p *fakePublisher) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error {
	if p.err != nil {
		return p.err
	}
	p.messages = append(p.messages, message)
	return nil
}
