package rocketmqx

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"

	"hyapp/pkg/logx"

	"github.com/apache/rocketmq-client-go/v2/consumer"
	"github.com/apache/rocketmq-client-go/v2/primitive"
)

type fakePushConsumer struct {
	callback func(context.Context, ...*primitive.MessageExt) (consumer.ConsumeResult, error)
}

func (f *fakePushConsumer) Start() error    { return nil }
func (f *fakePushConsumer) Shutdown() error { return nil }
func (f *fakePushConsumer) Subscribe(_ string, _ consumer.MessageSelector, callback func(context.Context, ...*primitive.MessageExt) (consumer.ConsumeResult, error)) error {
	f.callback = callback
	return nil
}
func (f *fakePushConsumer) Unsubscribe(string) error           { return nil }
func (f *fakePushConsumer) Suspend()                           {}
func (f *fakePushConsumer) Resume()                            {}
func (f *fakePushConsumer) GetOffsetDiffMap() map[string]int64 { return nil }

func TestSubscribeHandlerErrorReturnsRetryLaterWithoutSDKError(t *testing.T) {
	var output bytes.Buffer
	if err := logx.Init(logx.Config{Service: "rocketmqx-test", Env: "test", Format: "json", Output: &output}); err != nil {
		t.Fatalf("init test logger: %v", err)
	}

	client := &fakePushConsumer{}
	consumerWrapper := &Consumer{client: client, group: "group-safe-log"}
	handlerErr := errors.New("projection unavailable")
	if err := consumerWrapper.Subscribe("topic-safe-log", "tag-safe-log", func(context.Context, ConsumedMessage) error {
		return handlerErr
	}); err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	if client.callback == nil {
		t.Fatal("subscribe callback was not captured")
	}

	extended := &primitive.MessageExt{MsgId: "msg-safe-log", ReconsumeTimes: 3}
	// 直接初始化嵌入的 Message，避免复制其 RWMutex；Queue 仍故意保持 nil，确保日志路径
	// 一旦把 MessageExt 交给 String 格式化就会在测试中立即暴露。
	extended.Topic = "topic-safe-log"
	extended.Body = []byte("sensitive-body-must-not-be-logged")
	extended.WithTag("tag-safe-log")
	extended.WithKeys([]string{"event-safe-log"})
	extended.WithProperty("private-property", "secret-value-must-not-be-logged")
	// Queue 故意保持 nil：MessageExt.String 会解引用 Queue 并 panic。callback 能完成并写日志，
	// 因而同时锁定包装层只记录复制后的安全标量，绝不把 MessageExt 交给格式化器。
	result, callbackErr := client.callback(context.Background(), extended)
	if result != consumer.ConsumeRetryLater {
		t.Fatalf("consume result = %v, want ConsumeRetryLater", result)
	}
	if callbackErr != nil {
		t.Fatalf("callback error = %v, want nil so SDK does not format the shared message batch", callbackErr)
	}

	rawLog := strings.TrimSpace(output.String())
	var record map[string]any
	if err := json.Unmarshal([]byte(rawLog), &record); err != nil {
		t.Fatalf("decode safe consume log %q: %v", rawLog, err)
	}
	for key, want := range map[string]string{
		"msg":            "rocketmq_consume_handler_failed",
		"consumer_group": "group-safe-log",
		"topic":          "topic-safe-log",
		"tag":            "tag-safe-log",
		"keys":           "event-safe-log",
		"msg_id":         "msg-safe-log",
		"error":          handlerErr.Error(),
	} {
		if got := record[key]; got != want {
			t.Errorf("log field %s = %#v, want %q", key, got, want)
		}
	}
	if got := record["reconsume_times"]; got != float64(3) {
		t.Errorf("log field reconsume_times = %#v, want 3", got)
	}
	for _, forbidden := range []string{"sensitive-body-must-not-be-logged", "private-property", "secret-value-must-not-be-logged"} {
		if strings.Contains(rawLog, forbidden) {
			t.Errorf("safe consume log leaked forbidden message data %q: %s", forbidden, rawLog)
		}
	}
}
