package smoke_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	userv1 "hyapp.local/api/proto/user/v1"
)

func TestCPIntimacyLeaderboardLocalRealFlow(t *testing.T) {
	if os.Getenv("CP_INTIMACY_REAL_FLOW_TEST") != "1" {
		t.Skip("set CP_INTIMACY_REAL_FLOW_TEST=1 to run the local CP intimacy leaderboard flow with real users and real data")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()

	baseURL := strings.TrimRight(envOr("CP_INTIMACY_BASE_URL", "http://127.0.0.1:13000/api/v1"), "/")
	userServiceAddr := envOr("CP_INTIMACY_USER_SERVICE_ADDR", "127.0.0.1:13005")
	appCode := envOr("CP_INTIMACY_APP_CODE", "lalu")
	token := requiredCPIntimacyEnv(t, "CP_INTIMACY_ACCESS_TOKEN")
	relationType := strings.TrimSpace(os.Getenv("CP_INTIMACY_RELATION_TYPE"))
	pageSize := int32EnvOr(t, "CP_INTIMACY_PAGE_SIZE", 100)
	batchSize := int32EnvOr(t, "CP_INTIMACY_BATCH_SIZE", 10000)
	expectedUserA, expectedUserB := optionalCPIntimacyExpectedPair(t)

	processed := refreshCPIntimacyLeaderboard(t, ctx, userServiceAddr, appCode, batchSize)
	if processed <= 0 {
		t.Fatalf("CP intimacy refresh processed no active relationships; local real data must contain at least one active CP/brother/sister relationship")
	}

	data := requestCPIntimacyLeaderboard(t, ctx, &http.Client{Timeout: 15 * time.Second}, baseURL, appCode, token, relationType, pageSize)
	if data.Total <= 0 || len(data.Items) == 0 {
		t.Fatalf("gateway returned empty CP intimacy leaderboard after refresh: total=%d items=%d", data.Total, len(data.Items))
	}
	if data.Page != 1 || data.PageSize != pageSize || data.ServerTimeMS <= 0 {
		t.Fatalf("gateway leaderboard pagination/server time mismatch: %+v", data)
	}

	item := data.Items[0]
	if expectedUserA != "" && expectedUserB != "" {
		found, ok := findCPIntimacyPair(data.Items, expectedUserA, expectedUserB)
		if !ok {
			t.Fatalf("expected real CP pair (%s,%s) not found in first page: total=%d page_size=%d items=%+v", expectedUserA, expectedUserB, data.Total, data.PageSize, data.Items)
		}
		item = found
	}
	assertCPIntimacyLeaderboardItem(t, item)
	if shouldRequireCPIntimacyAvatarFrame() {
		assertCPIntimacyAvatarFrame(t, item.UserA, "user_a")
		assertCPIntimacyAvatarFrame(t, item.UserB, "user_b")
	}

	t.Logf(
		"cp intimacy real flow passed: base_url=%s user_service=%s app_code=%s relation_type=%q processed=%d total=%d rank=%d relationship_id=%s user_a=%s/%s user_b=%s/%s",
		baseURL,
		userServiceAddr,
		appCode,
		relationType,
		processed,
		data.Total,
		item.Rank,
		item.RelationshipID,
		item.UserA.UserID,
		item.UserA.DisplayUserID,
		item.UserB.UserID,
		item.UserB.DisplayUserID,
	)
}

type cpIntimacyEnvelope struct {
	Code      string          `json:"code"`
	Message   string          `json:"message"`
	RequestID string          `json:"request_id"`
	Data      json.RawMessage `json:"data"`
}

type cpIntimacyLeaderboardData struct {
	Items        []cpIntimacyLeaderboardItem `json:"items"`
	Total        int64                       `json:"total"`
	Page         int32                       `json:"page"`
	PageSize     int32                       `json:"page_size"`
	ServerTimeMS int64                       `json:"server_time_ms"`
}

type cpIntimacyLeaderboardItem struct {
	Rank           int64          `json:"rank"`
	RelationshipID string         `json:"relationship_id"`
	RelationType   string         `json:"relation_type"`
	IntimacyValue  int64          `json:"intimacy_value"`
	Level          int32          `json:"level"`
	UserA          cpIntimacyUser `json:"user_a"`
	UserB          cpIntimacyUser `json:"user_b"`
	FormedAtMS     int64          `json:"formed_at_ms"`
	UpdatedAtMS    int64          `json:"updated_at_ms"`
}

type cpIntimacyUser struct {
	UserID        string                 `json:"user_id"`
	DisplayUserID string                 `json:"display_user_id"`
	Username      string                 `json:"username"`
	Avatar        string                 `json:"avatar"`
	AvatarFrame   *cpIntimacyAvatarFrame `json:"avatar_frame"`
}

type cpIntimacyAvatarFrame struct {
	ResourceID   int64  `json:"resource_id"`
	ResourceCode string `json:"resource_code"`
	Name         string `json:"name"`
	AssetURL     string `json:"asset_url"`
	PreviewURL   string `json:"preview_url"`
	AnimationURL string `json:"animation_url"`
	MetadataJSON string `json:"metadata_json"`
}

func refreshCPIntimacyLeaderboard(t *testing.T, ctx context.Context, addr string, appCode string, batchSize int32) int32 {
	t.Helper()

	conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		t.Fatalf("dial local user-service %s failed: %v", addr, err)
	}
	defer conn.Close()

	runID := fmt.Sprintf("cp_intimacy_real_flow_%d", time.Now().UTC().UnixMilli())
	resp, err := userv1.NewUserCronServiceClient(conn).RefreshCPIntimacyLeaderboard(ctx, &userv1.CronBatchRequest{
		Meta: &userv1.RequestMeta{
			RequestId:     runID,
			Caller:        "cp-intimacy-real-flow-test",
			GatewayNodeId: "local-smoke-test",
			SentAtMs:      time.Now().UTC().UnixMilli(),
			AppCode:       appCode,
		},
		RunId:     runID,
		WorkerId:  "local-smoke-test",
		BatchSize: batchSize,
		LockTtlMs: int64((5 * time.Minute).Milliseconds()),
	})
	if err != nil {
		t.Fatalf("refresh CP intimacy leaderboard through local user-service failed: %v", err)
	}
	if resp.GetFailureCount() != 0 {
		t.Fatalf("refresh CP intimacy leaderboard reported failures: %+v", resp)
	}
	return resp.GetProcessedCount()
}

func requestCPIntimacyLeaderboard(t *testing.T, ctx context.Context, client *http.Client, baseURL string, appCode string, token string, relationType string, pageSize int32) cpIntimacyLeaderboardData {
	t.Helper()

	query := url.Values{}
	query.Set("page", "1")
	query.Set("page_size", strconv.FormatInt(int64(pageSize), 10))
	if relationType != "" {
		query.Set("relation_type", relationType)
	}
	var data cpIntimacyLeaderboardData
	doCPIntimacyJSON(t, ctx, client, http.MethodGet, baseURL+"/cp/intimacy-leaderboard?"+query.Encode(), appCode, token, nil, &data)
	return data
}

func doCPIntimacyJSON(t *testing.T, ctx context.Context, client *http.Client, method string, url string, appCode string, token string, body any, out any) {
	t.Helper()

	var payload io.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("marshal %s %s body failed: %v", method, url, err)
		}
		payload = bytes.NewReader(raw)
	}
	req, err := http.NewRequestWithContext(ctx, method, url, payload)
	if err != nil {
		t.Fatalf("build %s %s request failed: %v", method, url, err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("X-App-Code", appCode)
	req.Header.Set("X-App-Language", "en")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("%s %s failed: %v", method, url, err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read %s %s response failed: %v", method, url, err)
	}

	var envelope cpIntimacyEnvelope
	if err := json.Unmarshal(raw, &envelope); err != nil {
		t.Fatalf("decode %s %s envelope failed: status=%d body=%s err=%v", method, url, resp.StatusCode, string(raw), err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" {
		t.Fatalf("%s %s returned non-OK: status=%d code=%s message=%s request_id=%s body=%s", method, url, resp.StatusCode, envelope.Code, envelope.Message, envelope.RequestID, string(raw))
	}
	if out != nil {
		if len(envelope.Data) == 0 {
			t.Fatalf("%s %s returned empty data: request_id=%s", method, url, envelope.RequestID)
		}
		if err := json.Unmarshal(envelope.Data, out); err != nil {
			t.Fatalf("decode %s %s data failed: request_id=%s data=%s err=%v", method, url, envelope.RequestID, string(envelope.Data), err)
		}
	}
}

func assertCPIntimacyLeaderboardItem(t *testing.T, item cpIntimacyLeaderboardItem) {
	t.Helper()

	if item.Rank <= 0 || strings.TrimSpace(item.RelationshipID) == "" || strings.TrimSpace(item.RelationType) == "" || item.IntimacyValue < 0 || item.Level <= 0 {
		t.Fatalf("leaderboard item base fields invalid: %+v", item)
	}
	assertCPIntimacyUser(t, item.UserA, "user_a")
	assertCPIntimacyUser(t, item.UserB, "user_b")
	if item.UserA.UserID == item.UserB.UserID {
		t.Fatalf("leaderboard pair must contain two different users: %+v", item)
	}
}

func assertCPIntimacyUser(t *testing.T, user cpIntimacyUser, side string) {
	t.Helper()

	if strings.TrimSpace(user.UserID) == "" || strings.TrimSpace(user.DisplayUserID) == "" || strings.TrimSpace(user.Username) == "" || strings.TrimSpace(user.Avatar) == "" {
		t.Fatalf("%s must include real long id, short id, username and avatar: %+v", side, user)
	}
	if user.AvatarFrame != nil {
		assertCPIntimacyAvatarFrame(t, user, side)
	}
}

func assertCPIntimacyAvatarFrame(t *testing.T, user cpIntimacyUser, side string) {
	t.Helper()

	if user.AvatarFrame == nil || user.AvatarFrame.ResourceID <= 0 || strings.TrimSpace(user.AvatarFrame.ResourceCode) == "" {
		t.Fatalf("%s must include equipped avatar_frame when CP_INTIMACY_REQUIRE_AVATAR_FRAME=1: %+v", side, user)
	}
}

func findCPIntimacyPair(items []cpIntimacyLeaderboardItem, userA string, userB string) (cpIntimacyLeaderboardItem, bool) {
	for _, item := range items {
		if sameCPIntimacyPair(item, userA, userB) {
			return item, true
		}
	}
	return cpIntimacyLeaderboardItem{}, false
}

func sameCPIntimacyPair(item cpIntimacyLeaderboardItem, userA string, userB string) bool {
	left := strings.TrimSpace(item.UserA.UserID)
	right := strings.TrimSpace(item.UserB.UserID)
	return (left == userA && right == userB) || (left == userB && right == userA)
}

func optionalCPIntimacyExpectedPair(t *testing.T) (string, string) {
	t.Helper()

	userA := strings.TrimSpace(os.Getenv("CP_INTIMACY_EXPECT_USER_A_ID"))
	userB := strings.TrimSpace(os.Getenv("CP_INTIMACY_EXPECT_USER_B_ID"))
	if userA == "" && userB == "" {
		return "", ""
	}
	if userA == "" || userB == "" {
		t.Fatalf("CP_INTIMACY_EXPECT_USER_A_ID and CP_INTIMACY_EXPECT_USER_B_ID must be set together")
	}
	if _, err := strconv.ParseInt(userA, 10, 64); err != nil {
		t.Fatalf("CP_INTIMACY_EXPECT_USER_A_ID must be an int64: %v", err)
	}
	if _, err := strconv.ParseInt(userB, 10, 64); err != nil {
		t.Fatalf("CP_INTIMACY_EXPECT_USER_B_ID must be an int64: %v", err)
	}
	return userA, userB
}

func int32EnvOr(t *testing.T, key string, fallback int32) int32 {
	t.Helper()

	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	value, err := strconv.ParseInt(raw, 10, 32)
	if err != nil || value <= 0 {
		t.Fatalf("%s must be a positive int32", key)
	}
	return int32(value)
}

func requiredCPIntimacyEnv(t *testing.T, key string) string {
	t.Helper()

	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		t.Fatalf("%s is required for CP intimacy real flow test", key)
	}
	return value
}

func shouldRequireCPIntimacyAvatarFrame() bool {
	return strings.TrimSpace(os.Getenv("CP_INTIMACY_REQUIRE_AVATAR_FRAME")) == "1"
}
