package user

import (
	"bytes"
	"context"
	"database/sql/driver"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	logmodel "github.com/perfect-panel/server/internal/model/log"
	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"
)

func TestQueryWithdrawalLog_WithWithdrawalBizType(t *testing.T) {
	const userID = int64(42)
	createdAt := time.Unix(1700000000, 0)
	updatedAt := createdAt.Add(time.Minute)

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

	mock.ExpectQuery("SELECT count(*) FROM `withdrawals` WHERE user_id = ?").
		WithArgs(userID).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
	mock.ExpectQuery("SELECT * FROM `withdrawals` WHERE user_id = ? ORDER BY id DESC LIMIT ?").
		WithArgs(userID, 10).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "user_id", "amount", "content", "status", "reason", "method", "account", "qr_code_url", "created_at", "updated_at",
		}).AddRow(
			int64(1001), userID, int64(3000), "bank withdrawal", usermodel.WithdrawalStatusPending, "", uint8(3), "acct-001", "", createdAt, updatedAt,
		))

	logic := newTestQueryWithdrawalLogLogic(t, db, userID)
	resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{
		BizType: withdrawalLogBizTypeWithdrawal,
		Page:    1,
		Size:    10,
	})
	if err != nil {
		t.Fatalf("QueryWithdrawalLog unexpected error: %v", err)
	}
	if resp.Total != 1 || len(resp.List) != 1 {
		t.Fatalf("QueryWithdrawalLog response = %+v, want one withdrawal", resp)
	}
	got := resp.List[0]
	if got.BizType != withdrawalLogBizTypeWithdrawal {
		t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeWithdrawal)
	}
	if got.Id != 1001 || got.UserId != userID || got.Amount != 3000 || got.CreatedAt != createdAt.UnixMilli() || got.UpdatedAt != updatedAt.UnixMilli() {
		t.Fatalf("withdrawal item = %+v", got)
	}
	assertQueryWithdrawalLogExpectations(t, mock)
}

func TestQueryCommissionReturnLog_HappyPathIncludes333337338(t *testing.T) {
	const userID = int64(42)
	createdAt := time.Unix(1700000000, 0)

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

	expectCommissionReturnQueries(mock, userID, 3, logmodel.CommissionTypeRefund, logmodel.CommissionTypeWithdrawReject, logmodel.CommissionTypeWithdrawCancel)
	mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?").
		WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10).
		WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}).
			AddRow(int64(2003), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":338,"amount":1500,"order_no":"ORDER-3","timestamp":1700000003123}`, createdAt).
			AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, createdAt).
			AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, createdAt))

	logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db})
	resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10})
	if err != nil {
		t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err)
	}
	if resp.Total != 3 || len(resp.List) != 3 {
		t.Fatalf("QueryCommissionReturnLog response = %+v, want three logs", resp)
	}

	eventTypes := []uint16{resp.List[0].EventType, resp.List[1].EventType, resp.List[2].EventType}
	wantTypes := []uint16{logmodel.CommissionTypeWithdrawCancel, logmodel.CommissionTypeWithdrawReject, logmodel.CommissionTypeRefund}
	if fmt.Sprint(eventTypes) != fmt.Sprint(wantTypes) {
		t.Fatalf("event types = %v, want %v", eventTypes, wantTypes)
	}

	assertQueryWithdrawalLogExpectations(t, mock)
}

func TestQueryWithdrawalLog_WithCommissionRefundBizTypeOnlyIncludes333(t *testing.T) {
	const userID = int64(42)
	createdAt := time.Unix(1700000000, 0)

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

	expectCommissionReturnQueries(mock, userID, 1, logmodel.CommissionTypeRefund)
	mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ?) ORDER BY id DESC LIMIT ?").
		WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", 10).
		WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}).
			AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, createdAt))

	logic := newTestQueryWithdrawalLogLogic(t, db, userID)
	resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{
		BizType: withdrawalLogBizTypeCommissionRefund,
		Page:    1,
		Size:    10,
	})
	if err != nil {
		t.Fatalf("QueryWithdrawalLog unexpected error: %v", err)
	}
	if resp.Total != 1 || len(resp.List) != 1 {
		t.Fatalf("QueryWithdrawalLog response = %+v, want one commission refund", resp)
	}
	for _, item := range resp.List {
		if item.BizType != withdrawalLogBizTypeCommissionRefund {
			t.Fatalf("BizType = %q, want %q", item.BizType, withdrawalLogBizTypeCommissionRefund)
		}
		if item.Status != 0 || item.Reason != "" || item.Method != 0 || item.Account != "" || item.QrCodeUrl != "" {
			t.Fatalf("withdrawal-only fields should keep zero values, got %+v", item)
		}
	}
	if resp.List[0].Id != 2001 {
		t.Fatalf("commission refund item id = %d, want 2001", resp.List[0].Id)
	}
	assertQueryWithdrawalLogExpectations(t, mock)
}

func TestQueryCommissionReturnLog_SkipsInvalidJSONAndLogsWarn(t *testing.T) {
	const userID = int64(42)
	createdAt := time.Unix(1700000000, 0)

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

	expectCommissionReturnQueries(mock, userID, 2, logmodel.CommissionTypeRefund, logmodel.CommissionTypeWithdrawReject, logmodel.CommissionTypeWithdrawCancel)
	mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?").
		WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10).
		WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}).
			AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, createdAt).
			AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333`, createdAt))

	var buf bytes.Buffer
	restoreLogger := captureTestLogs(&buf)
	defer restoreLogger()

	logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db})
	resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10})
	if err != nil {
		t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err)
	}
	if resp.Total != 2 || len(resp.List) != 1 {
		t.Fatalf("QueryCommissionReturnLog response = %+v, want total=2 and one valid row", resp)
	}
	if !strings.Contains(buf.String(), "unmarshal commission return log content failed") {
		t.Fatalf("expected warn log, got %q", buf.String())
	}
	assertQueryWithdrawalLogExpectations(t, mock)
}

func TestQueryCommissionReturnLog_FiltersOtherUsersByObjectID(t *testing.T) {
	const userID = int64(42)

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

	expectCommissionReturnQueries(mock, userID, 0, logmodel.CommissionTypeRefund, logmodel.CommissionTypeWithdrawReject, logmodel.CommissionTypeWithdrawCancel)
	mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?").
		WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10).
		WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}))

	logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db})
	resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10})
	if err != nil {
		t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err)
	}
	if resp.Total != 0 || len(resp.List) != 0 {
		t.Fatalf("QueryCommissionReturnLog response = %+v, want no rows for filtered user", resp)
	}
	assertQueryWithdrawalLogExpectations(t, mock)
}

func TestQueryWithdrawalLog_RejectsInvalidBizType(t *testing.T) {
	db, mock, cleanup := newQueryWithdrawalLogTestDB(t)
	defer cleanup()

	logic := newTestQueryWithdrawalLogLogic(t, db, 42)
	_, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{
		BizType: "all",
		Page:    1,
		Size:    10,
	})
	if !isQueryWithdrawalLogErrCode(err, xerr.InvalidParams) {
		t.Fatalf("QueryWithdrawalLog err = %v, want InvalidParams", err)
	}
	assertQueryWithdrawalLogExpectations(t, mock)
}

func newQueryWithdrawalLogTestDB(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 newTestQueryWithdrawalLogLogic(t *testing.T, db *gorm.DB, userID int64) *QueryWithdrawalLogLogic {
	t.Helper()
	ctx := newTestQueryCtx(userID)
	return &QueryWithdrawalLogLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{
			DB: db,
		},
	}
}

func newTestQueryCtx(userID int64) context.Context {
	return context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID})
}

func expectCommissionReturnQueries(mock sqlmock.Sqlmock, userID int64, total int64, eventTypes ...uint16) {
	query := "SELECT count(*) FROM `system_logs` WHERE `type` = ? AND object_id = ?"
	args := []driver.Value{logmodel.TypeCommission.Uint8(), userID}
	if len(eventTypes) > 0 {
		clauses := make([]string, 0, len(eventTypes))
		for _, eventType := range eventTypes {
			clauses = append(clauses, "`content` LIKE ?")
			args = append(args, fmt.Sprintf("%%\"type\":%d%%", eventType))
		}
		query += " AND (" + strings.Join(clauses, " OR ") + ")"
	}
	mock.ExpectQuery(query).
		WithArgs(args...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total))
}

func assertQueryWithdrawalLogExpectations(t *testing.T, mock sqlmock.Sqlmock) {
	t.Helper()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func captureTestLogs(buf *bytes.Buffer) func() {
	prevWriter := logger.Reset()
	prevLevel := logger.InfoLevel
	logger.SetLevel(logger.DebugLevel)
	logger.SetWriter(logger.NewWriter(buf))
	return func() {
		logger.Reset()
		logger.SetLevel(prevLevel)
		if prevWriter != nil {
			logger.SetWriter(prevWriter)
		}
	}
}

func queryWithdrawalLogErrCodeOf(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 isQueryWithdrawalLogErrCode(err error, code uint32) bool {
	return queryWithdrawalLogErrCodeOf(err) == code
}
