package commerce

import (
	"context"
	"errors"
	"fmt"
	"os"
	"sync"
	"testing"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestRechargeIdempotencyAndBalanceConcurrencyIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run finance concurrency checks")
	}
	db, err := gorm.Open(mysql.Open(integrationDSN(t)), &gorm.Config{})
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	defer sqlDB.Close()

	suffix := randomToken(10)
	email := fmt.Sprintf("finance-%s@example.com", suffix)
	user := UserAccount{Email: &email, Username: "finance_" + suffix, Balance: 0, Role: "user", Status: "active", IDCardVerified: "unverified"}
	if err := db.Create(&user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	defer cleanupFinanceIntegrationUser(db, user.ID)

	plugin, method := "epay_alipay", "alipay"
	order := RechargeOrder{UserID: user.ID, OutTradeNo: "IT-RC-" + suffix, PaymentPlugin: &plugin, PaymentMethod: &method, Channel: method, Amount: 12.34, AmountCents: 1234, Status: "pending"}
	if err := db.Create(&order).Error; err != nil {
		t.Fatalf("create recharge order: %v", err)
	}
	params := map[string]string{"amount_cents": "1234", "payment_plugin": plugin, "payment_method": method, "trade_no": "IT-TRADE-" + suffix}
	var wg sync.WaitGroup
	errs := make(chan error, 2)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			errs <- db.Transaction(func(tx *gorm.DB) error {
				return NewService(tx, nil, nil).processRechargePaid(context.Background(), order.OutTradeNo, params)
			})
		}()
	}
	wg.Wait()
	close(errs)
	for err := range errs {
		if err != nil {
			t.Fatalf("duplicate callback: %v", err)
		}
	}
	if err := db.Where("id = ?", user.ID).First(&user).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	if user.Balance != 1234 {
		t.Fatalf("balance = %d, want 1234", user.Balance)
	}
	var rechargeLogs int64
	if err := db.Model(&BalanceLog{}).Where("user_id = ? AND ref_no = ? AND change_type = ?", user.ID, order.OutTradeNo, "recharge").Count(&rechargeLogs).Error; err != nil {
		t.Fatal(err)
	}
	if rechargeLogs != 1 {
		t.Fatalf("recharge logs = %d, want 1", rechargeLogs)
	}

	for index, test := range []struct {
		status string
		params map[string]string
	}{
		{status: "pending", params: map[string]string{"amount_cents": "1", "payment_plugin": plugin, "payment_method": method}},
		{status: "pending", params: map[string]string{"amount_cents": "1234", "payment_plugin": "epay_wxpay", "payment_method": method}},
		{status: "pending", params: map[string]string{"amount_cents": "1234", "payment_plugin": plugin, "payment_method": "wxpay"}},
		{status: "expired", params: params},
	} {
		invalid := RechargeOrder{UserID: user.ID, OutTradeNo: fmt.Sprintf("IT-RC-%s-%d", suffix, index), PaymentPlugin: &plugin, PaymentMethod: &method, Channel: method, Amount: 12.34, AmountCents: 1234, Status: test.status}
		if err := db.Create(&invalid).Error; err != nil {
			t.Fatal(err)
		}
		if err := db.Transaction(func(tx *gorm.DB) error {
			return NewService(tx, nil, nil).processRechargePaid(context.Background(), invalid.OutTradeNo, test.params)
		}); err == nil {
			t.Fatalf("invalid callback %d unexpectedly succeeded", index)
		}
	}
	if err := db.Where("id = ?", user.ID).First(&user).Error; err != nil {
		t.Fatal(err)
	}
	if user.Balance != 1234 {
		t.Fatalf("invalid callbacks changed balance to %d", user.Balance)
	}

	const adjustments = 8
	adjustErrs := make(chan error, adjustments)
	for i := 0; i < adjustments; i++ {
		wg.Add(1)
		go func(index int) {
			defer wg.Done()
			_, err := NewService(db, nil, nil).AdminAdjustBalance(context.Background(), user.ID, user.ID, 100, fmt.Sprintf("integration adjustment %d", index))
			adjustErrs <- err
		}(i)
	}
	wg.Wait()
	close(adjustErrs)
	for err := range adjustErrs {
		if err != nil {
			t.Fatalf("admin adjustment: %v", err)
		}
	}
	if err := db.Where("id = ?", user.ID).First(&user).Error; err != nil {
		t.Fatal(err)
	}
	if want := 1234 + adjustments*100; user.Balance != want {
		t.Fatalf("balance after adjustments = %d, want %d", user.Balance, want)
	}
	var adjustmentLogs int64
	if err := db.Model(&BalanceLog{}).Where("user_id = ? AND change_type = ?", user.ID, "admin_adjust").Count(&adjustmentLogs).Error; err != nil {
		t.Fatal(err)
	}
	if adjustmentLogs != adjustments {
		t.Fatalf("admin adjustment logs = %d, want %d", adjustmentLogs, adjustments)
	}
}

func TestCouponCheckoutPointClaimAndAutoRenewConcurrencyIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run checkout concurrency checks")
	}
	db, err := gorm.Open(mysql.Open(integrationDSN(t)), &gorm.Config{})
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatal(err)
	}
	defer sqlDB.Close()
	var category ProductCategory
	if err := db.Where("status = ?", "active").Order("id ASC").First(&category).Error; err != nil {
		t.Fatalf("load product category: %v", err)
	}
	suffix := randomToken(10)
	users := []UserAccount{
		{Username: "checkout_a_" + suffix, Balance: 10000, Role: "user", Status: "active", IDCardVerified: "unverified"},
		{Username: "checkout_b_" + suffix, Balance: 10000, Role: "user", Status: "active", IDCardVerified: "unverified"},
	}
	if err := db.Create(&users).Error; err != nil {
		t.Fatalf("create users: %v", err)
	}
	group := ProductGroup{CategoryID: category.ID, Slug: "it-concurrency-" + suffix, Name: "integration concurrency"}
	if err := db.Create(&group).Error; err != nil {
		t.Fatal(err)
	}
	product := Product{GroupID: group.ID, SKU: "IT-CONC-" + suffix, Name: "integration concurrency product", Type: "server", Status: "active", StockPolicy: "tracked", Stock: 2, MinQuantity: 1, MaxQuantity: 1, APIType: "none"}
	if err := db.Create(&product).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&ProductPricing{ProductID: product.ID, Cycle: "monthly", Price: 10, IsEnabled: true}).Error; err != nil {
		t.Fatal(err)
	}
	now := time.Now().UTC()
	starts, expires := now.Add(-time.Hour), now.Add(time.Hour)
	coupon := PromoCode{Code: "ITCP" + suffix, Type: "fixed", Value: 500, Scope: "all", DiscountLevel: "order", StartsAt: &starts, ExpiresAt: &expires, MaxUses: 1, MaxUsesPerUser: 1, AppliesToRenewals: true, IsEnabled: true}
	if err := db.Create(&coupon).Error; err != nil {
		t.Fatal(err)
	}
	defer cleanupCheckoutConcurrency(db, users, group.ID, product.ID, coupon.ID)

	service := NewService(db, nil, nil)
	items := make([]CartItemResponse, 0, len(users))
	for _, user := range users {
		item, err := service.AddCartItem(context.Background(), user.ID, CartItemCreateRequest{ProductID: product.ID, Cycle: "monthly", Quantity: 1})
		if err != nil {
			t.Fatalf("add cart item: %v", err)
		}
		items = append(items, item)
	}
	var wg sync.WaitGroup
	checkoutErrs := make(chan error, len(users))
	checkoutResults := make(chan CheckoutResponse, len(users))
	for index, user := range users {
		wg.Add(1)
		go func(index int, user UserAccount) {
			defer wg.Done()
			code := coupon.Code
			result, err := service.Checkout(context.Background(), user.ID, CheckoutRequest{CartItemIDs: []int64{items[index].ID}, CouponCode: &code})
			checkoutErrs <- err
			checkoutResults <- result
		}(index, user)
	}
	wg.Wait()
	close(checkoutErrs)
	close(checkoutResults)
	successes := 0
	for err := range checkoutErrs {
		if err == nil {
			successes++
		}
	}
	if successes != 1 {
		t.Fatalf("checkout successes = %d, want 1", successes)
	}
	var successful CheckoutResponse
	for result := range checkoutResults {
		if result.OrderID > 0 {
			successful = result
		}
	}
	if err := db.Where("id = ?", coupon.ID).First(&coupon).Error; err != nil {
		t.Fatal(err)
	}
	if coupon.UsedCount != 1 {
		t.Fatalf("coupon used_count = %d, want 1", coupon.UsedCount)
	}
	var usageCount int64
	if err := db.Model(&PromoCodeUsage{}).Where("promo_code_id = ?", coupon.ID).Count(&usageCount).Error; err != nil || usageCount != 1 {
		t.Fatalf("coupon usages = %d error=%v", usageCount, err)
	}

	var instance Instance
	if err := db.Where("order_id = ?", successful.OrderID).First(&instance).Error; err != nil {
		t.Fatalf("load successful instance: %v", err)
	}
	if err := db.Where("order_id = ?", instance.OrderID).Delete(&PointReward{}).Error; err != nil {
		t.Fatal(err)
	}
	rewards := []PointReward{
		{UserID: instance.UserID, OrderID: instance.OrderID, InstanceID: &instance.ID, SourceType: "self_purchase", Status: "claimable", Points: 11},
		{UserID: instance.UserID, OrderID: instance.OrderID, InstanceID: &instance.ID, SourceType: "self_purchase", Status: "claimable", Points: 13},
	}
	if err := db.Create(&rewards).Error; err != nil {
		t.Fatal(err)
	}
	claimResults := make(chan map[string]any, 2)
	claimErrs := make(chan error, 2)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			result, err := service.ClaimInstancePoints(context.Background(), instance.UserID, instance.ID)
			claimResults <- result
			claimErrs <- err
		}()
	}
	wg.Wait()
	close(claimResults)
	close(claimErrs)
	for err := range claimErrs {
		if err != nil {
			t.Fatalf("claim points: %v", err)
		}
	}
	claimedPoints, claimedCount := 0, 0
	for result := range claimResults {
		claimedPoints += intValueAny(result["claimed_points"], 0)
		claimedCount += intValueAny(result["claimed_count"], 0)
	}
	if claimedPoints != 24 || claimedCount != 2 {
		t.Fatalf("claimed points/count = %d/%d, want 24/2", claimedPoints, claimedCount)
	}

	if err := db.Model(&Instance{}).Where("id = ?", instance.ID).Updates(map[string]any{"status": "active", "expires_at": expires, "next_renew_amount": 100, "auto_renew": 1}).Error; err != nil {
		t.Fatal(err)
	}
	instance.Status = "active"
	instance.ExpiresAt = &expires
	instance.NextRenewAmount = 100
	if err := db.Model(&UserAccount{}).Where("id = ?", instance.UserID).Update("balance", 5000).Error; err != nil {
		t.Fatal(err)
	}
	renewErrs := make(chan error, 2)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_, err := service.renewInstance(context.Background(), instance.UserID, instance.ID, 1, "auto_renew", &expires)
			renewErrs <- err
		}()
	}
	wg.Wait()
	close(renewErrs)
	alreadyProcessed, upstreamFailed := 0, 0
	for err := range renewErrs {
		switch {
		case errors.Is(err, ErrAutoRenewalAlreadyProcessed):
			alreadyProcessed++
		case err != nil:
			upstreamFailed++
		}
	}
	if alreadyProcessed != 1 || upstreamFailed != 1 {
		t.Fatalf("auto renewal outcomes already=%d upstream_failed=%d", alreadyProcessed, upstreamFailed)
	}
	var eventCount int64
	if err := db.Model(&AutoRenewalEvent{}).Where("instance_id = ?", instance.ID).Count(&eventCount).Error; err != nil || eventCount != 1 {
		t.Fatalf("auto renewal events = %d error=%v", eventCount, err)
	}
	var renewalOrders int64
	if err := db.Model(&Order{}).Where("user_id = ? AND items_snapshot LIKE ?", instance.UserID, "%\"type\":\"renewal\"%").Count(&renewalOrders).Error; err != nil || renewalOrders != 1 {
		t.Fatalf("renewal orders = %d error=%v", renewalOrders, err)
	}
}

type integrationTaskExecutor struct {
	definition TaskDefinition
	started    chan struct{}
	release    chan struct{}
	err        error
	panicValue any
	waitForCtx bool
	once       sync.Once
}

func (e *integrationTaskExecutor) Definitions() []TaskDefinition {
	return []TaskDefinition{e.definition}
}

func (e *integrationTaskExecutor) Execute(ctx context.Context, _ string) (map[string]any, error) {
	e.once.Do(func() { close(e.started) })
	if e.panicValue != nil {
		panic(e.panicValue)
	}
	if e.waitForCtx {
		<-ctx.Done()
		return nil, ctx.Err()
	}
	if e.release != nil {
		<-e.release
	}
	return map[string]any{"executed": e.err == nil}, e.err
}

func TestTaskExecutionStateAndLockIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run task execution integration checks")
	}
	db, err := gorm.Open(mysql.Open(integrationDSN(t)), &gorm.Config{})
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatal(err)
	}
	defer sqlDB.Close()
	suffix := randomToken(10)
	keys := []string{"it_task_lock_" + suffix, "it_task_fail_" + suffix, "it_task_cancel_" + suffix, "it_task_panic_" + suffix}
	defer db.Where("task_key IN ?", keys).Delete(&TaskRunLog{})
	var operator UserAccount
	if err := db.Order("id ASC").First(&operator).Error; err != nil {
		t.Fatalf("load task operator: %v", err)
	}

	lockExecutor := &integrationTaskExecutor{definition: TaskDefinition{Key: keys[0], Name: "integration lock", ManualEnabled: true}, started: make(chan struct{}), release: make(chan struct{})}
	service := NewService(db, nil, nil).WithTaskExecutor(lockExecutor)
	firstDone := make(chan error, 1)
	go func() {
		_, err := service.AdminRunTask(context.Background(), keys[0], operator.ID)
		firstDone <- err
	}()
	select {
	case <-lockExecutor.started:
	case <-time.After(5 * time.Second):
		t.Fatal("first task did not start")
	}
	second, err := service.AdminRunTask(context.Background(), keys[0], operator.ID)
	if err != nil || second["status"] != "skipped" || second["accepted"] != false {
		t.Fatalf("concurrent task result = %#v error=%v", second, err)
	}
	close(lockExecutor.release)
	if err := <-firstDone; err != nil {
		t.Fatalf("first task: %v", err)
	}
	assertTaskRunStatus(t, db, keys[0], "success")
	if _, err := service.AdminRunTask(context.Background(), "missing_"+suffix, operator.ID); err == nil {
		t.Fatal("unknown task unexpectedly succeeded")
	}

	failExecutor := &integrationTaskExecutor{definition: TaskDefinition{Key: keys[1], Name: "integration fail", ManualEnabled: true}, started: make(chan struct{}), err: errors.New("expected failure")}
	if _, err := NewService(db, nil, nil).WithTaskExecutor(failExecutor).AdminRunTask(context.Background(), keys[1], operator.ID); err == nil {
		t.Fatal("failed handler unexpectedly succeeded")
	}
	assertTaskRunStatus(t, db, keys[1], "failed")

	cancelExecutor := &integrationTaskExecutor{definition: TaskDefinition{Key: keys[2], Name: "integration cancel", ManualEnabled: true}, started: make(chan struct{}), waitForCtx: true}
	cancelCtx, cancel := context.WithCancel(context.Background())
	cancelDone := make(chan error, 1)
	go func() {
		_, err := NewService(db, nil, nil).WithTaskExecutor(cancelExecutor).AdminRunTask(cancelCtx, keys[2], operator.ID)
		cancelDone <- err
	}()
	<-cancelExecutor.started
	cancel()
	if err := <-cancelDone; err == nil {
		t.Fatal("cancelled handler unexpectedly succeeded")
	}
	assertTaskRunStatus(t, db, keys[2], "failed")

	panicExecutor := &integrationTaskExecutor{definition: TaskDefinition{Key: keys[3], Name: "integration panic", ManualEnabled: true}, started: make(chan struct{}), panicValue: "expected panic"}
	if _, err := NewService(db, nil, nil).WithTaskExecutor(panicExecutor).AdminRunTask(context.Background(), keys[3], operator.ID); err == nil {
		t.Fatal("panicking handler unexpectedly succeeded")
	}
	assertTaskRunStatus(t, db, keys[3], "failed")
}

func assertTaskRunStatus(t *testing.T, db *gorm.DB, taskKey string, want string) {
	t.Helper()
	var run TaskRunLog
	if err := db.Where("task_key = ?", taskKey).Order("id DESC").First(&run).Error; err != nil {
		t.Fatalf("load task run %q: %v", taskKey, err)
	}
	if run.Status != want || run.FinishedAt == nil || run.DurationMS == nil {
		t.Fatalf("task run = %#v, want status %q with final fields", run, want)
	}
}

func cleanupFinanceIntegrationUser(db *gorm.DB, userID int64) {
	var orders []RechargeOrder
	_ = db.Where("user_id = ?", userID).Find(&orders).Error
	refs := make([]string, 0, len(orders))
	for _, order := range orders {
		refs = append(refs, order.OutTradeNo)
	}
	if len(refs) > 0 {
		_ = db.Where("ref_no IN ?", refs).Delete(&BalanceLog{}).Error
	}
	_ = db.Where("user_id = ? AND change_type = ?", userID, "admin_adjust").Delete(&BalanceLog{}).Error
	_ = db.Where("user_id = ?", userID).Delete(&RechargeOrder{}).Error
	_ = db.Where("id = ?", userID).Delete(&UserAccount{}).Error
}

func cleanupCheckoutConcurrency(db *gorm.DB, users []UserAccount, groupID int64, productID int64, couponID int64) {
	userIDs := make([]int64, 0, len(users))
	for _, user := range users {
		userIDs = append(userIDs, user.ID)
	}
	var orders []Order
	_ = db.Where("user_id IN ?", userIDs).Find(&orders).Error
	orderIDs := make([]int64, 0, len(orders))
	for _, order := range orders {
		orderIDs = append(orderIDs, order.ID)
	}
	var instances []Instance
	if len(orderIDs) > 0 {
		_ = db.Where("order_id IN ?", orderIDs).Find(&instances).Error
	}
	instanceIDs := make([]int64, 0, len(instances))
	for _, instance := range instances {
		instanceIDs = append(instanceIDs, instance.ID)
	}
	if len(instanceIDs) > 0 {
		_ = db.Where("instance_id IN ?", instanceIDs).Delete(&UpstreamActionLog{}).Error
		_ = db.Where("instance_id IN ?", instanceIDs).Delete(&AutoRenewalEvent{}).Error
	}
	if len(orderIDs) > 0 {
		_ = db.Where("order_id IN ?", orderIDs).Delete(&PointReward{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&PointLog{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&AgentSettlement{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&UserSpendEvent{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&PromoCodeUsage{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&BalanceLog{}).Error
		_ = db.Where("order_id IN ?", orderIDs).Delete(&Instance{}).Error
		_ = db.Where("id IN ?", orderIDs).Delete(&Order{}).Error
	}
	_ = db.Where("user_id IN ?", userIDs).Delete(&CartItem{}).Error
	_ = db.Where("user_id IN ?", userIDs).Delete(&UserMembership{}).Error
	_ = db.Where("user_id IN ?", userIDs).Delete(&BalanceLog{}).Error
	_ = db.Where("id IN ?", userIDs).Delete(&UserAccount{}).Error
	_ = db.Where("promo_code_id = ?", couponID).Delete(&PromoCodeUsage{}).Error
	_ = db.Where("id = ?", couponID).Delete(&PromoCode{}).Error
	_ = db.Where("product_id = ?", productID).Delete(&ProductPricing{}).Error
	_ = db.Where("id = ?", productID).Delete(&Product{}).Error
	_ = db.Where("id = ?", groupID).Delete(&ProductGroup{}).Error
}
