package tencentim

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"
)

func TestRealTencentIMCreateSendAndDestroyGroup(t *testing.T) {
	if os.Getenv("TENCENT_IM_REAL_TEST") != "1" {
		t.Skip("set TENCENT_IM_REAL_TEST=1 and Tencent IM env vars to run the real REST smoke test")
	}

	cfg := realRESTConfigFromEnv(t)
	client, err := NewRESTClient(cfg)
	if err != nil {
		t.Fatalf("NewRESTClient failed: %v", err)
	}

	now := time.Now().UTC()
	nowMS := now.UnixMilli()
	groupID := fmt.Sprintf("hy_codex_bc_r_%d", nowMS)
	eventID := fmt.Sprintf("codex_real_im_%d", nowMS)
	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()

	// 真实 IM 测试必须使用临时 GroupID，避免污染业务全局/区域播报群。
	if err := client.EnsureGroup(ctx, GroupSpec{
		GroupID:         groupID,
		Name:            "codex real im smoke",
		Type:            DefaultGroupType,
		ApplyJoinOption: "FreeAccess",
	}); err != nil {
		t.Fatalf("real Tencent IM create group failed: %v", err)
	}
	t.Cleanup(func() {
		cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 20*time.Second)
		defer cleanupCancel()
		if err := client.DestroyGroup(cleanupCtx, groupID); err != nil {
			t.Errorf("real Tencent IM destroy group cleanup failed: %v", err)
		}
	})
	memberUserID := nowMS % 9_000_000_000
	if err := realImportAccount(ctx, client, memberUserID); err != nil {
		t.Fatalf("real Tencent IM account import failed: %v", err)
	}
	if err := realAddGroupMember(ctx, client, groupID, memberUserID); err != nil {
		t.Fatalf("real Tencent IM add group member failed: %v", err)
	}

	const actorUserID = "323411852102995968"
	const targetUserID = "323411852102995969"
	c2cEventID := fmt.Sprintf("codex_real_im_c2c_%d", nowMS)
	c2cPayload, err := json.Marshal(map[string]any{
		"event_id":         c2cEventID,
		"event_type":       "room_user_kicked",
		"actor_user_id":    actorUserID,
		"target_user_id":   targetUserID,
		"sender_user_id":   actorUserID,
		"receiver_user_id": targetUserID,
		"sent_at_ms":       nowMS,
	})
	if err != nil {
		t.Fatalf("marshal c2c smoke payload failed: %v", err)
	}
	if err := client.PublishUserCustomMessage(ctx, CustomUserMessage{
		ToAccount:   strconv.FormatInt(memberUserID, 10),
		EventID:     c2cEventID,
		Desc:        "codex_real_im_c2c_probe",
		Ext:         "im_c2c_smoke",
		PayloadJSON: c2cPayload,
	}); err != nil {
		t.Fatalf("real Tencent IM send C2C custom message failed: %v", err)
	}
	// SyncOtherMachine=2 不同步管理员发送端，因此漫游消息必须从实际接收用户视角回读。
	c2cData, c2cCloudData := realWaitForC2CPayload(t, ctx, client, strconv.FormatInt(memberUserID, 10), cfg.AdminIdentifier, c2cEventID, now.Unix())
	realAssertStringUserIDs(t, c2cData, map[string]string{
		"actor_user_id":    actorUserID,
		"target_user_id":   targetUserID,
		"sender_user_id":   actorUserID,
		"receiver_user_id": targetUserID,
	})
	realAssertStringUserIDs(t, c2cCloudData, map[string]string{
		"actor_user_id":    actorUserID,
		"target_user_id":   targetUserID,
		"sender_user_id":   actorUserID,
		"receiver_user_id": targetUserID,
	})

	roomEvent := RoomEvent{
		GroupID:      groupID,
		EventID:      eventID,
		RoomID:       "codex-room-smoke",
		EventType:    "room_mic_change",
		ActorUserID:  actorUserID,
		TargetUserID: targetUserID,
		SeatNo:       3,
		RoomVersion:  22,
		Attributes: map[string]string{
			"action":          "change",
			"from_seat":       "1",
			"to_seat":         "3",
			"mic_session_id":  "codex-real-im-mic-session",
			"target_user_ids": `["323411852102995968","323411852102995969"]`,
		},
	}

	// 使用生产 RoomEvent 序列化和发送入口，避免真实测试绕开本次修复的代码路径。
	if err := client.PublishRoomEvent(ctx, roomEvent); err != nil {
		t.Fatalf("real Tencent IM send group custom message failed: %v", err)
	}
	groupData, groupCloudData := realWaitForGroupPayload(t, ctx, client, groupID, eventID)
	for source, payload := range map[string]string{"Data": groupData, "CloudCustomData": groupCloudData} {
		realAssertStringUserIDs(t, payload, map[string]string{
			"actor_user_id":  actorUserID,
			"target_user_id": targetUserID,
		})
		var decoded map[string]any
		if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
			t.Fatalf("decode group %s payload failed: %v", source, err)
		}
		attributes, ok := decoded["attributes"].(map[string]any)
		if !ok {
			t.Fatalf("group %s payload attributes type = %T, want object", source, decoded["attributes"])
		}
		var targetUserIDs []any
		if err := json.Unmarshal([]byte(fmt.Sprint(attributes["target_user_ids"])), &targetUserIDs); err != nil {
			t.Fatalf("decode group %s target_user_ids failed: %v", source, err)
		}
		if len(targetUserIDs) != 2 || targetUserIDs[0] != actorUserID || targetUserIDs[1] != targetUserID {
			t.Fatalf("group %s target_user_ids = %#v, want exact string IDs", source, targetUserIDs)
		}
	}
	if err := client.DeleteGroupMember(ctx, groupID, memberUserID); err != nil {
		t.Fatalf("real Tencent IM delete group member failed: %v", err)
	}

	t.Logf("real Tencent IM transport and history readback passed: sdk_app_id=%d group_id=%s group_event_id=%s c2c_event_id=%s exact_actor_user_id=%s removed_user_id=%d", cfg.SDKAppID, groupID, eventID, c2cEventID, actorUserID, memberUserID)
}

type realHistoryMessage struct {
	MsgBody         []messageElement `json:"MsgBody"`
	CloudCustomData string           `json:"CloudCustomData"`
}

type realGroupHistoryResponse struct {
	restResponse
	RspMsgList []realHistoryMessage `json:"RspMsgList"`
}

type realC2CHistoryResponse struct {
	restResponse
	MsgList []realHistoryMessage `json:"MsgList"`
}

func realWaitForGroupPayload(t *testing.T, ctx context.Context, client *RESTClient, groupID, eventID string) (string, string) {
	t.Helper()
	for attempt := 0; attempt < 10; attempt++ {
		var response realGroupHistoryResponse
		if err := client.post(ctx, "v4/group_open_http_svc/group_msg_get_simple", map[string]any{
			"GroupId":      groupID,
			"ReqMsgNumber": 20,
		}, &response); err != nil {
			t.Fatalf("real Tencent IM group history request failed: %v", err)
		}
		if err := response.err(); err != nil {
			t.Fatalf("real Tencent IM group history failed: %v", err)
		}
		if data, cloudData, ok := realFindHistoryPayload(response.RspMsgList, eventID); ok {
			return data, cloudData
		}
		select {
		case <-ctx.Done():
			t.Fatalf("real Tencent IM group history readback timed out: %v", ctx.Err())
		case <-time.After(500 * time.Millisecond):
		}
	}
	t.Fatalf("real Tencent IM group history did not contain event_id=%s", eventID)
	return "", ""
}

func realWaitForC2CPayload(t *testing.T, ctx context.Context, client *RESTClient, operatorAccount, peerAccount, eventID string, sentAtSeconds int64) (string, string) {
	t.Helper()
	for attempt := 0; attempt < 10; attempt++ {
		var response realC2CHistoryResponse
		if err := client.post(ctx, "v4/openim/admin_getroammsg", map[string]any{
			"Operator_Account": operatorAccount,
			"Peer_Account":     peerAccount,
			"MaxCnt":           100,
			"MinTime":          sentAtSeconds - 60,
			"MaxTime":          time.Now().UTC().Unix() + 60,
		}, &response); err != nil {
			t.Fatalf("real Tencent IM C2C history request failed: %v", err)
		}
		if err := response.err(); err != nil {
			t.Fatalf("real Tencent IM C2C history failed: %v", err)
		}
		if data, cloudData, ok := realFindHistoryPayload(response.MsgList, eventID); ok {
			return data, cloudData
		}
		select {
		case <-ctx.Done():
			t.Fatalf("real Tencent IM C2C history readback timed out: %v", ctx.Err())
		case <-time.After(500 * time.Millisecond):
		}
	}
	t.Fatalf("real Tencent IM C2C history did not contain event_id=%s", eventID)
	return "", ""
}

func realFindHistoryPayload(messages []realHistoryMessage, eventID string) (string, string, bool) {
	for _, message := range messages {
		for _, element := range message.MsgBody {
			if element.MsgType != "TIMCustomElem" || !strings.Contains(element.MsgContent.Data, eventID) {
				continue
			}
			return element.MsgContent.Data, message.CloudCustomData, true
		}
	}
	return "", "", false
}

func realAssertStringUserIDs(t *testing.T, payload string, expected map[string]string) {
	t.Helper()
	var decoded map[string]any
	if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
		t.Fatalf("decode Tencent IM payload failed: %v", err)
	}
	for key, want := range expected {
		got, ok := decoded[key].(string)
		if !ok {
			t.Fatalf("Tencent IM payload %s type = %T, want string", key, decoded[key])
		}
		if got != want {
			t.Fatalf("Tencent IM payload %s = %q, want %q", key, got, want)
		}
	}
}

func realImportAccount(ctx context.Context, client *RESTClient, userID int64) error {
	return client.ImportAccount(ctx, AccountProfile{
		UserID: userID,
		Nick:   "codex-im-smoke",
	})
}

func realAddGroupMember(ctx context.Context, client *RESTClient, groupID string, userID int64) error {
	var response restResponse
	if err := client.post(ctx, "v4/group_open_http_svc/add_group_member", map[string]any{
		"GroupId": groupID,
		"Silence": 1,
		"MemberList": []map[string]any{
			{"Member_Account": strconv.FormatInt(userID, 10)},
		},
	}, &response); err != nil {
		return err
	}
	return response.err()
}

func realRESTConfigFromEnv(t *testing.T) RESTConfig {
	t.Helper()

	sdkAppID, err := strconv.ParseInt(requiredEnv(t, "TENCENT_IM_SDK_APP_ID"), 10, 64)
	if err != nil || sdkAppID <= 0 {
		t.Fatalf("TENCENT_IM_SDK_APP_ID is invalid")
	}
	endpoint := strings.TrimSpace(os.Getenv("TENCENT_IM_ENDPOINT"))
	if endpoint == "" {
		endpoint = DefaultEndpoint
	}
	adminUserSigTTL := time.Hour
	if rawTTL := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_USERSIG_TTL")); rawTTL != "" {
		parsed, err := time.ParseDuration(rawTTL)
		if err != nil || parsed <= 0 {
			t.Fatalf("TENCENT_IM_ADMIN_USERSIG_TTL is invalid")
		}
		adminUserSigTTL = parsed
	}

	return RESTConfig{
		SDKAppID:        sdkAppID,
		SecretKey:       requiredEnv(t, "TENCENT_IM_SECRET_KEY"),
		AdminIdentifier: requiredEnv(t, "TENCENT_IM_ADMIN_IDENTIFIER"),
		AdminUserSigTTL: adminUserSigTTL,
		Endpoint:        endpoint,
		GroupType:       DefaultGroupType,
	}
}

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

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