package user

import (
	"context"
	"fmt"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	usermodel "github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/constant"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// TestCancelWithdrawal_DoesNotRefundCommission 验证 HIF-140 修复：
// 用户撤销 pending 提现的事务体必须只更新 withdrawal.status，
// 严禁触发 UpdateCommission（commission 列读 / 写）或写 333/338 commission 日志。
//
// sqlmock 严格匹配期望 SQL：只允许出现 BEGIN / SELECT withdrawal FOR UPDATE /
// UPDATE withdrawal SET status / COMMIT，不允许出现 SELECT/UPDATE `user` 或
// INSERT system_logs。
func TestCancelWithdrawal_DoesNotRefundCommission(t *testing.T) {
	const (
		withdrawalID = int64(987654)
		userID       = int64(42)
		amount       = int64(2500)
	)

	db, mock, cleanup := newCancelWithdrawalTestDB(t)
	defer cleanup()

	mock.ExpectBegin()
	mock.ExpectQuery("FROM `withdrawals`").
		WithArgs(withdrawalID, 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "amount", "status"}).
			AddRow(withdrawalID, userID, amount, usermodel.WithdrawalStatusPending))
	mock.ExpectExec("UPDATE `withdrawals` SET").
		WithArgs(usermodel.WithdrawalStatusCancelled, sqlmock.AnyArg(), withdrawalID, usermodel.WithdrawalStatusPending).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	logic := newTestCancelWithdrawalLogic(t, db, userID)
	resp, err := logic.CancelWithdrawal(&types.CancelWithdrawalRequest{WithdrawalId: withdrawalID})
	if err != nil {
		t.Fatalf("CancelWithdrawal unexpected error: %v", err)
	}
	if resp == nil || resp.Id != withdrawalID {
		t.Fatalf("CancelWithdrawal response = %+v, want id=%d", resp, withdrawalID)
	}
	if resp.Status != usermodel.WithdrawalStatusCancelled {
		t.Fatalf("CancelWithdrawal status = %d, want %d", resp.Status, usermodel.WithdrawalStatusCancelled)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

// TestCancelWithdrawal_RejectsNonPending 覆盖：状态非 pending（已批准/拒绝/已撤销）
// 时撤销必须短路返回 WithdrawalStatusInvalid，且不得写任何状态或佣金。
func TestCancelWithdrawal_RejectsNonPending(t *testing.T) {
	const (
		withdrawalID = int64(55555)
		userID       = int64(42)
	)

	cases := []struct {
		name   string
		status uint8
	}{
		{"already approved", usermodel.WithdrawalStatusApproved},
		{"already rejected", usermodel.WithdrawalStatusRejected},
		{"already cancelled", usermodel.WithdrawalStatusCancelled},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			db, mock, cleanup := newCancelWithdrawalTestDB(t)
			defer cleanup()

			mock.ExpectBegin()
			mock.ExpectQuery("FROM `withdrawals`").
				WithArgs(withdrawalID, 1).
				WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "amount", "status"}).
					AddRow(withdrawalID, userID, int64(2500), tc.status))
			mock.ExpectRollback()

			logic := newTestCancelWithdrawalLogic(t, db, userID)
			_, err := logic.CancelWithdrawal(&types.CancelWithdrawalRequest{WithdrawalId: withdrawalID})
			if !isCancelErrCode(err, xerr.WithdrawalStatusInvalid) {
				t.Fatalf("CancelWithdrawal err = %v, want WithdrawalStatusInvalid", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("unmet sql expectations: %v", err)
			}
		})
	}
}

// TestCancelWithdrawal_RejectsOtherUserWithdrawal 覆盖：当前登录用户尝试撤销
// 不属于自己的 pending 提现，必须返回 PermissionDenied 且不得写库。
func TestCancelWithdrawal_RejectsOtherUserWithdrawal(t *testing.T) {
	const (
		withdrawalID = int64(33333)
		ownerUserID  = int64(99)
		attackerID   = int64(42)
	)

	db, mock, cleanup := newCancelWithdrawalTestDB(t)
	defer cleanup()

	mock.ExpectBegin()
	mock.ExpectQuery("FROM `withdrawals`").
		WithArgs(withdrawalID, 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "amount", "status"}).
			AddRow(withdrawalID, ownerUserID, int64(2500), usermodel.WithdrawalStatusPending))
	mock.ExpectRollback()

	logic := newTestCancelWithdrawalLogic(t, db, attackerID)
	_, err := logic.CancelWithdrawal(&types.CancelWithdrawalRequest{WithdrawalId: withdrawalID})
	if !isCancelErrCode(err, xerr.PermissionDenied) {
		t.Fatalf("CancelWithdrawal err = %v, want PermissionDenied", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

// TestCancelWithdrawal_ConcurrentRaceLosesViaFORUPDATE 模拟撤销与审批并发的场景：
// 第二个 cancel 在 LoadPendingWithdrawalForUpdate 取到行时，状态已被先到的 approve
// 改成 1（FOR UPDATE 行锁让出后看到的最新状态），cancel 应当短路返回错误，
// 严禁继续往下写 status 或动 commission。
func TestCancelWithdrawal_ConcurrentRaceLosesViaFORUPDATE(t *testing.T) {
	const (
		withdrawalID = int64(77777)
		userID       = int64(42)
	)

	db, mock, cleanup := newCancelWithdrawalTestDB(t)
	defer cleanup()

	mock.ExpectBegin()
	mock.ExpectQuery("FROM `withdrawals`").
		WithArgs(withdrawalID, 1).
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "amount", "status"}).
			AddRow(withdrawalID, userID, int64(2500), usermodel.WithdrawalStatusApproved))
	mock.ExpectRollback()

	logic := newTestCancelWithdrawalLogic(t, db, userID)
	_, err := logic.CancelWithdrawal(&types.CancelWithdrawalRequest{WithdrawalId: withdrawalID})
	if !isCancelErrCode(err, xerr.WithdrawalStatusInvalid) {
		t.Fatalf("CancelWithdrawal err = %v, want WithdrawalStatusInvalid (loser of race)", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func newCancelWithdrawalTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
	t.Helper()

	sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
		if strings.Contains(actualSQL, expectedSQL) {
			return nil
		}
		return fmt.Errorf("actual sql %q does not contain %q", actualSQL, expectedSQL)
	})))
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}

	db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		_ = sqlDB.Close()
		t.Fatalf("open gorm db: %v", err)
	}

	return db, mock, func() {
		_ = sqlDB.Close()
	}
}

func newTestCancelWithdrawalLogic(t *testing.T, db *gorm.DB, userID int64) *CancelWithdrawalLogic {
	t.Helper()
	ctx := context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID})
	return &CancelWithdrawalLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{
			DB:        db,
			UserModel: stubUserModelForCancel{},
		},
	}
}

// stubUserModelForCancel 是 user.Model 的零依赖替身，仅覆盖 CancelWithdrawal 必须
// 调用的 ClearUserCache。其它方法被调用会导致 nil-interface panic，正好可以暴露
// 测试边界外的意外依赖。
type stubUserModelForCancel struct {
	usermodel.Model
}

func (stubUserModelForCancel) ClearUserCache(_ context.Context, _ ...*usermodel.User) error {
	return nil
}

func cancelErrCodeOf(err error) uint32 {
	if err == nil {
		return 0
	}
	type coder interface {
		GetErrCode() uint32
	}
	cause := errors.Cause(err)
	if c, ok := cause.(coder); ok {
		return c.GetErrCode()
	}
	return 0
}

func isCancelErrCode(err error, code uint32) bool {
	return cancelErrCodeOf(err) == code
}
