package repository

import (
	"testing"

	"hyapp-admin-server/internal/model"

	"github.com/DATA-DOG/go-sqlmock"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestReplaceAndListUserMoneyScopes(t *testing.T) {
	store, mock, closeStore := newRepositorySQLMock(t)
	defer closeStore()

	mock.ExpectBegin()
	mock.ExpectQuery("SELECT \\* FROM `admin_users` WHERE `admin_users`.`id` = \\? ORDER BY `admin_users`.`id` LIMIT \\?").
		WillReturnRows(sqlmock.NewRows([]string{"id", "username", "name", "status"}).AddRow(7, "ops", "Ops", "active"))
	mock.ExpectExec("DELETE FROM `admin_user_money_scopes` WHERE user_id = \\?").
		WillReturnResult(sqlmock.NewResult(0, 2))
	mock.ExpectExec("INSERT INTO `admin_user_money_scopes`").
		WillReturnResult(sqlmock.NewResult(1, 2))
	mock.ExpectCommit()

	if err := store.ReplaceUserMoneyScopes(7, []model.UserMoneyScope{
		{AppCode: " LALU ", RegionID: 2},
		{AppCode: "lalu", RegionID: 2},
		{AppCode: "yumi", RegionID: 0},
	}); err != nil {
		t.Fatalf("replace money scopes failed: %v", err)
	}

	mock.ExpectQuery("SELECT \\* FROM `admin_user_money_scopes` WHERE user_id = \\? ORDER BY app_code ASC, region_id ASC").
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "app_code", "region_id", "created_at_ms", "updated_at_ms"}).
			AddRow(1, 7, "lalu", 2, int64(1700000000000), int64(1700000000000)).
			AddRow(2, 7, "yumi", 0, int64(1700000000000), int64(1700000000000)))

	scopes, err := store.ListUserMoneyScopes(7)
	if err != nil {
		t.Fatalf("list money scopes failed: %v", err)
	}
	if len(scopes) != 2 || scopes[0].AppCode != "lalu" || scopes[0].RegionID != 2 || scopes[1].AppCode != "yumi" || scopes[1].RegionID != 0 {
		t.Fatalf("money scopes mismatch: %+v", scopes)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestReplaceUserMoneyScopesRejectsInvalidScope(t *testing.T) {
	store, mock, closeStore := newRepositorySQLMock(t)
	defer closeStore()

	if err := store.ReplaceUserMoneyScopes(7, []model.UserMoneyScope{{RegionID: 2}}); err == nil {
		t.Fatal("expected invalid scope error")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestListAllUserMoneyScopes(t *testing.T) {
	store, mock, closeStore := newRepositorySQLMock(t)
	defer closeStore()

	mock.ExpectQuery("SELECT \\* FROM `admin_user_money_scopes` ORDER BY app_code ASC, region_id ASC, user_id ASC").
		WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "app_code", "region_id", "created_at_ms", "updated_at_ms"}).
			AddRow(1, 7, "aslan", 2, int64(1700000000000), int64(1700000000000)).
			AddRow(2, 9, "aslan", 5, int64(1700000000000), int64(1700000000000)).
			AddRow(3, 9, "lalu", 0, int64(1700000000000), int64(1700000000000)))

	scopes, err := store.ListAllUserMoneyScopes()
	if err != nil {
		t.Fatalf("list all money scopes failed: %v", err)
	}
	if len(scopes) != 3 || scopes[0].UserID != 7 || scopes[1].UserID != 9 || scopes[2].AppCode != "lalu" || scopes[2].RegionID != 0 {
		t.Fatalf("all money scopes mismatch: %+v", scopes)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func newRepositorySQLMock(t *testing.T) (*Store, sqlmock.Sqlmock, func()) {
	t.Helper()
	sqlDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock failed: %v", err)
	}
	gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		_ = sqlDB.Close()
		t.Fatalf("create gorm db failed: %v", err)
	}
	return New(gormDB), mock, func() {
		_ = sqlDB.Close()
	}
}

func TestRegionIDLooseEqual(t *testing.T) {
	exact := int64(2049039732077690882)
	rounded := int64(float64(exact))
	if !RegionIDLooseEqual(exact, rounded) || !RegionIDLooseEqual(rounded, exact) {
		t.Fatalf("expected rounded/exact pair to match")
	}
	// 浏览器实际写回的是 JS Number 的最短十进制表示（…691000），与 float64 回转值（…690880）不同，
	// 必须在 float64 层等价（线上「区域 2049039732077691000」事故的回归测试）。
	jsShortest := int64(2049039732077691000)
	if jsShortest == rounded {
		t.Fatalf("fixture should differ from cast value")
	}
	if !RegionIDLooseEqual(exact, jsShortest) || !RegionIDLooseEqual(jsShortest, exact) {
		t.Fatalf("expected js shortest-repr id to match exact id")
	}
	if !RegionIDLooseEqual(7, 7) {
		t.Fatalf("expected exact small ids to match")
	}
	if RegionIDLooseEqual(7, 8) {
		t.Fatalf("small ids must not loosely match")
	}
	if RegionIDLooseEqual(0, rounded) || RegionIDLooseEqual(exact, 0) {
		t.Fatalf("zero must never loosely match")
	}
}

func TestMoneyAccessAllowsRoundedRegion(t *testing.T) {
	exact := int64(2049039732077690882)
	rounded := int64(float64(exact))
	access := MoneyAccess{UserID: 1, Scopes: []model.UserMoneyScope{{UserID: 1, AppCode: "aslan", RegionID: rounded}}}
	if !access.Allows("aslan", exact) {
		t.Fatalf("rounded scope should allow exact region id")
	}
	if access.Allows("aslan", exact+999) {
		t.Fatalf("unrelated region must stay denied")
	}
}

func TestMoneyAccessAllowsAppIgnoresRegionButKeepsAppBoundary(t *testing.T) {
	t.Parallel()
	access := MoneyAccess{UserID: 7, Scopes: []model.UserMoneyScope{
		{UserID: 7, AppCode: "lalu", RegionID: 9},
		{UserID: 7, AppCode: "fami", RegionID: 0},
	}}
	if !access.AllowsApp("LALU") || !access.AllowsApp("fami") || access.AllowsApp("huwaa") {
		t.Fatalf("unexpected app-level money access: %+v", access)
	}
	if got := access.AppCodes(); len(got) != 2 || got[0] != "fami" || got[1] != "lalu" {
		t.Fatalf("money scope app codes must be stable and sorted: %v", got)
	}
}
