package roomadmin

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/robotclient"
	"hyapp-admin-server/internal/integration/roomclient"
)

func TestNormalizeRoomListSortKeepsOnlyContributionSort(t *testing.T) {
	tests := []struct {
		name      string
		query     listQuery
		wantOrder string
	}{
		{
			name:      "default",
			query:     listQuery{},
			wantOrder: "rle.created_at_ms DESC, rle.room_id DESC",
		},
		{
			name:      "contribution_desc",
			query:     listQuery{SortBy: "room_contribution", SortDirection: "desc"},
			wantOrder: "rle.heat DESC, rle.created_at_ms DESC, rle.room_id DESC",
		},
		{
			name:      "contribution_asc",
			query:     listQuery{SortBy: "heat", SortDirection: "asc"},
			wantOrder: "rle.heat ASC, rle.created_at_ms DESC, rle.room_id DESC",
		},
		{
			name:      "unknown_falls_back",
			query:     listQuery{SortBy: "owner_user_id", SortDirection: "asc"},
			wantOrder: "rle.created_at_ms DESC, rle.room_id DESC",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			query := normalizeListQuery(test.query)
			if got := roomListOrderBy(query); got != test.wantOrder {
				t.Fatalf("order by mismatch: got %q want %q", got, test.wantOrder)
			}
		})
	}
}

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

	const keyword = "VIP2026"
	const ownerUserID = int64(327702592329093120)
	expectRoomOwnerIdentityLookup(sqlMock, keyword, ownerUserID)

	roomClient := &recordingRoomClient{}
	service := NewService(db, nil, roomClient, nil)
	_, _, err = service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
		OwnerUserKeyword: keyword,
		Page:             1,
		PageSize:         20,
	})
	if err != nil {
		t.Fatalf("list rooms failed: %v", err)
	}
	if len(roomClient.requests) != 1 {
		t.Fatalf("expected one room-service request, got %+v", roomClient.requests)
	}
	if got := roomClient.requests[0].OwnerUserID; got != ownerUserID {
		t.Fatalf("owner user id should be resolved before room-service call: got %d want %d", got, ownerUserID)
	}
	if roomClient.requests[0].Keyword != "" {
		t.Fatalf("owner keyword must not be forwarded as room keyword: %+v", roomClient.requests[0])
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

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

	const keyword = "VIP2026"
	const ownerUserID = int64(327702592329093120)
	expectRoomOwnerNumberLookup(sqlMock, keyword, ownerUserID)

	roomClient := &recordingRoomClient{}
	service := NewService(db, nil, roomClient, nil)
	_, _, err = service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
		Keyword:  keyword,
		Page:     1,
		PageSize: 20,
	})
	if err != nil {
		t.Fatalf("list rooms failed: %v", err)
	}
	if len(roomClient.requests) != 1 {
		t.Fatalf("expected one room-service request, got %+v", roomClient.requests)
	}
	if got := roomClient.requests[0].OwnerUserID; got != ownerUserID {
		t.Fatalf("room keyword should resolve owner user id before room-service call: got %d want %d", got, ownerUserID)
	}
	if roomClient.requests[0].Keyword != "" {
		t.Fatalf("owner pretty id must not be forwarded as room keyword: %+v", roomClient.requests[0])
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

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

	const keyword = "VIP404"
	expectRoomOwnerIdentityMiss(sqlMock, keyword)

	roomClient := &recordingRoomClient{}
	service := NewService(db, nil, roomClient, nil)
	items, total, err := service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
		OwnerUserKeyword: keyword,
		Page:             1,
		PageSize:         20,
	})
	if err != nil {
		t.Fatalf("list rooms failed: %v", err)
	}
	if len(items) != 0 || total != 0 {
		t.Fatalf("unmatched owner keyword should return empty page, got items=%+v total=%d", items, total)
	}
	if len(roomClient.requests) != 0 {
		t.Fatalf("room-service must not be called when owner keyword is known unmatched: %+v", roomClient.requests)
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestRobotRoomProfileFromOwnerUsesOwnerNameAndAvatar(t *testing.T) {
	name, avatar, err := robotRoomProfileFromOwner(RoomOwner{
		Username: "  Robot Host  ",
		Avatar:   "  https://cdn.example.com/robot.png  ",
	})
	if err != nil {
		t.Fatalf("robot room owner profile should be valid: %v", err)
	}
	if name != "Robot Host" || avatar != "https://cdn.example.com/robot.png" {
		t.Fatalf("owner profile mismatch: name=%q avatar=%q", name, avatar)
	}
}

func expectRoomOwnerIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
	expectRoomOwnerIdentityQuery(sqlMock, keyword).
		WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}

func expectRoomOwnerIdentityMiss(sqlMock sqlmock.Sqlmock, keyword string) {
	expectRoomOwnerIdentityQuery(sqlMock, keyword).
		WillReturnError(sql.ErrNoRows)
}

func expectRoomOwnerNumberLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
	expectRoomOwnerNumberQuery(sqlMock, keyword).
		WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}

func expectRoomOwnerIdentityQuery(sqlMock sqlmock.Sqlmock, keyword string) *sqlmock.ExpectedQuery {
	return sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
		WithArgs(
			"lalu",
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
			"%"+keyword+"%",
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
		)
}

func expectRoomOwnerNumberQuery(sqlMock sqlmock.Sqlmock, keyword string) *sqlmock.ExpectedQuery {
	return sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*CAST\(u\.user_id AS CHAR\) = \?.*LIMIT 1`).
		WithArgs(
			"lalu",
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
		)
}

type recordingRoomClient struct {
	roomclient.Client
	requests []roomclient.ListRoomsRequest
	result   *roomclient.ListRoomsResult
}

func (c *recordingRoomClient) ListRooms(_ context.Context, req roomclient.ListRoomsRequest) (*roomclient.ListRoomsResult, error) {
	c.requests = append(c.requests, req)
	if c.result != nil {
		return c.result, nil
	}
	return &roomclient.ListRoomsResult{}, nil
}

func TestRobotRoomProfileFromOwnerRequiresNameAndAvatar(t *testing.T) {
	tests := []struct {
		name  string
		owner RoomOwner
	}{
		{name: "missing_name", owner: RoomOwner{Avatar: "https://cdn.example.com/robot.png"}},
		{name: "missing_avatar", owner: RoomOwner{Username: "Robot Host"}},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, _, err := robotRoomProfileFromOwner(test.owner)
			if !errors.Is(err, ErrInvalidArgument) {
				t.Fatalf("expected ErrInvalidArgument, got %v", err)
			}
		})
	}
}

func TestApplyOwnerProfileToRobotRoomRequestFollowsOwnerRegionAndCountry(t *testing.T) {
	req := createRobotRoomRequest{
		RoomName:         "manual name",
		RoomAvatar:       "https://cdn.example.com/manual.png",
		VisibleRegionID:  999,
		OwnerCountryCode: "XX",
	}
	err := applyOwnerProfileToRobotRoomRequest(&req, RoomOwner{
		Username:        "  Robot Owner  ",
		Avatar:          "  https://cdn.example.com/owner.png  ",
		CountryCode:     " ae ",
		VisibleRegionID: 686,
	})
	if err != nil {
		t.Fatalf("apply owner profile failed: %v", err)
	}
	if req.RoomName != "Robot Owner" || req.RoomAvatar != "https://cdn.example.com/owner.png" {
		t.Fatalf("owner display profile mismatch: name=%q avatar=%q", req.RoomName, req.RoomAvatar)
	}
	if req.VisibleRegionID != 686 || req.OwnerCountryCode != "AE" {
		t.Fatalf("owner region or country mismatch: region=%d country=%q", req.VisibleRegionID, req.OwnerCountryCode)
	}
}

func TestCreateRobotRoomRequestAcceptsStringInt64IDs(t *testing.T) {
	var req createRobotRoomRequest
	if err := json.Unmarshal([]byte(`{
		"ownerRobotUserId":"325379237278126080",
		"candidateRobotUserIds":["325379237278126080","325455441691676672","325455441691676672"],
		"minRobotCount":2,
		"maxRobotCount":8,
		"giftIds":["84"],
		"luckyGiftIds":["28"],
		"normalGiftIntervalMs":10000,
		"luckyComboMin":100,
		"luckyComboMax":10000,
		"luckyPauseMinMs":5000,
		"luckyPauseMaxMs":20000
	}`), &req); err != nil {
		t.Fatalf("robot room request should accept string int64 ids: %v", err)
	}

	normalized, err := normalizeCreateRobotRoomRequest(req)
	if err != nil {
		t.Fatalf("robot room request should normalize: %v", err)
	}
	if got := int64(normalized.OwnerRobotUserID); got != 325379237278126080 {
		t.Fatalf("owner id mismatch: got %d", got)
	}
	if got := []int64(normalized.CandidateRobotUserIDs); len(got) != 1 || got[0] != 325455441691676672 {
		t.Fatalf("candidate ids should drop owner and duplicates, got %+v", got)
	}
}

func TestCreateRobotRoomRequestBindsStringInt64IDsFromHTTP(t *testing.T) {
	gin.SetMode(gin.TestMode)
	body := `{
		"ownerRobotUserId":"325379237278126080",
		"candidateRobotUserIds":["325455441691676672"],
		"minRobotCount":2,
		"maxRobotCount":8,
		"giftIds":["84"],
		"luckyGiftIds":["28"],
		"normalGiftIntervalMs":10000,
		"luckyComboMin":100,
		"luckyComboMax":10000,
		"luckyPauseMinMs":5000,
		"luckyPauseMaxMs":20000
	}`
	req := httptest.NewRequest(http.MethodPost, "/admin/rooms/robot-rooms", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(rec)
	ctx.Request = req

	var payload createRobotRoomRequest
	if err := ctx.ShouldBindJSON(&payload); err != nil {
		t.Fatalf("gin binding should accept string int64 ids: %v", err)
	}
	if got := int64(payload.OwnerRobotUserID); got != 325379237278126080 {
		t.Fatalf("owner id mismatch: got %d", got)
	}
	if got := []int64(payload.CandidateRobotUserIDs); len(got) != 1 || got[0] != 325455441691676672 {
		t.Fatalf("candidate ids mismatch: %+v", got)
	}
}

func TestNormalizeRoomWhitelistUserIDsKeepsStringInt64Order(t *testing.T) {
	got, err := normalizeRoomWhitelistUserIDs([]string{"325379237278126080", "1001", "1001"})
	if err != nil {
		t.Fatalf("normalize room whitelist failed: %v", err)
	}
	want := []string{"1001", "325379237278126080"}
	if len(got) != len(want) {
		t.Fatalf("whitelist length mismatch: got %+v want %+v", got, want)
	}
	for index := range want {
		if got[index] != want[index] {
			t.Fatalf("whitelist id %d mismatch: got %+v want %+v", index, got, want)
		}
	}
}

func TestNormalizeRoomWhitelistUserIDsRejectsInvalidID(t *testing.T) {
	if _, err := normalizeRoomWhitelistUserIDs([]string{"1.2"}); !errors.Is(err, ErrInvalidArgument) {
		t.Fatalf("invalid id should return ErrInvalidArgument, got %v", err)
	}
}

func TestAvailableRoomRobotsFromGamePoolKeepsOnlyRoomAvailableUsers(t *testing.T) {
	robots := []robotclient.Robot{
		{UserID: 101, Status: "active", LastUsedAtMS: 1000, UsedCount: 2},
		{UserID: 102, Status: "active", LastUsedAtMS: 2000, UsedCount: 3},
		{UserID: 103, Status: "active", LastUsedAtMS: 3000, UsedCount: 4},
	}
	owners := map[int64]RoomOwner{
		101: {UserID: "101", Username: "Robot A"},
		103: {UserID: "103", Username: "Robot C"},
	}

	items := availableRoomRobotsFromGamePool(robots, []int64{103, 101}, owners)
	if len(items) != 2 {
		t.Fatalf("available robots length mismatch: got %d want 2", len(items))
	}
	if items[0].UserID != "101" || items[0].User.Username != "Robot A" || items[0].UsedCount != 2 {
		t.Fatalf("first available robot mismatch: %+v", items[0])
	}
	if items[1].UserID != "103" || items[1].User.Username != "Robot C" || items[1].LastUsedAtMS != 3000 {
		t.Fatalf("second available robot mismatch: %+v", items[1])
	}
}

func TestListAllActiveGameRobotsFollowsCursor(t *testing.T) {
	client := &pagedRobotClient{
		pages: map[string]robotclient.ListGameRobotsResult{
			"": {
				Robots:     []robotclient.Robot{{UserID: 101}, {UserID: 102}},
				NextCursor: "102",
			},
			"102": {
				Robots: []robotclient.Robot{{UserID: 201}},
			},
		},
	}

	robots, err := listAllActiveGameRobots(context.Background(), client)
	if err != nil {
		t.Fatalf("list all active game robots failed: %v", err)
	}
	if len(robots) != 3 || robots[0].UserID != 101 || robots[1].UserID != 102 || robots[2].UserID != 201 {
		t.Fatalf("robots should include every cursor page, got %+v", robots)
	}
	if len(client.requests) != 2 {
		t.Fatalf("expected two page requests, got %+v", client.requests)
	}
	if client.requests[0].Cursor != "" || client.requests[1].Cursor != "102" {
		t.Fatalf("cursor chain mismatch: %+v", client.requests)
	}
	for _, req := range client.requests {
		if req.Status != "active" || req.PageSize != availableRoomRobotPageSize {
			t.Fatalf("request should use active status and page size %d, got %+v", availableRoomRobotPageSize, req)
		}
	}
}

type pagedRobotClient struct {
	pages    map[string]robotclient.ListGameRobotsResult
	requests []robotclient.ListGameRobotsRequest
}

func (c *pagedRobotClient) ListGameRobots(_ context.Context, req robotclient.ListGameRobotsRequest) (robotclient.ListGameRobotsResult, error) {
	c.requests = append(c.requests, req)
	return c.pages[req.Cursor], nil
}

func (c *pagedRobotClient) ListRoomRobots(context.Context, robotclient.ListRoomRobotsRequest) (robotclient.ListRoomRobotsResult, error) {
	return robotclient.ListRoomRobotsResult{}, errors.New("not used")
}
