package financewithdrawal

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

func TestCreateApplicationRequiresFreezeCommandID(t *testing.T) {
	command := normalizeCreateApplicationCommand(CreateApplicationCommand{
		AppCode:             "huwaa",
		UserID:              42001,
		SalaryAssetType:     "point",
		WithdrawAmount:      "9.50",
		WithdrawAmountMinor: 1_000_000,
		WithdrawAddress:     "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
		FreezeTransactionID: "tx-freeze",
		CreatedAtMS:         1700000000000,
	})
	if err := validateCreateApplicationCommand(command); err == nil || !strings.Contains(err.Error(), "freeze_command_id") {
		t.Fatalf("missing freeze command id should be rejected, got %v", err)
	}
}

func TestCreateApplicationReturnsExistingByAppAndFreezeCommand(t *testing.T) {
	db, mock, closeDB := newWriterSQLMock(t)
	defer closeDB()
	writer := &MySQLWriter{db: db}

	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-freeze").
		WillReturnRows(withdrawalApplicationRows().AddRow(
			int64(77),
			"huwaa",
			"42001",
			"POINT",
			"9.50",
			int64(1000000),
			int64(50000),
			int64(950000),
			int64(100000),
			int32(500),
			"huwaa-policy-v1",
			MethodUSDTTRC20,
			"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
			"cmd-point-freeze",
			"tx-freeze",
			StatusPending,
			"skipped",
			int64(1700000000000),
			int64(1700000000000),
		))

	got, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
		AppCode:             "huwaa",
		UserID:              42001,
		SalaryAssetType:     "POINT",
		WithdrawAmount:      "9.50",
		WithdrawAmountMinor: 1_000_000,
		PointFeeAmount:      50_000,
		PointNetAmount:      950_000,
		PointsPerUSD:        100_000,
		PointFeeBPS:         500,
		PointPolicyInstance: "huwaa-policy-v1",
		WithdrawMethod:      MethodUSDTTRC20,
		WithdrawAddress:     "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
		FreezeCommandID:     "cmd-point-freeze",
		FreezeTransactionID: "tx-new-should-not-insert",
		CreatedAtMS:         1700000009999,
	})
	if err != nil {
		t.Fatalf("CreateApplication existing lookup failed: %v", err)
	}
	if got.ID != 77 || got.AppCode != "huwaa" || got.UserID != 42001 || got.FreezeTransactionID != "tx-freeze" || got.OperationsStatus != "skipped" {
		t.Fatalf("existing withdrawal application mismatch: %+v", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestCreateApplicationInsertsAfterAppScopedMiss(t *testing.T) {
	db, mock, closeDB := newWriterSQLMock(t)
	defer closeDB()
	writer := &MySQLWriter{db: db}

	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-freeze").
		WillReturnRows(withdrawalApplicationRows())
	mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
		WithArgs(
			"huwaa",
			"hyapp_wallet",
			"cmd-point-freeze",
			"42001",
			"POINT",
			"9.50",
			int64(1000000),
			int64(50000),
			int64(950000),
			int64(100000),
			int32(500),
			"huwaa-policy-v1",
			MethodUSDTTRC20,
			"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
			"cmd-point-freeze",
			"tx-freeze",
			StatusPending,
			OperationsStatusPending,
			int64(1700000000000),
			int64(1700000000000),
		).
		WillReturnResult(sqlmock.NewResult(88, 1))

	got, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
		AppCode:             "huwaa",
		UserID:              42001,
		SalaryAssetType:     "point",
		WithdrawAmount:      "9.50",
		WithdrawAmountMinor: 1_000_000,
		PointFeeAmount:      50_000,
		PointNetAmount:      950_000,
		PointsPerUSD:        100_000,
		PointFeeBPS:         500,
		PointPolicyInstance: "huwaa-policy-v1",
		WithdrawAddress:     "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
		FreezeCommandID:     "cmd-point-freeze",
		FreezeTransactionID: "tx-freeze",
		CreatedAtMS:         1700000000000,
	})
	if err != nil {
		t.Fatalf("CreateApplication insert failed: %v", err)
	}
	if got.ID != 88 || got.AppCode != "huwaa" || got.SalaryAssetType != "POINT" || got.WithdrawMethod != MethodUSDTTRC20 || got.Status != StatusPending || got.OperationsStatus != OperationsStatusPending {
		t.Fatalf("inserted withdrawal application mismatch: %+v", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestCreateApplicationReconcilesAnyInsertErrorByFreezeCommand(t *testing.T) {
	db, mock, closeDB := newWriterSQLMock(t)
	defer closeDB()
	writer := &MySQLWriter{db: db}

	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-uncertain").
		WillReturnRows(withdrawalApplicationRows())
	// 非重复键错误同样可能发生在 INSERT 已提交但响应丢失之后，不能据此直接释放钱包冻结。
	mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
		WillReturnError(errors.New("insert result uncertain"))
	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-uncertain").
		WillReturnRows(withdrawalApplicationRows().AddRow(
			int64(91),
			"huwaa",
			"42001",
			"POINT",
			"9.50",
			int64(1000000),
			int64(50000),
			int64(950000),
			int64(100000),
			int32(500),
			"huwaa-policy-v1",
			MethodUSDTTRC20,
			"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
			"cmd-point-uncertain",
			"tx-freeze",
			StatusPending,
			OperationsStatusPending,
			int64(1700000000000),
			int64(1700000000000),
		))

	got, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
		AppCode:             "huwaa",
		UserID:              42001,
		SalaryAssetType:     "POINT",
		WithdrawAmount:      "9.50",
		WithdrawAmountMinor: 1_000_000,
		PointFeeAmount:      50_000,
		PointNetAmount:      950_000,
		PointsPerUSD:        100_000,
		PointFeeBPS:         500,
		PointPolicyInstance: "huwaa-policy-v1",
		WithdrawMethod:      MethodUSDTTRC20,
		WithdrawAddress:     "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
		FreezeCommandID:     "cmd-point-uncertain",
		FreezeTransactionID: "tx-freeze",
		CreatedAtMS:         1700000000000,
	})
	if err != nil {
		t.Fatalf("CreateApplication should absorb uncertain insert result after finding persisted row: %v", err)
	}
	if got.ID != 91 || got.FreezeCommandID != "cmd-point-uncertain" || got.FreezeTransactionID != "tx-freeze" {
		t.Fatalf("reconciled withdrawal application mismatch: %+v", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestCreateApplicationMarksOutcomeUnknownWhenInsertAndLookupBothFail(t *testing.T) {
	db, mock, closeDB := newWriterSQLMock(t)
	defer closeDB()
	writer := &MySQLWriter{db: db}

	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-unknown").
		WillReturnRows(withdrawalApplicationRows())
	mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
		WillReturnError(errors.New("insert result uncertain"))
	mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
		WithArgs("huwaa", "cmd-point-unknown").
		WillReturnError(errors.New("result lookup unavailable"))

	_, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
		AppCode:             "huwaa",
		UserID:              42001,
		SalaryAssetType:     "POINT",
		WithdrawAmount:      "9.50",
		WithdrawAmountMinor: 1_000_000,
		PointFeeAmount:      50_000,
		PointNetAmount:      950_000,
		PointsPerUSD:        100_000,
		PointFeeBPS:         500,
		PointPolicyInstance: "huwaa-policy-v1",
		WithdrawMethod:      MethodUSDTTRC20,
		WithdrawAddress:     "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
		FreezeCommandID:     "cmd-point-unknown",
		FreezeTransactionID: "tx-freeze",
		CreatedAtMS:         1700000000000,
	})
	if !IsApplicationOutcomeUnknown(err) {
		t.Fatalf("failed result confirmation must retain outcome-unknown classification, got %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func withdrawalApplicationRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"id",
		"app_code",
		"user_id",
		"salary_asset_type",
		"withdraw_amount",
		"withdraw_amount_minor",
		"point_fee_amount",
		"point_net_amount",
		"points_per_usd",
		"point_fee_bps",
		"point_policy_instance_code",
		"withdraw_method",
		"withdraw_address",
		"freeze_command_id",
		"freeze_transaction_id",
		"status",
		"operations_status",
		"created_at_ms",
		"updated_at_ms",
	})
}

func newWriterSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
	t.Helper()
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock failed: %v", err)
	}
	return db, mock, func() { _ = db.Close() }
}
