package commerce

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

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

func TestCartPricingAndRealNameIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed cart chain checks")
	}
	dsn := integrationDSN(t)
	db, err := gorm.Open(mysql.Open(dsn), &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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	group := ProductGroup{CategoryID: 1, Slug: "it-cart-chain", Name: "integration cart chain"}
	if err := tx.Create(&group).Error; err != nil {
		t.Fatalf("create group: %v", err)
	}
	product := Product{
		GroupID:         group.ID,
		SKU:             "it-cart-chain-sku",
		Name:            "integration pricing product",
		Type:            "server",
		Status:          "active",
		StockPolicy:     "finite",
		Stock:           100,
		MinQuantity:     1,
		MaxQuantity:     10,
		ProvisionConfig: JSONRaw(`[{"def":"system_disk_size|系统盘","type":"slider","slider_min":10,"slider_max":20,"slider_step":1,"slider_unit_price":2}]`),
	}
	if err := tx.Create(&product).Error; err != nil {
		t.Fatalf("create product: %v", err)
	}
	if err := tx.Create(&ProductPricing{ProductID: product.ID, Cycle: "monthly", Price: 12.34, SetupFee: 1.23, IsEnabled: true}).Error; err != nil {
		t.Fatalf("create pricing: %v", err)
	}

	item, err := svc.AddCartItem(ctx, user.ID, CartItemCreateRequest{
		ProductID:    product.ID,
		Cycle:        "monthly",
		Quantity:     2,
		UnitPrice:    0,
		AddonPrice:   0,
		SetupFee:     0,
		SliderValues: map[string]any{"system_disk_size": 15},
	})
	if err != nil {
		t.Fatalf("add cart item: %v", err)
	}
	if item.UnitPrice != 12.34 || item.AddonPrice != 10 || item.SetupFee != 1.23 {
		t.Fatalf("server pricing not applied: unit=%v addon=%v setup=%v", item.UnitPrice, item.AddonPrice, item.SetupFee)
	}
	if err := tx.Model(&CartItem{}).Where("id = ?", item.ID).Updates(map[string]any{"unit_price": 0, "addon_price": 0, "setup_fee": 0}).Error; err != nil {
		t.Fatalf("tamper cart item: %v", err)
	}
	checkout, err := svc.buildCheckoutContext(ctx, user.ID, []int64{item.ID})
	if err != nil {
		t.Fatalf("checkout context: %v", err)
	}
	if got, want := checkout.itemTotals[item.ID], 4591; got != want {
		t.Fatalf("checkout total = %d, want %d", got, want)
	}
	if got := checkout.cartItems[0].UnitPrice; got != 12.34 {
		t.Fatalf("checkout did not refresh unit price: %v", got)
	}

	if err := tx.Model(&Product{}).Where("id = ?", product.ID).Update("requires_real_name", true).Error; err != nil {
		t.Fatalf("enable real-name flag: %v", err)
	}
	if err := tx.Model(&UserAccount{}).Where("id = ?", user.ID).Update("id_card_verified", "unverified").Error; err != nil {
		t.Fatalf("mark user unverified: %v", err)
	}
	_, err = svc.buildCheckoutContext(ctx, user.ID, []int64{item.ID})
	if err == nil {
		t.Fatalf("expected real-name checkout rejection")
	}
	httpErr, ok := err.(HTTPError)
	if !ok || httpErr.Status != 403 {
		t.Fatalf("real-name error = %#v", err)
	}
	detail, ok := httpErr.Detail.(map[string]any)
	if !ok || detail["code"] != "REAL_NAME_REQUIRED" {
		t.Fatalf("real-name detail = %#v", httpErr.Detail)
	}
}

func TestCurrentXianGroupReadinessAndStoreVisibilityIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed store 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()

	svc := &Service{db: db}
	var group ProductGroup
	if err := db.Where("name = ?", "西安云服务器").First(&group).Error; err != nil {
		t.Fatalf("load xian group: %v", err)
	}
	readiness, err := svc.AdminGroupReadiness(context.Background(), group.ID)
	if err != nil {
		t.Fatalf("readiness: %v", err)
	}
	summary, ok := readiness["summary"].(map[string]any)
	if !ok {
		t.Fatalf("readiness summary missing: %#v", readiness)
	}
	if got := summary["error_count"]; got != 0 {
		t.Fatalf("xian readiness error_count = %#v, want 0; readiness=%#v", got, readiness)
	}

	var category ProductCategory
	if err := db.Where("id = ?", group.CategoryID).First(&category).Error; err != nil {
		t.Fatalf("load category: %v", err)
	}
	products, err := svc.ListProducts(context.Background(), category.Slug, group.Slug)
	if err != nil {
		t.Fatalf("list store products: %v", err)
	}
	if len(products) != 8 {
		t.Fatalf("store product count = %d, want 8", len(products))
	}
	for _, product := range products {
		if product.APIType != "dcim_cloud_image" {
			t.Fatalf("visible product %s has unexpected api_type %s", product.SKU, product.APIType)
		}
	}
}

func TestCurrentShiyanGroupReadinessAndStoreVisibilityIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed store 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()

	svc := &Service{db: db}
	var group ProductGroup
	if err := db.Where("name = ?", "十堰云服务器").First(&group).Error; err != nil {
		t.Fatalf("load shiyan group: %v", err)
	}
	readiness, err := svc.AdminGroupReadiness(context.Background(), group.ID)
	if err != nil {
		t.Fatalf("readiness: %v", err)
	}
	summary := readiness["summary"].(map[string]any)
	if got := summary["error_count"]; got != 0 {
		t.Fatalf("shiyan readiness error_count = %#v, want 0; readiness=%#v", got, readiness)
	}

	var category ProductCategory
	if err := db.Where("id = ?", group.CategoryID).First(&category).Error; err != nil {
		t.Fatalf("load category: %v", err)
	}
	products, err := svc.ListProducts(context.Background(), category.Slug, group.Slug)
	if err != nil {
		t.Fatalf("list store products: %v", err)
	}
	if len(products) != 2 {
		t.Fatalf("store product count = %d, want 2", len(products))
	}
	for _, product := range products {
		if product.APIType != "zjmf_finance" {
			t.Fatalf("visible product %s has unexpected api_type %s", product.SKU, product.APIType)
		}
	}
}

func TestAdminProductSaveValidationIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed admin validation 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()

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}

	group := ProductGroup{
		CategoryID: 1,
		Slug:       "it-admin-validation",
		Name:       "integration admin validation",
		OSOptions:  JSONRaw(`[{"label":"Debian","value":14}]`),
	}
	if err := tx.Create(&group).Error; err != nil {
		t.Fatalf("create group: %v", err)
	}
	var dcim CloudServer
	if err := tx.Where("server_type IN ?", []string{"dcimcloud", "dcim_cloud", "dcim"}).First(&dcim).Error; err != nil {
		t.Fatalf("load dcim server: %v", err)
	}

	validProduct := Product{
		GroupID:       group.ID,
		SKU:           "it-admin-validation-sku",
		Name:          "valid admin product",
		Type:          "server",
		Status:        "active",
		StockPolicy:   "finite",
		AutoProvision: true,
		APIType:       "dcim_cloud_image",
		CloudServerID: &dcim.ID,
	}
	if err := svc.validateProductForSave(context.Background(), validProduct, []ProductPricing{{Cycle: "monthly", Price: 1, IsEnabled: true}}); err != nil {
		t.Fatalf("valid dcim image product rejected: %v", err)
	}
	invalidNone := validProduct
	invalidNone.SKU = "it-admin-validation-none"
	invalidNone.APIType = "none"
	if err := svc.validateProductForSave(context.Background(), invalidNone, nil); err == nil {
		t.Fatalf("expected api_type none auto-provision product to be rejected")
	}
	duplicateCycle := validProduct
	duplicateCycle.SKU = "it-admin-validation-dupe-cycle"
	if err := svc.validateProductForSave(context.Background(), duplicateCycle, []ProductPricing{{Cycle: "monthly", Price: 1}, {Cycle: "monthly", Price: 2}}); err == nil {
		t.Fatalf("expected duplicate pricing cycle to be rejected")
	}
	badPrice := validProduct
	badPrice.SKU = "it-admin-validation-bad-price"
	if err := svc.validateProductForSave(context.Background(), badPrice, []ProductPricing{{Cycle: "monthly", Price: -1}}); err == nil {
		t.Fatalf("expected negative pricing to be rejected")
	}
	noOSGroup := ProductGroup{CategoryID: 1, Slug: "it-admin-no-os", Name: "integration no os"}
	if err := tx.Create(&noOSGroup).Error; err != nil {
		t.Fatalf("create no-os group: %v", err)
	}
	noOSProduct := validProduct
	noOSProduct.SKU = "it-admin-validation-no-os"
	noOSProduct.GroupID = noOSGroup.ID
	if err := svc.validateProductForSave(context.Background(), noOSProduct, nil); err == nil {
		t.Fatalf("expected dcim image product without default image and group OS options to be rejected")
	}
}

func TestUserOpsPointsCouponsAndMallIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed user ops 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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	membership, err := svc.getOrCreateMembership(ctx, user.ID)
	if err != nil {
		t.Fatalf("membership: %v", err)
	}
	if err := tx.Model(&UserMembership{}).
		Where("id = ?", membership.ID).
		Updates(map[string]any{"available_points": 20000, "total_points": 20000}).Error; err != nil {
		t.Fatalf("seed points: %v", err)
	}

	taskType := "qq_community"
	if err := tx.Where("user_id = ? AND task_type = ?", user.ID, taskType).Delete(&PointTask{}).Error; err != nil {
		t.Fatalf("clear old point task: %v", err)
	}
	taskAnswer := svc.userOpsSettingString(ctx, "points.qq_answer", "")
	if _, err := svc.CompletePointTask(ctx, user.ID, taskType, taskAnswer); err != nil {
		t.Fatalf("complete point task: %v", err)
	}
	var taskLog PointLog
	if err := tx.Where("user_id = ? AND ref_no = ?", user.ID, fmt.Sprintf("task:%s:%d", taskType, user.ID)).First(&taskLog).Error; err != nil {
		t.Fatalf("load task point log: %v", err)
	}
	if taskLog.OrderID != nil {
		t.Fatalf("non-order point task wrote order_id = %v, want NULL", *taskLog.OrderID)
	}

	checkinToday := pointTaskLocalDate(time.Now())
	if err := tx.Where("user_id = ? AND task_type = ? AND checkin_date = ?", user.ID, "daily_checkin", checkinToday).Delete(&PointTask{}).Error; err != nil {
		t.Fatalf("clear old daily checkin: %v", err)
	}
	if _, err := svc.CompletePointTask(ctx, user.ID, "daily_checkin", ""); err != nil {
		t.Fatalf("daily checkin: %v", err)
	}
	var checkinTask PointTask
	if err := tx.Where("user_id = ? AND task_type = ? AND checkin_date = ?", user.ID, "daily_checkin", checkinToday).First(&checkinTask).Error; err != nil {
		t.Fatalf("load daily checkin task: %v", err)
	}
	if checkinTask.CheckinDate == nil || checkinTask.CheckinDate.In(pointTaskLocation()).Format("2006-01-02") != checkinToday {
		t.Fatalf("daily checkin date = %#v, want %s", checkinTask.CheckinDate, checkinToday)
	}
	if _, err := svc.CompletePointTask(ctx, user.ID, "daily_checkin", ""); err == nil {
		t.Fatalf("second daily checkin succeeded, want already completed error")
	} else {
		var httpErr HTTPError
		if !errors.As(err, &httpErr) || httpErr.Status != http.StatusBadRequest {
			t.Fatalf("second daily checkin error = %v, want bad request", err)
		}
	}

	code := "IT" + strings.ToUpper(randomToken(8))
	desc := "integration member coupon"
	couponOut, err := svc.CreatePromotionCoupon(ctx, user.ID, PromotionCouponCreateRequest{
		Code:           &code,
		Description:    &desc,
		Type:           "fixed",
		Value:          100,
		MaxUses:        1,
		MaxUsesPerUser: 1,
		ClaimMethod:    "code",
	})
	if err != nil {
		t.Fatalf("create member coupon: %v", err)
	}
	couponMap, _ := couponOut["coupon"].(map[string]any)
	if couponMap["code"] != code {
		t.Fatalf("coupon output = %#v", couponOut)
	}
	var couponLog PointLog
	if err := tx.Where("user_id = ? AND ref_no = ?", user.ID, "coupon:create:"+code).First(&couponLog).Error; err != nil {
		t.Fatalf("load coupon point log: %v", err)
	}
	if couponLog.OrderID != nil {
		t.Fatalf("member coupon point log wrote order_id = %v, want NULL", *couponLog.OrderID)
	}

	mallCode := "IM" + strings.ToUpper(randomToken(8))
	mallDesc := "integration mall coupon"
	startedAt := time.Now().UTC().Add(-time.Hour)
	coupon := PromoCode{
		Code:              mallCode,
		Description:       &mallDesc,
		IssuerUserID:      &user.ID,
		IssuerType:        stringPtr("member"),
		Type:              "fixed",
		Value:             100,
		Scope:             "all",
		DiscountLevel:     "order",
		MaxUses:           1,
		MaxUsesPerUser:    1,
		ClaimMethod:       "mall",
		ListOnMall:        true,
		IsEnabled:         true,
		AppliesToRenewals: true,
		StartsAt:          &startedAt,
	}
	if err := tx.Create(&coupon).Error; err != nil {
		t.Fatalf("create mall promo code: %v", err)
	}
	item := CouponMallItem{
		PromoCodeID:         coupon.ID,
		IssuerUserID:        user.ID,
		Title:               &mallDesc,
		Description:         &mallDesc,
		PointsPrice:         500,
		StockTotal:          1,
		StockRemaining:      1,
		Status:              "active",
		Scope:               "public",
		Category:            stringPtr("integration"),
		MaxClaimsPerUser:    1,
		RequiredMemberLevel: 0,
		IsHot:               true,
	}
	if err := tx.Create(&item).Error; err != nil {
		t.Fatalf("create mall item: %v", err)
	}
	items, err := svc.ListMallItems(ctx, user.ID, map[string]string{"type": "fixed", "min_points": "500", "max_points": "500", "is_hot": "true"}, 20, 0)
	if err != nil {
		t.Fatalf("list mall items: %v", err)
	}
	if total, _ := items["total"].(int64); total < 1 {
		t.Fatalf("mall filters did not find seeded item: %#v", items)
	}
	claim, err := svc.ClaimMallCoupon(ctx, user.ID, item.ID)
	if err != nil {
		t.Fatalf("claim mall coupon: %v", err)
	}
	if claim["code"] != mallCode {
		t.Fatalf("claim output = %#v", claim)
	}
	var claimLog PointLog
	if err := tx.Where("user_id = ? AND ref_no = ?", user.ID, fmt.Sprintf("mall:claim:%d:%d", item.ID, user.ID)).First(&claimLog).Error; err != nil {
		t.Fatalf("load mall claim point log: %v", err)
	}
	if claimLog.OrderID != nil {
		t.Fatalf("mall claim point log wrote order_id = %v, want NULL", *claimLog.OrderID)
	}
}

func TestNotificationUnreadCountIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed notification 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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}
	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	now := time.Now().UTC()
	notification := Notification{
		UserID:     user.ID,
		Category:   "system",
		Level:      "info",
		SenderType: "system",
		Title:      "integration unread notification",
		Content:    "integration",
		IsRead:     false,
	}
	if err := tx.Create(&notification).Error; err != nil {
		t.Fatalf("create notification: %v", err)
	}
	announcement := Announcement{
		Title:          "integration unread announcement",
		Content:        "integration",
		Category:       "announcement",
		Level:          "info",
		TargetType:     "all",
		TargetAudience: "all",
		IsPublished:    true,
		PublishedAt:    &now,
	}
	if err := tx.Create(&announcement).Error; err != nil {
		t.Fatalf("create announcement: %v", err)
	}
	if err := tx.Where("announcement_id = ? AND user_id = ?", announcement.ID, user.ID).Delete(&AnnouncementRead{}).Error; err != nil {
		t.Fatalf("clear stale announcement read: %v", err)
	}

	counts, err := svc.NotificationUnreadCount(ctx, user.ID)
	if err != nil {
		t.Fatalf("unread count: %v", err)
	}
	if got := counts["notifications"].(int64); got < 1 {
		t.Fatalf("notifications unread = %d, want at least 1; counts=%#v", got, counts)
	}
	if got := counts["announcements"].(int64); got < 1 {
		t.Fatalf("announcements unread = %d, want at least 1; counts=%#v", got, counts)
	}
	if counts["total"].(int64) != counts["notifications"].(int64)+counts["announcements"].(int64) {
		t.Fatalf("total mismatch: %#v", counts)
	}

	if err := tx.Create(&AnnouncementRead{AnnouncementID: announcement.ID, UserID: user.ID, ReadAt: now}).Error; err != nil {
		t.Fatalf("mark announcement read: %v", err)
	}
	afterRead, err := svc.NotificationUnreadCount(ctx, user.ID)
	if err != nil {
		t.Fatalf("unread count after read: %v", err)
	}
	if afterRead["announcements"].(int64) != counts["announcements"].(int64)-1 {
		t.Fatalf("announcement read was not reflected: before=%#v after=%#v", counts, afterRead)
	}
}

func TestAdminAgentReviewAndManualActionsIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database-backed admin agent 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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}
	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	tier := AgentTier{
		Name:                           "integration agent tier",
		Code:                           "it-agent-" + strings.ToLower(randomToken(6)),
		CostRate:                       8000,
		MinMemberLevel:                 0,
		MonthlySalesRequired:           12345,
		MonthlyActiveCustomersRequired: 3,
		MaxRefundRateBps:               2500,
		MaxMonthlySubsidyCents:         6789,
		MinBalanceForSubsidyCents:      4567,
		ObserveDays:                    9,
		FreezeDays:                     11,
		AutoDowngradeEnabled:           true,
		Enabled:                        true,
	}
	if err := tx.Create(&tier).Error; err != nil {
		t.Fatalf("create tier: %v", err)
	}
	tiers, err := svc.ListAgentTiers(ctx)
	if err != nil {
		t.Fatalf("list tiers: %v", err)
	}
	var foundTier map[string]any
	for _, item := range tiers {
		if int64Value(item["id"]) == tier.ID {
			foundTier = item
			break
		}
	}
	if foundTier == nil || int64Value(foundTier["monthly_sales_required"]) != int64(tier.MonthlySalesRequired) || int64Value(foundTier["observe_days"]) != int64(tier.ObserveDays) {
		t.Fatalf("agent tier advanced fields missing: %#v", foundTier)
	}
	if err := tx.Where("user_id = ?", user.ID).Delete(&UserAgent{}).Error; err != nil {
		t.Fatalf("clear existing user agent: %v", err)
	}
	agent := UserAgent{UserID: user.ID, Status: "pending", ApplyTier: &tier.Code}
	if err := tx.Create(&agent).Error; err != nil {
		t.Fatalf("create pending agent: %v", err)
	}

	rejected, err := svc.AdminReviewAgent(ctx, agent.ID, map[string]any{"action": "reject", "reject_reason": "资料不足", "remark": "资料不足"}, user.ID)
	if err != nil {
		t.Fatalf("reject agent: %v", err)
	}
	if rejected["status"] != "rejected" {
		t.Fatalf("reject status = %#v, want rejected; data=%#v", rejected["status"], rejected)
	}

	approved, err := svc.AdminReviewAgent(ctx, agent.ID, map[string]any{"action": "approve", "agent_tier_id": tier.ID, "remark": "ok"}, user.ID)
	if err != nil {
		t.Fatalf("approve agent: %v", err)
	}
	if approved["status"] != "active" || int64Value(approved["agent_tier_id"]) != tier.ID {
		t.Fatalf("approve data = %#v", approved)
	}
	if approved["agent_tier"] == nil || approved["user"] == nil {
		t.Fatalf("agent detail did not include nested user/tier: %#v", approved)
	}

	locked, err := svc.AdminAgentManualAction(ctx, agent.ID, map[string]any{"action": "lock", "reason": "risk"}, user.ID)
	if err != nil {
		t.Fatalf("lock agent: %v", err)
	}
	if locked["status"] != "risk_locked" {
		t.Fatalf("lock status = %#v, want risk_locked", locked["status"])
	}
	restored, err := svc.AdminAgentManualAction(ctx, agent.ID, map[string]any{"action": "restore", "reason": "ok"}, user.ID)
	if err != nil {
		t.Fatalf("restore agent: %v", err)
	}
	if restored["status"] != "active" {
		t.Fatalf("restore status = %#v, want active", restored["status"])
	}

	settlements := []AgentSettlement{
		{OrderID: 910001, OrderNo: "IT-SETTLE-1", SettlementType: "agent_profit", UserID: user.ID, StandardAmount: 20000, CustomerPayAmount: 18000, DeltaAmount: 2000, Status: "settled"},
		{OrderID: 910002, OrderNo: "IT-SETTLE-2", SettlementType: "agent_subsidy", UserID: user.ID, StandardAmount: 20000, CustomerPayAmount: 15000, DeltaAmount: -3000, Status: "frozen"},
		{OrderID: 910003, OrderNo: "IT-SETTLE-3", SettlementType: "member_commission", UserID: user.ID + 999999, StandardAmount: 20000, CustomerPayAmount: 20000, DeltaAmount: 500, Status: "pending"},
	}
	if err := tx.Create(&settlements).Error; err != nil {
		t.Fatalf("create settlements: %v", err)
	}
	filtered, err := svc.AdminListAgentSettlements(ctx, AdminAgentSettlementQuery{SettlementType: "agent_profit", Status: "settled", UserID: &user.ID, Limit: 20})
	if err != nil {
		t.Fatalf("list filtered settlements: %v", err)
	}
	items, _ := filtered["items"].([]map[string]any)
	if len(items) != 1 || items[0]["settlement_type"] != "agent_profit" || int64Value(items[0]["user_id"]) != user.ID {
		t.Fatalf("filtered settlements = %#v", filtered)
	}
	orderID := int64(910002)
	filteredByOrder, err := svc.AdminListAgentSettlements(ctx, AdminAgentSettlementQuery{OrderID: &orderID, Limit: 20})
	if err != nil {
		t.Fatalf("list settlement by order: %v", err)
	}
	orderItems, _ := filteredByOrder["items"].([]map[string]any)
	if len(orderItems) != 1 || orderItems[0]["order_no"] != "IT-SETTLE-2" {
		t.Fatalf("order filtered settlements = %#v", filteredByOrder)
	}
}

func TestAdminMallManagementIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database integration test")
	}
	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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}
	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	code := "AM" + strings.ToUpper(randomToken(8))
	created, err := svc.AdminCreateMallCoupon(ctx, user.ID, map[string]any{
		"code":                        code,
		"title":                       "integration mall item",
		"subtitle":                    "integration subtitle",
		"description":                 "integration mall coupon",
		"category":                    "integration",
		"type":                        "fixed",
		"value":                       1200,
		"points_price":                66,
		"stock_total":                 3,
		"max_claims_per_user":         2,
		"max_claims_per_user_per_day": 1,
		"required_member_level":       1,
		"is_hot":                      true,
	})
	if err != nil {
		t.Fatalf("create admin mall coupon: %v", err)
	}
	itemID := int64Value(created["id"])
	promoCodeID := int64Value(created["promo_code_id"])
	if itemID <= 0 || promoCodeID <= 0 || created["code"] != code || created["type"] != "fixed" {
		t.Fatalf("created mall item missing joined coupon fields: %#v", created)
	}

	listed, err := svc.AdminListMallItems(ctx, AdminListQuery{Status: "active", Search: "integration", Limit: 20}, "integration")
	if err != nil {
		t.Fatalf("list admin mall items: %v", err)
	}
	items, _ := listed["items"].([]map[string]any)
	found := false
	for _, item := range items {
		if int64Value(item["id"]) == itemID && item["code"] == code && int64Value(item["value"]) == 1200 {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("admin mall list did not include joined item: %#v", listed)
	}

	updated, err := svc.AdminUpdateMallItem(ctx, itemID, map[string]any{"stock_total": 5, "points_price": 88, "status": "active"})
	if err != nil {
		t.Fatalf("update admin mall item: %v", err)
	}
	if int64Value(updated["stock_total"]) != 5 || int64Value(updated["stock_remaining"]) != 5 || int64Value(updated["points_price"]) != 88 {
		t.Fatalf("updated mall item = %#v", updated)
	}

	claim := CouponMallClaim{MallItemID: itemID, PromoCodeID: promoCodeID, UserID: user.ID, PointsPaid: 88, Code: &code, Status: "claimed"}
	if err := tx.Create(&claim).Error; err != nil {
		t.Fatalf("create mall claim: %v", err)
	}
	claims, err := svc.AdminListMallClaims(ctx, AdminListQuery{Status: "claimed", Limit: 20}, &itemID)
	if err != nil {
		t.Fatalf("list admin mall claims: %v", err)
	}
	claimItems, _ := claims["items"].([]map[string]any)
	if len(claimItems) != 1 || claimItems[0]["username"] == nil || int64Value(claimItems[0]["mall_item_id"]) != itemID {
		t.Fatalf("admin mall claims = %#v", claims)
	}

	deleted, err := svc.AdminDeleteMallItem(ctx, itemID)
	if err != nil {
		t.Fatalf("delete admin mall item: %v", err)
	}
	if deleted["status"] != "paused" {
		t.Fatalf("delete mall item = %#v", deleted)
	}
}

func TestAdminPromoCodeManagementIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database integration test")
	}
	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()

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	code := "PC" + strings.ToUpper(randomToken(8))
	created, err := svc.AdminSavePromoCode(ctx, 0, map[string]any{
		"code":                  code,
		"description":           "integration promo code",
		"type":                  "threshold_fixed",
		"value":                 500,
		"discount_level":        "product",
		"scope":                 "product",
		"scope_ids":             []any{101, 102},
		"max_uses":              9,
		"max_uses_per_user":     2,
		"applicable_cycles":     []any{"monthly", "quarterly"},
		"min_order_amount":      1000,
		"min_quantity":          1,
		"max_discount_amount":   2000,
		"required_product_ids":  []any{101},
		"required_group_ids":    []any{202},
		"required_category_ids": []any{303},
		"allowed_user_ids":      []any{2},
		"new_users_only":        true,
		"applies_to_renewals":   false,
		"is_stackable":          true,
		"is_enabled":            true,
	})
	if err != nil {
		t.Fatalf("create promo code: %v", err)
	}
	id := int64Value(created["id"])
	if id <= 0 || created["code"] != code || created["discount_level"] != "product" || int64Value(created["max_uses"]) != 9 {
		t.Fatalf("created promo code = %#v", created)
	}
	if len(created["scope_ids"].([]any)) != 2 || len(created["required_product_ids"].([]any)) != 1 {
		t.Fatalf("promo arrays not returned: %#v", created)
	}

	enabled := true
	listed, err := svc.AdminListPromoCodes(ctx, AdminListQuery{Search: code, Limit: 20}, "product", &enabled)
	if err != nil {
		t.Fatalf("list promo codes: %v", err)
	}
	items, _ := listed["items"].([]map[string]any)
	if len(items) != 1 || items[0]["code"] != code {
		t.Fatalf("filtered promo list = %#v", listed)
	}

	updated, err := svc.AdminSavePromoCode(ctx, id, map[string]any{"is_enabled": false, "max_uses_per_user": 3, "allowed_user_ids": []any{2, 3}})
	if err != nil {
		t.Fatalf("update promo code: %v", err)
	}
	if updated["is_enabled"] != false || int64Value(updated["max_uses_per_user"]) != 3 || len(updated["allowed_user_ids"].([]any)) != 2 {
		t.Fatalf("updated promo code = %#v", updated)
	}
}

func TestAdminUpstreamActionListAndUpdateIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database integration test")
	}
	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()

	var user UserAccount
	if err := db.Where("status <> ?", "deleted").Order("id ASC").First(&user).Error; err != nil {
		t.Fatalf("load test user: %v", err)
	}
	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	method := "POST"
	path := "/integration/action"
	resourceID := "integration-resource"
	errMsg := "integration failed"
	action := UpstreamActionLog{
		UserID:         &user.ID,
		ActionType:     "instance_delete",
		ResourceType:   "instance",
		ResourceID:     &resourceID,
		Source:         "manual",
		Status:         "failed",
		RequestMethod:  &method,
		RequestPath:    &path,
		RequestPayload: jsonRawFromAny(map[string]any{"integration": true}),
		ErrorMessage:   &errMsg,
	}
	if err := tx.Create(&action).Error; err != nil {
		t.Fatalf("create upstream action: %v", err)
	}
	listed, err := svc.AdminListUpstreamActions(ctx, AdminUpstreamActionQuery{Status: "failed", Source: "manual", ActionType: "instance_delete", UserID: &user.ID, Search: "integration", Limit: 20})
	if err != nil {
		t.Fatalf("list upstream actions: %v", err)
	}
	items, _ := listed["items"].([]map[string]any)
	if len(items) != 1 || int64Value(items[0]["id"]) != action.ID || items[0]["username"] == nil {
		t.Fatalf("filtered upstream actions = %#v", listed)
	}
	updated, err := svc.AdminUpstreamActionUpdate(ctx, action.ID, "ignored", "handled in integration test")
	if err != nil {
		t.Fatalf("update upstream action: %v", err)
	}
	if updated["status"] != "ignored" || updated["remark"] != "handled in integration test" || updated["completed_at"] == nil {
		t.Fatalf("updated upstream action = %#v", updated)
	}
}

func TestAdminTaskRunsIntegration(t *testing.T) {
	if os.Getenv("QYCW_DB_INTEGRATION") != "1" {
		t.Skip("set QYCW_DB_INTEGRATION=1 to run database integration test")
	}
	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()

	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("begin tx: %v", tx.Error)
	}
	defer tx.Rollback()
	svc := &Service{db: tx}
	ctx := context.Background()

	now := time.Now().UTC()
	rows := []map[string]any{
		{"task_key": "upstream_actions", "task_name": "Upstream action sync", "trigger_type": "manual", "status": "success", "started_at": now, "finished_at": now, "duration_ms": 12, "summary": jsonRawFromAny(map[string]any{"integration": true}), "created_at": now, "updated_at": now},
		{"task_key": "settlement", "task_name": "Settlement worker", "trigger_type": "auto", "status": "failed", "started_at": now, "finished_at": now, "duration_ms": 34, "error_message": "integration failed", "created_at": now, "updated_at": now},
	}
	if err := tx.Table("task_run_logs").Create(&rows).Error; err != nil {
		t.Fatalf("create task runs: %v", err)
	}
	taskList, err := svc.AdminTaskList(ctx)
	if err != nil {
		t.Fatalf("task list: %v", err)
	}
	foundTask := false
	for _, item := range taskList {
		if item["key"] == "upstream_actions" && item["latest_run"] != nil {
			foundTask = true
			break
		}
	}
	if !foundTask {
		t.Fatalf("task list missing latest run: %#v", taskList)
	}
	listed, err := svc.AdminListTaskRuns(ctx, AdminListQuery{Status: "success", Search: "Upstream", Limit: 20}, "upstream_actions", "manual")
	if err != nil {
		t.Fatalf("list task runs: %v", err)
	}
	items, _ := listed["items"].([]map[string]any)
	if len(items) != 1 || items[0]["task_key"] != "upstream_actions" || items[0]["status"] != "success" {
		t.Fatalf("filtered task runs = %#v", listed)
	}
}

func integrationDSN(t *testing.T) string {
	t.Helper()
	env := map[string]string{}
	path := filepath.Join("..", "..", "..", ".env")
	file, err := os.Open(path)
	if err != nil {
		t.Fatalf("open %s: %v", path, err)
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		env[strings.TrimSpace(parts[0])] = strings.Trim(strings.TrimSpace(parts[1]), `"`)
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("read env: %v", err)
	}
	port, _ := strconv.Atoi(valueOr(env["DB_PORT"], "3306"))
	return env["DB_USER"] + ":" + env["DB_PASSWORD"] + "@tcp(" + env["DB_HOST"] + ":" + strconv.Itoa(port) + ")/" + env["DB_NAME"] + "?charset=utf8mb4&parseTime=True&loc=Local"
}

func valueOr(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}
