package services

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"testing"
	"time"

	"quyun/v2/app/commands/testx"
	"quyun/v2/app/errorx"
	"quyun/v2/app/http/tenant/dto"
	"quyun/v2/database"
	"quyun/v2/database/models"
	"quyun/v2/pkg/consts"

	"github.com/samber/lo"
	. "github.com/smartystreets/goconvey/convey"
	"github.com/stretchr/testify/suite"

	_ "go.ipao.vip/atom"
	"go.ipao.vip/atom/contracts"
	"go.ipao.vip/gen/types"
	"go.uber.org/dig"
)

type OrderTestSuiteInjectParams struct {
	dig.In

	DB       *sql.DB
	Initials []contracts.Initial `group:"initials"` // nolint:structcheck
}

type OrderTestSuite struct {
	suite.Suite
	OrderTestSuiteInjectParams
}

func Test_Order(t *testing.T) {
	providers := testx.Default().With(Provide)

	testx.Serve(providers, t, func(p OrderTestSuiteInjectParams) {
		suite.Run(t, &OrderTestSuite{OrderTestSuiteInjectParams: p})
	})
}

func (s *OrderTestSuite) truncate(ctx context.Context, tableNames ...string) {
	database.Truncate(ctx, s.DB, tableNames...)
}

func (s *OrderTestSuite) seedTenantUser(ctx context.Context, tenantID, userID, balance, frozen int64) {
	tu := &models.TenantUser{
		TenantID:      tenantID,
		UserID:        userID,
		Role:          types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}),
		Balance:       balance,
		BalanceFrozen: frozen,
		Status:        consts.UserStatusVerified,
	}
	So(tu.Create(ctx), ShouldBeNil)
}

func (s *OrderTestSuite) seedPublishedContent(ctx context.Context, tenantID, ownerUserID int64) *models.Content {
	m := &models.Content{
		TenantID:            tenantID,
		UserID:              ownerUserID,
		Title:               "标题",
		Description:         "描述",
		Status:              consts.ContentStatusPublished,
		Visibility:          consts.ContentVisibilityTenantOnly,
		PreviewSeconds:      consts.DefaultContentPreviewSeconds,
		PreviewDownloadable: false,
		PublishedAt:         time.Now().UTC(),
	}
	So(m.Create(ctx), ShouldBeNil)
	return m
}

func (s *OrderTestSuite) seedContentPrice(ctx context.Context, tenantID, contentID, priceAmount int64) {
	p := &models.ContentPrice{
		TenantID:        tenantID,
		UserID:          1,
		ContentID:       contentID,
		Currency:        consts.CurrencyCNY,
		PriceAmount:     priceAmount,
		DiscountType:    consts.DiscountTypeNone,
		DiscountValue:   0,
		DiscountStartAt: time.Time{},
		DiscountEndAt:   time.Time{},
	}
	So(p.Create(ctx), ShouldBeNil)
}

func (s *OrderTestSuite) Test_AdminTopupUser() {
	Convey("Order.AdminTopupUser", s.T(), func() {
		ctx := s.T().Context()
		now := time.Now().UTC()
		tenantID := int64(1)
		operatorUserID := int64(10)
		targetUserID := int64(20)

		s.truncate(
			ctx,
			models.TableNameTenantLedger,
			models.TableNameOrderItem,
			models.TableNameOrder,
			models.TableNameTenantUser,
		)

		Convey("参数非法应返回错误", func() {
			_, err := Order.AdminTopupUser(ctx, 0, operatorUserID, targetUserID, 100, "", "", now)
			So(err, ShouldNotBeNil)

			_, err = Order.AdminTopupUser(ctx, tenantID, 0, targetUserID, 100, "", "", now)
			So(err, ShouldNotBeNil)

			_, err = Order.AdminTopupUser(ctx, tenantID, operatorUserID, 0, 100, "", "", now)
			So(err, ShouldNotBeNil)

			_, err = Order.AdminTopupUser(ctx, tenantID, operatorUserID, targetUserID, 0, "", "", now)
			So(err, ShouldNotBeNil)
		})

		Convey("目标用户不属于该租户应返回前置条件失败", func() {
			_, err := Order.AdminTopupUser(ctx, tenantID, operatorUserID, targetUserID, 100, "idem_not_member", "", now)
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
		})

		Convey("成功充值并写入账本", func() {
			s.seedTenantUser(ctx, tenantID, targetUserID, 0, 0)

			orderModel, err := Order.AdminTopupUser(ctx, tenantID, operatorUserID, targetUserID, 300, "idem_topup_1", "充值原因", now)
			So(err, ShouldBeNil)
			So(orderModel, ShouldNotBeNil)
			So(orderModel.ID, ShouldBeGreaterThan, 0)
			So(orderModel.Type, ShouldEqual, consts.OrderTypeTopup)
			So(orderModel.Status, ShouldEqual, consts.OrderStatusPaid)
			So(orderModel.AmountPaid, ShouldEqual, 300)

			var snap map[string]any
			So(json.Unmarshal([]byte(orderModel.Snapshot), &snap), ShouldBeNil)
			So(snap["operator_user_id"], ShouldEqual, float64(operatorUserID))
			So(snap["target_user_id"], ShouldEqual, float64(targetUserID))
			So(snap["amount"], ShouldEqual, float64(300))

			var tu models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, targetUserID).First(&tu).Error, ShouldBeNil)
			So(tu.Balance, ShouldEqual, 300)

			var ledgers []*models.TenantLedger
			So(_db.WithContext(ctx).
				Where("tenant_id = ? AND user_id = ? AND type = ?", tenantID, targetUserID, consts.TenantLedgerTypeCreditTopup).
				Order("id ASC").
				Find(&ledgers).Error, ShouldBeNil)
			So(len(ledgers), ShouldEqual, 1)
			So(ledgers[0].OrderID, ShouldEqual, orderModel.ID)
			So(ledgers[0].Amount, ShouldEqual, 300)
		})

		Convey("幂等键重复调用不应重复入账", func() {
			s.seedTenantUser(ctx, tenantID, targetUserID, 0, 0)

			o1, err := Order.AdminTopupUser(ctx, tenantID, operatorUserID, targetUserID, 300, "idem_topup_2", "充值原因", now)
			So(err, ShouldBeNil)
			So(o1, ShouldNotBeNil)

			o2, err := Order.AdminTopupUser(ctx, tenantID, operatorUserID, targetUserID, 999, "idem_topup_2", "不同金额也不应重复处理", now.Add(time.Second))
			So(err, ShouldBeNil)
			So(o2, ShouldNotBeNil)
			So(o2.ID, ShouldEqual, o1.ID)

			var tu models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, targetUserID).First(&tu).Error, ShouldBeNil)
			So(tu.Balance, ShouldEqual, 300)
		})
	})
}

func (s *OrderTestSuite) Test_MyOrderPage() {
	Convey("Order.MyOrderPage", s.T(), func() {
		ctx := s.T().Context()
		now := time.Now().UTC()
		tenantID := int64(1)
		userID := int64(2)

		s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder)

		Convey("参数非法应返回错误", func() {
			_, err := Order.MyOrderPage(ctx, 0, userID, &dto.MyOrderListFilter{})
			So(err, ShouldNotBeNil)
		})

		Convey("空数据应返回 total=0", func() {
			pager, err := Order.MyOrderPage(ctx, tenantID, userID, &dto.MyOrderListFilter{})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 0)
		})

		Convey("按 content_id 过滤", func() {
			o1 := &models.Order{
				TenantID:   tenantID,
				UserID:     userID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 100,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(o1.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        userID,
				OrderID:       o1.ID,
				ContentID:     111,
				ContentUserID: 1,
				AmountPaid:    100,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			o2 := &models.Order{
				TenantID:   tenantID,
				UserID:     userID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 200,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now.Add(time.Minute),
				CreatedAt:  now.Add(time.Minute),
				UpdatedAt:  now.Add(time.Minute),
			}
			So(o2.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        userID,
				OrderID:       o2.ID,
				ContentID:     222,
				ContentUserID: 1,
				AmountPaid:    200,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now.Add(time.Minute),
				UpdatedAt:     now.Add(time.Minute),
			}).Create(ctx), ShouldBeNil)

			pager, err := Order.MyOrderPage(ctx, tenantID, userID, &dto.MyOrderListFilter{
				ContentID: lo.ToPtr(int64(111)),
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 1)
		})
	})
}

func (s *OrderTestSuite) Test_MyOrderDetail() {
	Convey("Order.MyOrderDetail", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)
		userID := int64(2)

		s.truncate(ctx, models.TableNameOrder, models.TableNameOrderItem)

		Convey("参数非法应返回错误", func() {
			_, err := Order.MyOrderDetail(ctx, 0, userID, 1)
			So(err, ShouldNotBeNil)
		})

		Convey("订单不存在应返回错误", func() {
			_, err := Order.MyOrderDetail(ctx, tenantID, userID, 999)
			So(err, ShouldNotBeNil)
		})
	})
}

func (s *OrderTestSuite) Test_AdminOrderPage() {
	Convey("Order.AdminOrderPage", s.T(), func() {
		ctx := s.T().Context()
		now := time.Now().UTC()
		tenantID := int64(1)

		s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder)

		Convey("参数非法应返回错误", func() {
			_, err := Order.AdminOrderPage(ctx, 0, &dto.AdminOrderListFilter{})
			So(err, ShouldNotBeNil)
		})

		Convey("空数据应返回 total=0", func() {
			pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 0)
		})

		Convey("按 paid_at 时间窗过滤", func() {
			o1 := &models.Order{
				TenantID:   tenantID,
				UserID:     2,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 100,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now.Add(-time.Hour),
				CreatedAt:  now.Add(-time.Hour),
				UpdatedAt:  now.Add(-time.Hour),
			}
			So(o1.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        2,
				OrderID:       o1.ID,
				ContentID:     333,
				ContentUserID: 1,
				AmountPaid:    100,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now.Add(-time.Hour),
				UpdatedAt:     now.Add(-time.Hour),
			}).Create(ctx), ShouldBeNil)

			o2 := &models.Order{
				TenantID:   tenantID,
				UserID:     3,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 200,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(o2.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        3,
				OrderID:       o2.ID,
				ContentID:     444,
				ContentUserID: 1,
				AmountPaid:    200,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			from := now.Add(-10 * time.Minute)
			to := now.Add(10 * time.Minute)
			pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{
				PaidAtFrom: &from,
				PaidAtTo:   &to,
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 1)
		})

		Convey("按 content_id 过滤", func() {
			s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder)

			o1 := &models.Order{
				TenantID:   tenantID,
				UserID:     2,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 100,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(o1.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        2,
				OrderID:       o1.ID,
				ContentID:     555,
				ContentUserID: 1,
				AmountPaid:    100,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{
				ContentID: lo.ToPtr(int64(555)),
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 1)
		})

		Convey("组合筛选：user_id + status + amount_paid 区间 + content_id", func() {
			s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder)

			statusPaid := consts.OrderStatusPaid
			userID := int64(7)
			contentID := int64(777)

			// 命中：user_id=7, status=paid, amount_paid=500, content_id=777
			oHit := &models.Order{
				TenantID:   tenantID,
				UserID:     userID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 500,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(oHit.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        userID,
				OrderID:       oHit.ID,
				ContentID:     contentID,
				ContentUserID: 1,
				AmountPaid:    500,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			// 不命中：amount_paid 不在区间
			oNoAmount := &models.Order{
				TenantID:   tenantID,
				UserID:     userID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 50,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(oNoAmount.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        userID,
				OrderID:       oNoAmount.ID,
				ContentID:     contentID,
				ContentUserID: 1,
				AmountPaid:    50,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			// 不命中：status 不同
			oNoStatus := &models.Order{
				TenantID:   tenantID,
				UserID:     userID,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusCreated,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 500,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(oNoStatus.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        userID,
				OrderID:       oNoStatus.ID,
				ContentID:     contentID,
				ContentUserID: 1,
				AmountPaid:    500,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			// 不命中：user_id 不同
			oNoUser := &models.Order{
				TenantID:   tenantID,
				UserID:     8,
				Type:       consts.OrderTypeContentPurchase,
				Status:     consts.OrderStatusPaid,
				Currency:   consts.CurrencyCNY,
				AmountPaid: 500,
				Snapshot:   types.JSON([]byte("{}")),
				PaidAt:     now,
				CreatedAt:  now,
				UpdatedAt:  now,
			}
			So(oNoUser.Create(ctx), ShouldBeNil)
			So((&models.OrderItem{
				TenantID:      tenantID,
				UserID:        8,
				OrderID:       oNoUser.ID,
				ContentID:     contentID,
				ContentUserID: 1,
				AmountPaid:    500,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}).Create(ctx), ShouldBeNil)

			min := int64(100)
			max := int64(900)
			pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{
				UserID:        &userID,
				Status:        &statusPaid,
				ContentID:     &contentID,
				AmountPaidMin: &min,
				AmountPaidMax: &max,
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 1)
		})
	})
}

func (s *OrderTestSuite) Test_AdminOrderDetail() {
	Convey("Order.AdminOrderDetail", s.T(), func() {
		ctx := s.T().Context()
		tenantID := int64(1)

		s.truncate(ctx, models.TableNameOrder, models.TableNameOrderItem)

		Convey("参数非法应返回错误", func() {
			_, err := Order.AdminOrderDetail(ctx, 0, 1)
			So(err, ShouldNotBeNil)
		})

		Convey("订单不存在应返回错误", func() {
			_, err := Order.AdminOrderDetail(ctx, tenantID, 999)
			So(err, ShouldNotBeNil)
		})
	})
}

func (s *OrderTestSuite) Test_AdminRefundOrder() {
	Convey("Order.AdminRefundOrder", s.T(), func() {
		ctx := s.T().Context()
		now := time.Now().UTC()
		tenantID := int64(1)
		operatorUserID := int64(10)
		buyerUserID := int64(20)

		s.truncate(
			ctx,
			models.TableNameTenantLedger,
			models.TableNameContentAccess,
			models.TableNameOrderItem,
			models.TableNameOrder,
			models.TableNameTenantUser,
		)

		Convey("参数非法应返回错误", func() {
			_, err := Order.AdminRefundOrder(ctx, 0, operatorUserID, 1, false, "", "", now)
			So(err, ShouldNotBeNil)
		})

		Convey("订单非已支付状态应返回状态冲突", func() {
			s.seedTenantUser(ctx, tenantID, buyerUserID, 0, 0)

			orderModel := &models.Order{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				Type:           consts.OrderTypeContentPurchase,
				Status:         consts.OrderStatusCreated,
				Currency:       consts.CurrencyCNY,
				AmountOriginal: 100,
				AmountDiscount: 0,
				AmountPaid:     100,
				Snapshot:       types.JSON([]byte("{}")),
				PaidAt:         now,
				CreatedAt:      now,
				UpdatedAt:      now,
			}
			So(orderModel.Create(ctx), ShouldBeNil)

			_, err := Order.AdminRefundOrder(ctx, tenantID, operatorUserID, orderModel.ID, false, "原因", "", now)
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeStatusConflict)
		})

		Convey("已超过默认退款时间窗且非强制应失败", func() {
			s.seedTenantUser(ctx, tenantID, buyerUserID, 0, 0)

			orderModel := &models.Order{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				Type:           consts.OrderTypeContentPurchase,
				Status:         consts.OrderStatusPaid,
				Currency:       consts.CurrencyCNY,
				AmountOriginal: 100,
				AmountDiscount: 0,
				AmountPaid:     100,
				Snapshot:       types.JSON([]byte("{}")),
				PaidAt:         now.Add(-consts.DefaultOrderRefundWindow).Add(-time.Second),
				CreatedAt:      now,
				UpdatedAt:      now,
			}
			So(orderModel.Create(ctx), ShouldBeNil)

			_, err := Order.AdminRefundOrder(ctx, tenantID, operatorUserID, orderModel.ID, false, "原因", "", now)
			So(err, ShouldNotBeNil)
		})

		Convey("成功退款应回收权益并入账", func() {
			s.seedTenantUser(ctx, tenantID, buyerUserID, 0, 0)

			contentID := int64(123)
			orderModel := &models.Order{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				Type:           consts.OrderTypeContentPurchase,
				Status:         consts.OrderStatusPaid,
				Currency:       consts.CurrencyCNY,
				AmountOriginal: 300,
				AmountDiscount: 0,
				AmountPaid:     300,
				Snapshot:       types.JSON([]byte("{}")),
				PaidAt:         now,
				CreatedAt:      now,
				UpdatedAt:      now,
			}
			So(orderModel.Create(ctx), ShouldBeNil)

			item := &models.OrderItem{
				TenantID:      tenantID,
				UserID:        buyerUserID,
				OrderID:       orderModel.ID,
				ContentID:     contentID,
				ContentUserID: 999,
				AmountPaid:    300,
				Snapshot:      types.JSON([]byte("{}")),
				CreatedAt:     now,
				UpdatedAt:     now,
			}
			So(item.Create(ctx), ShouldBeNil)

			access := &models.ContentAccess{
				TenantID:  tenantID,
				UserID:    buyerUserID,
				ContentID: contentID,
				OrderID:   orderModel.ID,
				Status:    consts.ContentAccessStatusActive,
				CreatedAt: now,
				UpdatedAt: now,
				RevokedAt: time.Time{},
			}
			So(access.Create(ctx), ShouldBeNil)

			refunded, err := Order.AdminRefundOrder(ctx, tenantID, operatorUserID, orderModel.ID, false, "原因", "", now.Add(time.Minute))
			So(err, ShouldBeNil)
			So(refunded, ShouldNotBeNil)
			So(refunded.Status, ShouldEqual, consts.OrderStatusRefunded)

			var tu models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu).Error, ShouldBeNil)
			So(tu.Balance, ShouldEqual, 300)

			var access2 models.ContentAccess
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ? AND content_id = ?", tenantID, buyerUserID, contentID).First(&access2).Error, ShouldBeNil)
			So(access2.Status, ShouldEqual, consts.ContentAccessStatusRevoked)
			So(access2.RevokedAt.IsZero(), ShouldBeFalse)

			refunded2, err := Order.AdminRefundOrder(ctx, tenantID, operatorUserID, orderModel.ID, false, "原因2", "", now.Add(2*time.Minute))
			So(err, ShouldBeNil)
			So(refunded2.Status, ShouldEqual, consts.OrderStatusRefunded)

			var tu2 models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu2).Error, ShouldBeNil)
			So(tu2.Balance, ShouldEqual, 300)

			var ledgers []*models.TenantLedger
			So(_db.WithContext(ctx).
				Where("tenant_id = ? AND user_id = ? AND idempotency_key = ?", tenantID, buyerUserID, fmt.Sprintf("refund:%d", orderModel.ID)).
				Find(&ledgers).Error, ShouldBeNil)
			So(len(ledgers), ShouldEqual, 1)
		})
	})
}

func (s *OrderTestSuite) Test_PurchaseContent() {
	Convey("Order.PurchaseContent", s.T(), func() {
		ctx := s.T().Context()
		now := time.Now().UTC()
		tenantID := int64(1)
		ownerUserID := int64(100)
		buyerUserID := int64(200)

		s.truncate(
			ctx,
			models.TableNameTenantLedger,
			models.TableNameContentAccess,
			models.TableNameOrderItem,
			models.TableNameOrder,
			models.TableNameContentPrice,
			models.TableNameContent,
			models.TableNameTenantUser,
		)

		Convey("参数非法应返回错误", func() {
			_, err := Order.PurchaseContent(ctx, nil)
			So(err, ShouldNotBeNil)

			_, err = Order.PurchaseContent(ctx, &PurchaseContentParams{TenantID: 0, UserID: 1, ContentID: 1})
			So(err, ShouldNotBeNil)
		})

		Convey("内容未发布应返回前置条件失败", func() {
			s.seedTenantUser(ctx, tenantID, buyerUserID, 1000, 0)

			content := &models.Content{
				TenantID:            tenantID,
				UserID:              ownerUserID,
				Title:               "标题",
				Description:         "描述",
				Status:              consts.ContentStatusDraft,
				Visibility:          consts.ContentVisibilityTenantOnly,
				PreviewSeconds:      consts.DefaultContentPreviewSeconds,
				PreviewDownloadable: false,
			}
			So(content.Create(ctx), ShouldBeNil)

			_, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_not_published",
				Now:            now,
			})
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
		})

		Convey("免费内容购买应创建订单并授予权益（幂等）", func() {
			s.seedTenantUser(ctx, tenantID, buyerUserID, 1000, 0)
			content := s.seedPublishedContent(ctx, tenantID, ownerUserID)

			res1, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_free_1",
				Now:            now,
			})
			So(err, ShouldBeNil)
			So(res1, ShouldNotBeNil)
			So(res1.AmountPaid, ShouldEqual, 0)
			So(res1.Order, ShouldNotBeNil)
			So(res1.Order.Status, ShouldEqual, consts.OrderStatusPaid)
			So(res1.Access, ShouldNotBeNil)
			So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)

			var snap map[string]any
			So(json.Unmarshal([]byte(res1.Order.Snapshot), &snap), ShouldBeNil)
			So(snap["content_id"], ShouldEqual, float64(content.ID))
			So(snap["content_title"], ShouldEqual, content.Title)
			So(snap["amount_paid"], ShouldEqual, float64(0))

			res2, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_free_1",
				Now:            now.Add(time.Second),
			})
			So(err, ShouldBeNil)
			So(res2.Order.ID, ShouldEqual, res1.Order.ID)
		})

		Convey("付费内容购买应冻结+扣款并授予权益（幂等）", func() {
			s.truncate(
				ctx,
				models.TableNameTenantLedger,
				models.TableNameContentAccess,
				models.TableNameOrderItem,
				models.TableNameOrder,
				models.TableNameContentPrice,
				models.TableNameContent,
				models.TableNameTenantUser,
			)
			s.seedTenantUser(ctx, tenantID, buyerUserID, 1000, 0)
			content := s.seedPublishedContent(ctx, tenantID, ownerUserID)
			s.seedContentPrice(ctx, tenantID, content.ID, 300)

			res1, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_paid_1",
				Now:            now,
			})
			So(err, ShouldBeNil)
			So(res1, ShouldNotBeNil)
			So(res1.AmountPaid, ShouldEqual, 300)
			So(res1.Order, ShouldNotBeNil)
			So(res1.Order.Status, ShouldEqual, consts.OrderStatusPaid)
			So(res1.Access, ShouldNotBeNil)
			So(res1.Access.Status, ShouldEqual, consts.ContentAccessStatusActive)

			var snap map[string]any
			So(json.Unmarshal([]byte(res1.Order.Snapshot), &snap), ShouldBeNil)
			So(snap["content_id"], ShouldEqual, float64(content.ID))
			So(snap["amount_paid"], ShouldEqual, float64(300))
			So(snap["amount_original"], ShouldEqual, float64(300))

			var itemSnap map[string]any
			So(json.Unmarshal([]byte(res1.OrderItem.Snapshot), &itemSnap), ShouldBeNil)
			So(itemSnap["content_id"], ShouldEqual, float64(content.ID))
			So(itemSnap["amount_paid"], ShouldEqual, float64(300))

			var tu models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu).Error, ShouldBeNil)
			So(tu.Balance, ShouldEqual, 700)
			So(tu.BalanceFrozen, ShouldEqual, 0)

			res2, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_paid_1",
				Now:            now.Add(2 * time.Second),
			})
			So(err, ShouldBeNil)
			So(res2.Order.ID, ShouldEqual, res1.Order.ID)

			var tu2 models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, buyerUserID).First(&tu2).Error, ShouldBeNil)
			So(tu2.Balance, ShouldEqual, 700)
			So(tu2.BalanceFrozen, ShouldEqual, 0)
		})

		Convey("存在回滚标记时应稳定返回“失败+已回滚”", func() {
			s.truncate(
				ctx,
				models.TableNameTenantLedger,
				models.TableNameContentAccess,
				models.TableNameOrderItem,
				models.TableNameOrder,
				models.TableNameContentPrice,
				models.TableNameContent,
				models.TableNameTenantUser,
			)
			s.seedTenantUser(ctx, tenantID, buyerUserID, 1000, 0)
			content := s.seedPublishedContent(ctx, tenantID, ownerUserID)
			s.seedContentPrice(ctx, tenantID, content.ID, 300)

			rollbackKey := "idem_rollback_1:rollback"
			ledger := &models.TenantLedger{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				OrderID:        0,
				Type:           consts.TenantLedgerTypeUnfreeze,
				Amount:         1,
				BalanceBefore:  0,
				BalanceAfter:   0,
				FrozenBefore:   0,
				FrozenAfter:    0,
				IdempotencyKey: rollbackKey,
				Remark:         "rollback marker",
				CreatedAt:      now,
				UpdatedAt:      now,
			}
			So(ledger.Create(ctx), ShouldBeNil)

			_, err := Order.PurchaseContent(ctx, &PurchaseContentParams{
				TenantID:       tenantID,
				UserID:         buyerUserID,
				ContentID:      content.ID,
				IdempotencyKey: "idem_rollback_1",
				Now:            now.Add(time.Second),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "失败+已回滚")
		})
	})
}
