package logx

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"

	"hyapp/pkg/xerr"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func TestNormalizeConfigRejectsBadLevel(t *testing.T) {
	_, err := NormalizeConfig(Config{Level: "verbose"})
	if !errors.Is(err, ErrUnsupportedLevel) {
		t.Fatalf("NormalizeConfig error=%v, want ErrUnsupportedLevel", err)
	}
}

func TestSafePayloadRedactsSensitiveFieldsAndOmitsLargePayload(t *testing.T) {
	payload := safePayload([]byte(`{"username":"tom","password":"pw","nested":{"user_sig":"sig"}}`), 2048, false)
	values, ok := payload.(map[string]any)
	if !ok {
		t.Fatalf("payload type=%T, want map", payload)
	}
	if values["password"] != redactedValue {
		t.Fatalf("password was not redacted: %+v", values)
	}
	nested := values["nested"].(map[string]any)
	if nested["user_sig"] != redactedValue {
		t.Fatalf("nested user_sig was not redacted: %+v", nested)
	}

	large := safePayload([]byte(`{"token":"secret-token","data":"abcdef"}`), 8, true)
	largeMap, ok := large.(map[string]any)
	if !ok || largeMap["truncated"] != true {
		t.Fatalf("large payload=%+v, want truncated marker", large)
	}
	if encoded := toJSON(t, large); bytes.Contains(encoded, []byte("secret-token")) {
		t.Fatalf("truncated payload leaked sensitive value: %s", encoded)
	}
}

func TestHTTPMiddlewareUsesConfiguredBodyLoggingAndWarnLevel(t *testing.T) {
	var output bytes.Buffer
	if err := Init(Config{Service: "gateway-service", Env: "test", Level: "debug", Format: "json", IncludeRequestBody: true, IncludeResponseBody: true, MaxPayloadBytes: 2048, Output: &output}); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-1" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.WriteHeader(http.StatusBadRequest)
		_, _ = writer.Write([]byte(`{"code":"INVALID_ARGUMENT","token":"server-token"}`))
	}))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login?debug=1", bytes.NewBufferString(`{"username":"tom","password":"client-password"}`))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Forwarded-For", "203.0.113.10, 10.0.0.1")
	handler.ServeHTTP(httptest.NewRecorder(), req)

	entry := decodeOne(t, output.Bytes())
	if entry["level"] != "WARN" || entry["msg"] != "http_access" || entry["service"] != "gateway-service" {
		t.Fatalf("unexpected http log entry: %+v", entry)
	}
	if entry["request_id"] != "req-http-1" || entry["path"] != "/api/v1/auth/login" || entry["client_ip"] != "203.0.113.10" {
		t.Fatalf("missing access fields: %+v", entry)
	}
	request := entry["request"].(map[string]any)
	if request["password"] != redactedValue {
		t.Fatalf("request body was not redacted: %+v", request)
	}
	response := entry["response"].(map[string]any)
	if response["token"] != redactedValue {
		t.Fatalf("response body was not redacted: %+v", response)
	}
}

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

	handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-2" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		_, _ = writer.Write([]byte(`{"ok":true}`))
	}))
	handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/v1/ping", bytes.NewBufferString(`{"password":"client-password"}`)))

	entry := decodeOne(t, output.Bytes())
	if _, exists := entry["request"]; exists {
		t.Fatalf("request body should be omitted by default: %+v", entry)
	}
	if _, exists := entry["response"]; exists {
		t.Fatalf("response body should be omitted by default: %+v", entry)
	}
}

func TestUnaryServerInterceptorLevelsAndRequestMeta(t *testing.T) {
	var output bytes.Buffer
	if err := Init(Config{Service: "user-service", Env: "test", Format: "json", IncludeRequestBody: true, Output: &output}); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	interceptor := UnaryServerInterceptor("user-service")
	request := &userv1.GetUserRequest{Meta: &userv1.RequestMeta{RequestId: "req-grpc-1", AppCode: "lalu"}, UserId: 1001}
	_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.user.v1.UserService/GetUser"}, func(ctx context.Context, req any) (any, error) {
		return nil, status.Error(codes.InvalidArgument, "bad user id")
	})
	if err == nil {
		t.Fatal("interceptor should return handler error")
	}

	entry := decodeOne(t, output.Bytes())
	if entry["level"] != "WARN" || entry["msg"] != "grpc_access" || entry["request_id"] != "req-grpc-1" || entry["app_code"] != "lalu" {
		t.Fatalf("unexpected grpc log entry: %+v", entry)
	}
	if entry["grpc_code"] != "InvalidArgument" {
		t.Fatalf("grpc_code=%v, want InvalidArgument", entry["grpc_code"])
	}
	if entry["reason"] != "INVALID_ARGUMENT" || entry["error_message"] != "bad user id" {
		t.Fatalf("grpc error fields=%+v, want stable reason and service message", entry)
	}
}

// TestUnaryServerInterceptorLogsOriginalCauseForInternal 锁定 2026-07-12 排障改进：
// Internal 类错误客户端只见 "internal error"，但 grpc_access ERROR 日志必须带脱敏前的原始错误链。
func TestUnaryServerInterceptorLogsOriginalCauseForInternal(t *testing.T) {
	var output bytes.Buffer
	if err := Init(Config{Service: "room-service", Env: "test", Format: "json", Output: &output}); err != nil {
		t.Fatalf("Init failed: %v", err)
	}

	interceptor := UnaryServerInterceptor("room-service")
	request := &userv1.GetUserRequest{Meta: &userv1.RequestMeta{RequestId: "req-grpc-2", AppCode: "lalu"}, UserId: 1001}
	_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.room.v1.RoomCommandService/MicHeartbeat"}, func(ctx context.Context, req any) (any, error) {
		return nil, xerr.ToGRPCError(fmt.Errorf("refresh mic presence: %w", errors.New("redis: connection refused")))
	})
	if err == nil {
		t.Fatal("interceptor should return handler error")
	}
	if st, ok := status.FromError(err); !ok || st.Message() != "internal error" {
		t.Fatalf("client-facing error must stay sanitized: %v", err)
	}

	entry := decodeOne(t, output.Bytes())
	if entry["level"] != "ERROR" || entry["msg"] != "grpc_access" || entry["request_id"] != "req-grpc-2" {
		t.Fatalf("unexpected grpc log entry: %+v", entry)
	}
	if entry["error_message"] != "internal error" {
		t.Fatalf("error_message=%v, want sanitized message", entry["error_message"])
	}
	cause, _ := entry["error_cause"].(string)
	if !strings.Contains(cause, "refresh mic presence") || !strings.Contains(cause, "redis: connection refused") {
		t.Fatalf("error_cause=%q, want original error chain", cause)
	}
}

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

	interceptor := UnaryServerInterceptor("wallet-service")
	request := &walletv1.AdminCreditAssetRequest{
		CommandId:    "cmd-credit-1",
		TargetUserId: 42001,
		AssetType:    "COIN_SELLER_COIN",
		Amount:       80000,
		AppCode:      "lalu",
	}
	_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.wallet.v1.WalletService/AdminCreditAsset"}, func(ctx context.Context, req any) (any, error) {
		return &walletv1.AdminCreditAssetResponse{}, nil
	})
	if err != nil {
		t.Fatalf("interceptor returned error: %v", err)
	}

	entry := decodeOne(t, output.Bytes())
	if entry["app_code"] != "lalu" || entry["command_id"] != "cmd-credit-1" || entry["asset_type"] != "COIN_SELLER_COIN" {
		t.Fatalf("missing top-level trace fields: %+v", entry)
	}
	if _, exists := entry["request"]; exists {
		t.Fatalf("request body should remain omitted: %+v", entry)
	}
}

func decodeOne(t *testing.T, raw []byte) map[string]any {
	t.Helper()
	lines := bytes.Split(bytes.TrimSpace(raw), []byte("\n"))
	if len(lines) != 1 || len(lines[0]) == 0 {
		t.Fatalf("log lines=%q, want exactly one", raw)
	}
	var entry map[string]any
	if err := json.Unmarshal(lines[0], &entry); err != nil {
		t.Fatalf("decode log JSON failed: %v raw=%s", err, lines[0])
	}
	return entry
}

func toJSON(t *testing.T, value any) []byte {
	t.Helper()
	encoded, err := json.Marshal(value)
	if err != nil {
		t.Fatalf("json marshal failed: %v", err)
	}
	return encoded
}
