package tencentim

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"
)

// roundTripFunc 允许测试拦截腾讯云 REST 请求而不访问外网。
type roundTripFunc func(*http.Request) (*http.Response, error)

// RoundTrip 实现 http.RoundTripper。
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return f(request)
}

// TestRESTClientEnsureRoomGroupBuildsTencentRequest 锁定建群请求的 REST 命令和核心字段。
func TestRESTClientEnsureRoomGroupBuildsTencentRequest(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}

		return okRESTResponse(), nil
	})

	if err := client.EnsureRoomGroup(context.Background(), "room-1001", 10001); err != nil {
		t.Fatalf("EnsureRoomGroup failed: %v", err)
	}

	if capturedPath != "/"+createGroupCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	if capturedBody["GroupId"] != "room-1001" || capturedBody["Type"] != DefaultGroupType {
		t.Fatalf("unexpected create group body: %+v", capturedBody)
	}
}

// TestRESTClientPublishRoomEventBuildsCustomMessage 锁定房间系统事件的自定义消息载荷。
func TestRESTClientPublishRoomEventBuildsCustomMessage(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}

		return okRESTResponse(), nil
	})

	err := client.PublishRoomEvent(context.Background(), RoomEvent{
		GroupID:      "room_1001",
		EventID:      "evt-1",
		RoomID:       "room-1001",
		EventType:    "room_gift_sent",
		ActorUserID:  "323411852102995968",
		TargetUserID: "323411852102996032",
		GiftValue:    20,
		RoomVersion:  7,
	})
	if err != nil {
		t.Fatalf("PublishRoomEvent failed: %v", err)
	}

	if capturedPath != "/"+sendGroupMsgCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	if capturedBody["GroupId"] != "room_1001" {
		t.Fatalf("group id mismatch: %+v", capturedBody)
	}
	body, ok := capturedBody["MsgBody"].([]any)
	if !ok || len(body) != 1 {
		t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"])
	}
	element, ok := body[0].(map[string]any)
	if !ok || element["MsgType"] != "TIMCustomElem" {
		t.Fatalf("unexpected message element: %+v", body[0])
	}
	content, ok := element["MsgContent"].(map[string]any)
	if !ok || content["Ext"] != "room_system_message" || content["Desc"] != "room_gift_sent" {
		t.Fatalf("unexpected custom content: %+v", element["MsgContent"])
	}
	if !strings.Contains(content["Data"].(string), `"event_id":"evt-1"`) {
		t.Fatalf("event payload missing event_id: %s", content["Data"])
	}
	if !strings.Contains(content["Data"].(string), `"room_id":"room-1001"`) {
		t.Fatalf("event payload must keep internal room_id: %s", content["Data"])
	}
	var eventPayload map[string]any
	if err := json.Unmarshal([]byte(content["Data"].(string)), &eventPayload); err != nil {
		t.Fatalf("decode room event payload failed: %v", err)
	}
	if eventPayload["actor_user_id"] != "323411852102995968" || eventPayload["target_user_id"] != "323411852102996032" {
		t.Fatalf("room event user IDs must remain exact JSON strings: %+v", eventPayload)
	}
}

func TestRESTClientPublishRoomEventFlattensAttributesForClientPayload(t *testing.T) {
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}
		return okRESTResponse(), nil
	})

	err := client.PublishRoomEvent(context.Background(), RoomEvent{
		GroupID:      "room_1001",
		EventID:      "evt-robot-lucky-1",
		RoomID:       "room-1001",
		EventType:    "lucky_gift_drawn",
		ActorUserID:  "10001",
		TargetUserID: "10002",
		GiftValue:    500,
		Attributes: map[string]string{
			"event_type":             "should_not_override",
			"draw_id":                "robot_lucky_draw_1",
			"gift_id":                "gift_lucky_1",
			"gift_count":             "1",
			"pool_id":                "robot_lucky_display",
			"multiplier_ppm":         "5000000",
			"effective_reward_coins": "500",
			"reward_status":          "granted",
		},
	})
	if err != nil {
		t.Fatalf("PublishRoomEvent failed: %v", err)
	}

	body, ok := capturedBody["MsgBody"].([]any)
	if !ok || len(body) != 1 {
		t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"])
	}
	element := body[0].(map[string]any)
	content := element["MsgContent"].(map[string]any)
	var payload map[string]any
	if err := json.Unmarshal([]byte(content["Data"].(string)), &payload); err != nil {
		t.Fatalf("decode custom data failed: %v", err)
	}
	if payload["event_type"] != "lucky_gift_drawn" {
		t.Fatalf("attributes must not override top-level event_type: %+v", payload)
	}
	if payload["draw_id"] != "robot_lucky_draw_1" || payload["effective_reward_coins"] != "500" || payload["reward_status"] != "granted" {
		t.Fatalf("lucky gift attributes must be flattened for existing clients: %+v", payload)
	}
	attributes, ok := payload["attributes"].(map[string]any)
	if !ok || attributes["effective_reward_coins"] != "500" {
		t.Fatalf("attributes must still be kept for diagnostics and fallback clients: %+v", payload["attributes"])
	}
}

func TestRESTClientPublishGroupCustomMessageBuildsBroadcastPayload(t *testing.T) {
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}
		return okRESTResponse(), nil
	})

	err := client.PublishGroupCustomMessage(context.Background(), CustomGroupMessage{
		GroupID:     "hy_lalu_bc_r_1001",
		EventID:     "gift_broadcast:room-1:cmd-1",
		Desc:        "super_gift",
		Ext:         "im_broadcast",
		FromAccount: "administrator",
		PayloadJSON: json.RawMessage(`{"event_id":"gift_broadcast:room-1:cmd-1","broadcast_type":"super_gift"}`),
	})
	if err != nil {
		t.Fatalf("PublishGroupCustomMessage failed: %v", err)
	}

	if capturedBody["GroupId"] != "hy_lalu_bc_r_1001" || capturedBody["From_Account"] != "administrator" {
		t.Fatalf("broadcast target mismatch: %+v", capturedBody)
	}
	if capturedBody["CloudCustomData"] != `{"event_id":"gift_broadcast:room-1:cmd-1","broadcast_type":"super_gift"}` {
		t.Fatalf("cloud custom data mismatch: %+v", capturedBody)
	}
}

func TestRESTClientPublishUserCustomMessageBuildsC2CPayload(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body: %v", err)
		}
		return okRESTResponse(), nil
	})

	err := client.PublishUserCustomMessage(context.Background(), CustomUserMessage{
		ToAccount:            "10001",
		EventID:              "evt-wallet-1",
		Desc:                 "WalletBalanceChanged",
		Ext:                  "wallet_notice",
		DeduplicateByEventID: true,
		PayloadJSON:          json.RawMessage(`{"event_id":"evt-wallet-1"}`),
	})
	if err != nil {
		t.Fatalf("PublishUserCustomMessage failed: %v", err)
	}
	if capturedPath != "/"+sendC2CMsgCommand {
		t.Fatalf("unexpected command path: %s", capturedPath)
	}
	if capturedBody["To_Account"] != "10001" || capturedBody["SyncOtherMachine"].(float64) != 2 {
		t.Fatalf("unexpected c2c target: %+v", capturedBody)
	}
	if capturedBody["From_Account"] != "administrator" {
		t.Fatalf("unexpected c2c sender: %+v", capturedBody)
	}
	firstSeq, seqOK := capturedBody["MsgSeq"].(float64)
	firstRandom, randomOK := capturedBody["MsgRandom"].(float64)
	if !seqOK || !randomOK || firstSeq <= 0 {
		t.Fatalf("c2c message must carry stable Tencent dedup fields: %+v", capturedBody)
	}
	body, ok := capturedBody["MsgBody"].([]any)
	if !ok || len(body) != 1 {
		t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"])
	}
	element := body[0].(map[string]any)
	if element["MsgType"] != "TIMCustomElem" {
		t.Fatalf("unexpected msg type: %+v", element)
	}
	content := element["MsgContent"].(map[string]any)
	if content["Desc"] != "WalletBalanceChanged" || content["Ext"] != "wallet_notice" || content["Data"] != `{"event_id":"evt-wallet-1"}` {
		t.Fatalf("unexpected custom content: %+v", content)
	}

	// 同一逻辑 event_id 的重试必须复用腾讯 120 秒去重键，不能因 REST 请求重新生成随机数而形成第二条消息。
	if err := client.PublishUserCustomMessage(context.Background(), CustomUserMessage{
		ToAccount:            "10001",
		EventID:              "evt-wallet-1",
		Desc:                 "WalletBalanceChanged",
		Ext:                  "wallet_notice",
		DeduplicateByEventID: true,
		PayloadJSON:          json.RawMessage(`{"event_id":"evt-wallet-1"}`),
	}); err != nil {
		t.Fatalf("PublishUserCustomMessage replay failed: %v", err)
	}
	if capturedBody["MsgSeq"] != firstSeq || capturedBody["MsgRandom"] != firstRandom {
		t.Fatalf("c2c retry dedup fields changed: first=(%v,%v) replay=(%v,%v)", firstSeq, firstRandom, capturedBody["MsgSeq"], capturedBody["MsgRandom"])
	}
}

func TestRESTClientImportAccountBuildsTencentRequest(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}
		return okRESTResponse(), nil
	})

	if err := client.ImportAccount(context.Background(), AccountProfile{
		UserID:  10002,
		Nick:    "Lingxian",
		FaceURL: "https://cdn.example/avatar.png",
	}); err != nil {
		t.Fatalf("ImportAccount failed: %v", err)
	}

	if capturedPath != "/"+accountImportCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	if capturedBody["UserID"] != "10002" || capturedBody["Nick"] != "Lingxian" || capturedBody["FaceUrl"] != "https://cdn.example/avatar.png" {
		t.Fatalf("unexpected account import body: %+v", capturedBody)
	}
}

func TestRESTClientImportAccountTreatsExistingAccountAsSuccess(t *testing.T) {
	client := newTestRESTClient(t, func(_ *http.Request) (*http.Response, error) {
		payload := []byte(`{"ActionStatus":"FAIL","ErrorCode":70107,"ErrorInfo":"account already exists"}`)
		return &http.Response{
			StatusCode: http.StatusOK,
			Body:       io.NopCloser(bytes.NewReader(payload)),
			Header:     make(http.Header),
		}, nil
	})

	if err := client.ImportAccount(context.Background(), AccountProfile{UserID: 10002}); err != nil {
		t.Fatalf("existing account must be idempotent success: %v", err)
	}
}

// TestRESTClientDestroyGroupBuildsTencentRequest 锁定临时群清理的 REST 请求。
func TestRESTClientDestroyGroupBuildsTencentRequest(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}

		return okRESTResponse(), nil
	})

	if err := client.DestroyGroup(context.Background(), "hy_codex_bc_r_210"); err != nil {
		t.Fatalf("DestroyGroup failed: %v", err)
	}

	if capturedPath != "/"+destroyGroupCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	if capturedBody["GroupId"] != "hy_codex_bc_r_210" {
		t.Fatalf("unexpected destroy group body: %+v", capturedBody)
	}
}

// TestRESTClientDeleteRoomGroupMemberBuildsTencentRequest 锁定踢出 IM 群成员的 REST 请求。
func TestRESTClientDeleteRoomGroupMemberBuildsTencentRequest(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}

		return okRESTResponse(), nil
	})

	if err := client.DeleteRoomGroupMember(context.Background(), "room-1001", 10002); err != nil {
		t.Fatalf("DeleteRoomGroupMember failed: %v", err)
	}

	if capturedPath != "/"+deleteGroupMemberCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	members, ok := capturedBody["MemberToDel_Account"].([]any)
	if !ok || len(members) != 1 || members[0] != "10002" {
		t.Fatalf("unexpected member removal body: %+v", capturedBody)
	}
	if capturedBody["GroupId"] != "room-1001" || capturedBody["Silence"].(float64) != 1 {
		t.Fatalf("unexpected group removal body: %+v", capturedBody)
	}
}

// TestRESTClientSetGroupMemberMutedUsesNativeForbidSendMsg 锁定禁言和解除禁言只调用目标群成员接口。
func TestRESTClientSetGroupMemberMutedUsesNativeForbidSendMsg(t *testing.T) {
	paths := make([]string, 0, 2)
	bodies := make([]map[string]any, 0, 2)
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		paths = append(paths, request.URL.Path)
		var body map[string]any
		if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}
		bodies = append(bodies, body)
		return okRESTResponse(), nil
	})

	if err := client.SetGroupMemberMuted(context.Background(), "room-1001", 10002, true); err != nil {
		t.Fatalf("SetGroupMemberMuted(true) failed: %v", err)
	}
	if err := client.SetGroupMemberMuted(context.Background(), "room-1001", 10002, false); err != nil {
		t.Fatalf("SetGroupMemberMuted(false) failed: %v", err)
	}

	for index, path := range paths {
		if path != "/"+forbidSendMsgCommand {
			t.Fatalf("request %d path mismatch: got %q", index, path)
		}
		members, ok := bodies[index]["Members_Account"].([]any)
		if !ok || len(members) != 1 || members[0] != "10002" || bodies[index]["GroupId"] != "room-1001" {
			t.Fatalf("request %d target mismatch: %+v", index, bodies[index])
		}
	}
	if bodies[0]["MuteTime"] != float64(PermanentGroupMuteSeconds) {
		t.Fatalf("mute must use permanent Tencent duration: %+v", bodies[0])
	}
	if bodies[1]["MuteTime"] != float64(0) {
		t.Fatalf("unmute must use MuteTime=0: %+v", bodies[1])
	}
}

// TestRESTClientKickUserBuildsTencentRequest 锁定封禁链路使用腾讯云 IM 失效账号登录态接口。
func TestRESTClientKickUserBuildsTencentRequest(t *testing.T) {
	var capturedPath string
	var capturedBody map[string]any
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		capturedPath = request.URL.Path
		if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil {
			t.Fatalf("decode request body failed: %v", err)
		}

		return okRESTResponse(), nil
	})

	if err := client.KickUser(context.Background(), 10002); err != nil {
		t.Fatalf("KickUser failed: %v", err)
	}

	if capturedPath != "/"+kickUserCommand {
		t.Fatalf("path mismatch: got %q", capturedPath)
	}
	if capturedBody["UserID"] != "10002" {
		t.Fatalf("unexpected kick body: %+v", capturedBody)
	}
}

func TestRESTClientTransportErrorDoesNotExposeUserSig(t *testing.T) {
	client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) {
		if request.URL.Query().Get("usersig") == "" {
			t.Fatal("test request must contain the signed Tencent IM query")
		}
		return nil, errors.New("synthetic EOF")
	})

	err := client.KickUser(context.Background(), 10002)
	if err == nil {
		t.Fatal("expected transport failure")
	}
	if strings.Contains(err.Error(), "usersig=") || strings.Contains(err.Error(), "sdkappid=") {
		t.Fatalf("transport error exposed signed request URL: %q", err.Error())
	}
	if !strings.Contains(err.Error(), "synthetic EOF") {
		t.Fatalf("transport cause must remain actionable: %q", err.Error())
	}
}

func newTestRESTClient(t *testing.T, roundTrip roundTripFunc) *RESTClient {
	t.Helper()

	client, err := NewRESTClient(RESTConfig{
		SDKAppID:        1400000000,
		SecretKey:       "secret",
		AdminIdentifier: "administrator",
		AdminUserSigTTL: time.Hour,
		HTTPClient: &http.Client{
			Transport: roundTrip,
		},
	})
	if err != nil {
		t.Fatalf("NewRESTClient failed: %v", err)
	}

	return client
}

func okRESTResponse() *http.Response {
	payload := []byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`)

	return &http.Response{
		StatusCode: http.StatusOK,
		Body:       io.NopCloser(bytes.NewReader(payload)),
		Header:     make(http.Header),
	}
}
