package appconfig

import (
	"context"
	"regexp"
	"testing"
	"time"

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

func TestRoomDiscoveryAllRegionsCachesPerAppUntilTTL(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("new sql mock failed: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(regexp.QuoteMeta(getRoomDiscoveryScopeSQL)).
		WithArgs("wide-app", roomDiscoveryGroup, roomDiscoveryScopeKey).
		WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow(roomDiscoveryScopeAllActiveRegions))
	reader := &MySQLReader{
		db:                    db,
		roomDiscoveryCache:    make(map[string]roomDiscoveryCacheEntry),
		roomDiscoveryCacheTTL: time.Minute,
	}
	for attempt := 0; attempt < 2; attempt++ {
		allowed, err := reader.RoomDiscoveryAllRegions(context.Background(), " WIDE-APP ")
		if err != nil || !allowed {
			t.Fatalf("cached all-region scope attempt %d failed: allowed=%t err=%v", attempt, allowed, err)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("scope must query DB only once inside TTL: %v", err)
	}

	reader.roomDiscoveryMu.Lock()
	entry := reader.roomDiscoveryCache["wide-app"]
	entry.expiresAt = time.Now().Add(-time.Second)
	reader.roomDiscoveryCache["wide-app"] = entry
	reader.roomDiscoveryMu.Unlock()
	mock.ExpectQuery(regexp.QuoteMeta(getRoomDiscoveryScopeSQL)).
		WithArgs("wide-app", roomDiscoveryGroup, roomDiscoveryScopeKey).
		WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow("viewer_region"))
	allowed, err := reader.RoomDiscoveryAllRegions(context.Background(), "wide-app")
	if err != nil || allowed {
		t.Fatalf("expired scope did not refresh to viewer-region: allowed=%t err=%v", allowed, err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("expired scope refresh mismatch: %v", err)
	}
}

func TestRuntimeCapabilitiesReadBothKeysOnceAndCachePerApp(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("new sql mock failed: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(regexp.QuoteMeta(getRuntimeCapabilitiesSQL)).
		WithArgs("tenant-a", runtimeCapabilitiesGroup, activityLeaderboardScopeKey, resourceEquipCommandIDPolicyKey).
		WillReturnRows(sqlmock.NewRows([]string{"key", "value"}).
			AddRow(activityLeaderboardScopeKey, ActivityLeaderboardScopeViewerRegion).
			AddRow(resourceEquipCommandIDPolicyKey, ResourceEquipCommandIDPolicyRequired))
	reader := &MySQLReader{
		db:                       db,
		runtimeCapabilitiesCache: make(map[string]runtimeCapabilitiesCacheEntry),
	}
	for attempt := 0; attempt < 2; attempt++ {
		capabilities, err := reader.GetRuntimeCapabilities(context.Background(), " TENANT-A ")
		if err != nil || capabilities.ActivityLeaderboardScope != ActivityLeaderboardScopeViewerRegion || capabilities.ResourceEquipCommandIDPolicy != ResourceEquipCommandIDPolicyRequired {
			t.Fatalf("runtime capabilities attempt %d mismatch: config=%+v err=%v", attempt, capabilities, err)
		}
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("runtime capabilities must query DB once inside TTL: %v", err)
	}
}

func TestRuntimeCapabilitiesMissingRowsUseGenericDefaults(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("new sql mock failed: %v", err)
	}
	defer db.Close()
	mock.ExpectQuery(regexp.QuoteMeta(getRuntimeCapabilitiesSQL)).
		WithArgs("future-app", runtimeCapabilitiesGroup, activityLeaderboardScopeKey, resourceEquipCommandIDPolicyKey).
		WillReturnRows(sqlmock.NewRows([]string{"key", "value"}))

	reader := &MySQLReader{db: db}
	capabilities, err := reader.GetRuntimeCapabilities(context.Background(), "future-app")
	if err != nil {
		t.Fatalf("missing runtime capabilities failed: %v", err)
	}
	if capabilities.ActivityLeaderboardScope != ActivityLeaderboardScopeAppGlobal || capabilities.ResourceEquipCommandIDPolicy != ResourceEquipCommandIDPolicyLegacyOptional {
		t.Fatalf("missing runtime capability defaults mismatch: %+v", capabilities)
	}
}

func TestListH5LinksReadsOnlyRequestedApp(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("new sql mock failed: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(regexp.QuoteMeta(listH5LinksSQL)).
		WithArgs("yumi", h5LinkGroup).
		WillReturnRows(sqlmock.NewRows([]string{"key", "description", "value", "updated_at_ms"}).
			AddRow("admin", "Yumi Admin", "https://yumi.example.com/admin", int64(200)).
			AddRow("public", "", "https://yumi.example.com/public", int64(300)))

	reader := &MySQLReader{db: db}
	items, err := reader.ListH5Links(context.Background(), H5LinkQuery{AppCode: " YUMI "})
	if err != nil {
		t.Fatalf("list scoped h5 links failed: %v", err)
	}
	if len(items) != 2 {
		t.Fatalf("unexpected merged h5 links: %+v", items)
	}
	if items[0].Key != "admin" || items[0].Label != "Yumi Admin" || items[0].URL != "https://yumi.example.com/admin" {
		t.Fatalf("scoped override did not replace baseline: %+v", items[0])
	}
	if items[1].Key != "public" || items[1].Label != "public" || items[1].URL != "https://yumi.example.com/public" {
		t.Fatalf("requested app config mismatch: %+v", items[1])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestNormalizeBannerDisplayScopeAcceptsChineseLabels(t *testing.T) {
	cases := map[string]string{
		"":     "home",
		"首页":   "home",
		"房间内":  "room",
		"充值页":  "recharge",
		"我的页":  "me",
		"ME":   "me",
		"ROOM": "room",
		"bad":  "",
	}
	for input, want := range cases {
		if got := NormalizeBannerDisplayScope(input); got != want {
			t.Fatalf("scope %q mismatch: got %q want %q", input, got, want)
		}
	}
}

func TestBannerDisplayScopeListParsesMultiScopeValue(t *testing.T) {
	got := bannerDisplayScopeList("room,home,recharge,me")
	want := []string{"home", "room", "recharge", "me"}
	if len(got) != len(want) {
		t.Fatalf("scope count mismatch: got %+v want %+v", got, want)
	}
	for index := range want {
		if got[index] != want[index] {
			t.Fatalf("scope %d mismatch: got %+v want %+v", index, got, want)
		}
	}
}

func TestParseRoomRegionWhitelistSupportsObjectAndArrayPayloads(t *testing.T) {
	cases := []string{
		`{"user_ids":["325379237278126080","1001","1001","0","bad"]}`,
		`["325379237278126080","1001","1001","0","bad"]`,
	}
	for _, input := range cases {
		got, err := parseRoomRegionWhitelist(input)
		if err != nil {
			t.Fatalf("parse whitelist %s failed: %v", input, err)
		}
		if _, ok := got[325379237278126080]; !ok {
			t.Fatalf("long user id missing: %+v", got)
		}
		if _, ok := got[1001]; !ok {
			t.Fatalf("small user id missing: %+v", got)
		}
		if _, ok := got[0]; ok {
			t.Fatalf("zero user id must be ignored: %+v", got)
		}
	}
}
