package dashboard

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/middleware"
)

func TestUserProfileOverviewAggregatesNaturalUsersAndDailyStatistics(t *testing.T) {
	userDB, sqlMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create user sqlmock: %v", err)
	}
	defer userDB.Close()

	sqlMock.ExpectQuery(`(?s)SELECT CASE.*COALESCE\(gender, ''\).*FROM users.*WHERE app_code = \?.*profile_completed = 1.*game_robot.*quick_account.*GROUP BY gender_key.*ORDER BY user_count DESC`).
		WithArgs("huwaa").
		WillReturnRows(sqlmock.NewRows([]string{"gender_key", "user_count"}).
			AddRow("male", int64(4)).
			AddRow("female", int64(2)).
			AddRow("unknown", int64(1)))
	sqlMock.ExpectQuery(`(?s)FROM users AS natural_user.*LEFT JOIN LATERAL.*login_type = 'password'.*ORDER BY audit.created_at_ms DESC, audit.id DESC.*UNION ALL.*login_type = 'third_party'.*UNION ALL.*login_type = 'refresh'.*ORDER BY candidate.created_at_ms DESC, candidate.id DESC.*WHERE natural_user.app_code = \?.*profile_completed = 1.*game_robot.*quick_account.*latest.app_version IS NOT NULL.*TRIM\(latest.app_version\) <> ''.*NOT IN \('unknown', '<nil>'\).*GROUP BY version_key`).
		WithArgs("huwaa").
		WillReturnRows(sqlmock.NewRows([]string{"version_key", "user_count"}).
			AddRow("2.4.0", int64(4)).
			AddRow("unknown", int64(3)))

	requested := map[string]bool{}
	statisticsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requested[r.URL.Path] = true
		query := r.URL.Query()
		if query.Get("app_code") != "huwaa" || query.Get("stat_tz") != "Asia/Shanghai" ||
			query.Get("start_ms") != "1783699200000" || query.Get("end_ms") != "1783785600000" {
			http.Error(w, "unexpected statistics query: "+r.URL.RawQuery, http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/internal/v1/statistics/overview":
			_ = json.NewEncoder(w).Encode(map[string]any{"active_users": 5, "new_users": 2})
		default:
			http.NotFound(w, r)
		}
	}))
	defer statisticsServer.Close()

	service := NewService(nil, config.Config{StatisticsService: config.StatisticsServiceConfig{
		BaseURL:        statisticsServer.URL,
		RequestTimeout: time.Second,
	}}, nil, WithUserDB(userDB))
	router := newDashboardUserProfileTestRouter(NewWithService(service), []string{"overview:view"})
	request := httptest.NewRequest(http.MethodGet,
		"/api/v1/dashboard/user-profile-overview?app_code=Huwaa&stat_tz=Asia%2FShanghai&start_ms=1783699200000&end_ms=1783785600000", nil)
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var envelope struct {
		Code int                 `json:"code"`
		Data UserProfileOverview `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != 0 || envelope.Data.AppCode != "huwaa" || envelope.Data.TotalUsers != 7 ||
		envelope.Data.NewUsers != 2 || envelope.Data.ActiveUsers != 5 || envelope.Data.UpdatedAtMS <= 0 {
		t.Fatalf("overview mismatch: %+v", envelope.Data)
	}
	if len(envelope.Data.GenderDistribution) != 3 ||
		envelope.Data.GenderDistribution[0] != (UserProfileDistributionItem{Key: "male", Label: "男", Count: 4}) ||
		envelope.Data.GenderDistribution[2] != (UserProfileDistributionItem{Key: "unknown", Label: "未知", Count: 1}) {
		t.Fatalf("gender distribution mismatch: %+v", envelope.Data.GenderDistribution)
	}
	if len(envelope.Data.AppVersionDistribution) != 1 ||
		envelope.Data.AppVersionDistribution[0] != (UserProfileDistributionItem{Key: "2.4.0", Label: "2.4.0", Count: 4}) {
		t.Fatalf("version distribution mismatch: %+v", envelope.Data.AppVersionDistribution)
	}
	// 版本分布直接查询 login_audit 且只返回有最近登录版本的自然用户；statistics-service 只保留新增/DAU 口径。
	if len(requested) != 1 || !requested["/internal/v1/statistics/overview"] {
		t.Fatalf("statistics calls mismatch: %+v", requested)
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestGenderDistributionLabelMapsFlutterValues(t *testing.T) {
	tests := map[string]string{
		"male":       "男",
		"female":     "女",
		"non_binary": "非二元",
		"unknown":    "未知",
	}
	for input, expected := range tests {
		if actual := genderDistributionLabel(input); actual != expected {
			t.Fatalf("gender label mismatch: input=%q got=%q want=%q", input, actual, expected)
		}
	}
}

func TestUserProfileOverviewRouteRequiresOverviewPermission(t *testing.T) {
	router := newDashboardUserProfileTestRouter(NewWithService(&DashboardService{}), nil)
	request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboard/user-profile-overview", nil)
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusForbidden {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestUserProfileOverviewRecordsInternalErrorAgainstRequestID(t *testing.T) {
	userDB, sqlMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create user sqlmock: %v", err)
	}
	defer userDB.Close()

	sqlMock.ExpectQuery(`(?s)SELECT CASE.*FROM users.*profile_completed = 1`).
		WithArgs("lalu").
		WillReturnError(errors.New("synthetic mysql failure"))

	service := NewService(nil, config.Config{}, nil, WithUserDB(userDB))
	gin.SetMode(gin.TestMode)
	router := gin.New()
	var recordedErrors string
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextPermissions, []string{"overview:view"})
		c.Next()
		recordedErrors = c.Errors.String()
	})
	protected := router.Group("/api/v1")
	RegisterRoutes(protected, protected, NewWithService(service))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboard/user-profile-overview", nil)
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if !strings.Contains(recordedErrors, "query dashboard user profile: synthetic mysql failure") {
		t.Fatalf("request error was not recorded: %q", recordedErrors)
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func newDashboardUserProfileTestRouter(handler *Handler, permissions []string) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		// 复现 AuthRequired/AppCode 已完成的上下文写入，让路由测试同时覆盖 overview:view 中间件和 query 优先级。
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextPermissions, permissions)
		c.Next()
	})
	protected := router.Group("/api/v1")
	RegisterRoutes(protected, protected, handler)
	return router
}
