package mysql

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	domain "hyapp/services/activity-service/internal/domain/luckygift"
	luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"

	"google.golang.org/grpc"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

func TestLuckyGiftRewardSettlementOutboxPressureLocal(t *testing.T) {
	if os.Getenv("RUN_LUCKY_GIFT_SETTLEMENT_PRESSURE") != "1" {
		t.Skip("set RUN_LUCKY_GIFT_SETTLEMENT_PRESSURE=1 to run local settlement outbox pressure test")
	}
	cases := []struct {
		name        string
		total       int
		batchSize   int
		concurrency int
	}{
		{name: "1k_batch100_c4", total: 1000, batchSize: 100, concurrency: 4},
		{name: "5k_batch100_c4", total: 5000, batchSize: 100, concurrency: 4},
		{name: "10k_batch200_c8", total: 10000, batchSize: 200, concurrency: 8},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			runLuckyGiftSettlementPressureCase(t, tc.total, tc.batchSize, tc.concurrency)
		})
	}
}

func runLuckyGiftSettlementPressureCase(t *testing.T, total int, batchSize int, concurrency int) {
	t.Helper()
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hy_lucky_press",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })

	seedStart := time.Now()
	seedLuckyGiftSettlementPressureRows(t, repository, total)
	seedElapsed := time.Since(seedStart)

	wallet := &pressureLuckyGiftWallet{}
	service := luckygiftservice.New(repository, luckygiftservice.WithWallet(wallet))
	ctx := appcode.WithContext(context.Background(), "lalu")
	options := luckygiftservice.WorkerOptions{
		WorkerID:    "pressure-worker",
		BatchSize:   batchSize,
		Concurrency: concurrency,
		LockTTL:     time.Minute,
		MaxRetry:    3,
	}

	start := time.Now()
	processedTotal := 0
	loops := 0
	for processedTotal < total {
		processed, err := service.ProcessPendingDrawOutbox(ctx, options)
		if err != nil {
			t.Fatalf("process settlement outbox: %v", err)
		}
		if processed == 0 {
			break
		}
		processedTotal += processed
		loops++
	}
	elapsed := time.Since(start)
	granted := countLuckyGiftPressureRows(t, repository, "lucky_draw_records", "reward_status = 'granted'")
	delivered := countLuckyGiftPressureRows(t, repository, "activity_outbox", "status = 'delivered'")
	if processedTotal != total || granted != int64(total) || delivered != int64(total) || wallet.calls.Load() != int64(total) {
		t.Fatalf("pressure result mismatch: processed=%d granted=%d delivered=%d wallet_calls=%d total=%d",
			processedTotal, granted, delivered, wallet.calls.Load(), total)
	}
	t.Logf("settlement_pressure total=%d batch=%d concurrency=%d loops=%d seed_ms=%d process_ms=%d throughput=%.2f events/s avg_event_ms=%.4f",
		total, batchSize, concurrency, loops, seedElapsed.Milliseconds(), elapsed.Milliseconds(),
		float64(total)/elapsed.Seconds(), float64(elapsed.Microseconds())/1000.0/float64(total))
}

func seedLuckyGiftSettlementPressureRows(t *testing.T, repository *Repository, total int) {
	t.Helper()
	const chunkSize = 500
	for start := 0; start < total; start += chunkSize {
		end := start + chunkSize
		if end > total {
			end = total
		}
		if err := seedLuckyGiftSettlementPressureChunk(repository, start, end); err != nil {
			t.Fatalf("seed pressure rows %d-%d: %v", start, end, err)
		}
	}
}

func seedLuckyGiftSettlementPressureChunk(repository *Repository, start int, end int) error {
	nowMS := time.Now().UTC().UnixMilli()
	drawPlaceholders := make([]string, 0, end-start)
	drawArgs := make([]any, 0, (end-start)*31)
	outboxPlaceholders := make([]string, 0, end-start)
	outboxArgs := make([]any, 0, (end-start)*12)
	for index := start; index < end; index++ {
		drawID := fmt.Sprintf("pressure_draw_%08d", index)
		commandID := fmt.Sprintf("pressure_cmd_%08d", index)
		userID := int64(100000 + index%1000)
		roomID := fmt.Sprintf("room_%03d", index%100)
		createdAtMS := nowMS + int64(index)
		drawPlaceholders = append(drawPlaceholders, "("+luckySQLPlaceholders(31)+")")
		drawArgs = append(drawArgs,
			"lalu", drawID, commandID, userID, "pressure-device", roomID, "pressure-anchor", "lucky", "rose",
			int64(100), int64(1), domain.PoolNovice, int64(1), int64(1),
			"hit", int64(10), int64(0), int64(0),
			int64(10), "{}", false, false,
			"{}", "{}", "{}", domain.StatusPending, "", "",
			createdAtMS, createdAtMS, createdAtMS,
		)
		payload, _ := json.Marshal(map[string]any{
			"event_type":             "lucky_gift_drawn",
			"event_id":               "lucky_gift_drawn:" + drawID,
			"app_code":               "lalu",
			"draw_id":                drawID,
			"command_id":             commandID,
			"pool_id":                "lucky",
			"user_id":                userID,
			"room_id":                roomID,
			"gift_id":                "rose",
			"gift_count":             1,
			"coin_spent":             100,
			"effective_reward_coins": 10,
			"created_at_ms":          createdAtMS,
		})
		outboxPlaceholders = append(outboxPlaceholders, "("+luckySQLPlaceholders(12)+")")
		outboxArgs = append(outboxArgs,
			"lalu", "lucky_reward_"+drawID, domain.EventTypeLuckyGiftRewardSettlement, payload,
			"pending", 0, createdAtMS, "", int64(0), "", createdAtMS, createdAtMS,
		)
	}
	ctx := context.Background()
	if _, err := repository.db.ExecContext(ctx, `
		INSERT INTO lucky_draw_records (
			app_code, draw_id, command_id, user_id, device_id, room_id, anchor_id, pool_id, gift_id,
			coin_spent, rule_version, experience_pool, rtp_window_index, gift_rtp_window_index,
			selected_tier_id, base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
			effective_reward_coins, budget_sources_json, stage_feedback, high_multiplier,
			candidate_tiers_json, pool_snapshot_json, rtp_snapshot_json, reward_status,
			reward_transaction_id, reward_failure_reason, paid_at_ms, created_at_ms, updated_at_ms
		) VALUES `+strings.Join(drawPlaceholders, ","), drawArgs...); err != nil {
		return err
	}
	if _, err := repository.db.ExecContext(ctx, `
		INSERT INTO activity_outbox (
			app_code, outbox_id, event_type, payload, status, retry_count, next_retry_at_ms,
			locked_by, lock_until_ms, last_error, created_at_ms, updated_at_ms
		) VALUES `+strings.Join(outboxPlaceholders, ","), outboxArgs...); err != nil {
		return err
	}
	return nil
}

func countLuckyGiftPressureRows(t *testing.T, repository *Repository, table string, where string) int64 {
	t.Helper()
	var count int64
	if err := repository.db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM "+table+" WHERE app_code = 'lalu' AND "+where).Scan(&count); err != nil {
		t.Fatalf("count %s: %v", table, err)
	}
	return count
}

type pressureLuckyGiftWallet struct {
	calls atomic.Int64
}

func (w *pressureLuckyGiftWallet) CreditLuckyGiftReward(_ context.Context, req *walletv1.CreditLuckyGiftRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error) {
	w.calls.Add(1)
	return &walletv1.CreditLuckyGiftRewardResponse{
		TransactionId: "pressure_tx_" + req.GetDrawId(),
		Balance:       &walletv1.AssetBalance{AvailableAmount: 1_000_000},
		Amount:        req.GetAmount(),
		GrantedAtMs:   time.Now().UTC().UnixMilli(),
	}, nil
}
