package innerauth

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"unicode/utf8"

	"github.com/mem9-ai/drive9/pkg/logger"
	"github.com/mem9-ai/drive9/pkg/metrics"
	"github.com/mem9-ai/drive9/pkg/tenant"
	"github.com/mem9-ai/drive9/pkg/traceid"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"
)

func TestClientSendsAccountProviderRequestsWithBasicAuth(t *testing.T) {
	tests := []struct {
		name     string
		path     string
		response string
		call     func(context.Context, *Client) error
		wantBody map[string]any
	}{
		{
			name:     "user authn",
			path:     "/inner/account-provider/api/v1/user/authn",
			response: `{"check_result":true,"user_info":{"id":123}}`,
			call: func(ctx context.Context, client *Client) error {
				got, err := client.UserAuthn(ctx, "header.payload.signature")
				if err == nil && got.UserID != 123 {
					return fmt.Errorf("user ID = %d, want 123", got.UserID)
				}
				return err
			},
			wantBody: map[string]any{"auth0_token": "header.payload.signature"},
		},
		{
			name:     "user authz",
			path:     "/inner/account-provider/api/v1/user/authz",
			response: `{"has_permission":true}`,
			call: func(ctx context.Context, client *Client) error {
				return client.UserAuthz(ctx, 123, "/v1/tenants/42", "/v1/tenants/{id}")
			},
			wantBody: map[string]any{
				"user_id":      float64(123),
				"req_uri":      "/v1/tenants/42",
				"resource_uri": "/v1/tenants/{id}",
			},
		},
		{
			name:     "api key authn",
			path:     "/inner/account-provider/api/v1/api-key/authn",
			response: `{"public_key":"public","auth_ret":true,"resource_infos":[{"scope_type":"ORG","resource_id":456,"role":"org:owner","role_id":30}]}`,
			call: func(ctx context.Context, client *Client) error {
				got, err := client.APIKeyAuthn(ctx, "encoded-key")
				if err == nil && (got.PublicKey != "public" || got.OrganizationID != 456) {
					return fmt.Errorf("identity = %#v, want public key public and organization 456", got)
				}
				return err
			},
			wantBody: map[string]any{"api_key": "encoded-key"},
		},
		{
			name:     "api key authz",
			path:     "/inner/account-provider/api/v1/api-key/authz",
			response: `{"auth_ret":true}`,
			call: func(ctx context.Context, client *Client) error {
				return client.APIKeyAuthz(ctx, "public", "/v1/tenants/42", "/v1/tenants/{id}")
			},
			wantBody: map[string]any{
				"public_key":   "public",
				"req_uri":      "/v1/tenants/42",
				"resource_uri": "/v1/tenants/{id}",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Method != http.MethodPost {
					t.Errorf("method = %s, want POST", r.Method)
				}
				if r.URL.Path != tt.path {
					t.Errorf("path = %s, want %s", r.URL.Path, tt.path)
				}
				username, password, ok := r.BasicAuth()
				if !ok || username != "drive9" || password != "inner-secret" {
					t.Errorf("Basic Auth = (%q, %q, %t), want configured credentials", username, password, ok)
				}
				if got := r.Header.Get("Content-Type"); got != "application/json" {
					t.Errorf("Content-Type = %q, want application/json", got)
				}
				var gotBody map[string]any
				if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
					t.Errorf("decode body: %v", err)
				}
				if fmt.Sprint(gotBody) != fmt.Sprint(tt.wantBody) {
					t.Errorf("body = %#v, want %#v", gotBody, tt.wantBody)
				}
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write([]byte(tt.response))
			}))
			t.Cleanup(server.Close)

			client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
			if err != nil {
				t.Fatalf("NewClient: %v", err)
			}
			if err := tt.call(context.Background(), client); err != nil {
				t.Fatalf("call: %v", err)
			}
		})
	}
}

func TestClientLogsInnerHTTPFailureWithTraceAndMessage(t *testing.T) {
	core, recorded := observer.New(zap.WarnLevel)
	previousLogger := logger.L()
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-Trace-ID", "account-trace-456")
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusForbidden)
		_, _ = w.Write([]byte(`{"auth_ret":false,"message":"access scope is not matched assigned scopes public-key-secret inner-token-secret"}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-token-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	ctx := traceid.With(context.Background(), "drive9-trace-123")
	err = client.APIKeyAuthz(ctx, "public-key-secret", "/serverless/orgs/90032/clusters/create", "/serverless/orgs/{org_id}/clusters/create")
	if !errors.Is(err, ErrForbidden) {
		t.Fatalf("APIKeyAuthz error = %v, want %v", err, ErrForbidden)
	}

	entries := recorded.FilterMessage("tidbcloud_inner_api_request_failed").AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("failure log count = %d, want 1", len(entries))
	}
	fields := entries[0].ContextMap()
	if fields["method"] != http.MethodPost || fields["path"] != apiKeyAuthzPath || fields["status"] != int64(http.StatusForbidden) {
		t.Fatalf("failure fields = %#v", fields)
	}
	if fields["trace_id"] != "drive9-trace-123" || fields["upstream_trace_id"] != "account-trace-456" {
		t.Fatalf("trace fields = %#v", fields)
	}
	if fields["message"] != "access scope is not matched assigned scopes [REDACTED] [REDACTED]" {
		t.Fatalf("message field = %#v", fields["message"])
	}
	logged := fmt.Sprint(entries[0].Message, fields)
	for _, secret := range []string{"public-key-secret", "inner-token-secret"} {
		if strings.Contains(logged, secret) {
			t.Fatalf("failure log contains credential material %q", secret)
		}
	}
}

func TestRequestSecretValuesKeepsStringSecretsWhenSiblingFieldsAreNonString(t *testing.T) {
	raw := []byte(`{"api_key":"api-secret","user_id":123,"public_key":"public-secret","auth0_token":"jwt-secret"}`)
	got := requestSecretValues(raw)
	want := []string{"api-secret", "public-secret", "jwt-secret"}
	if fmt.Sprint(got) != fmt.Sprint(want) {
		t.Fatalf("requestSecretValues = %#v, want %#v", got, want)
	}
}

func TestClientLogsSemanticUserAuthnDenialMessage(t *testing.T) {
	core, recorded := observer.New(zap.WarnLevel)
	previousLogger := logger.L()
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"check_result":false,"check_message":"JWT session is invalid"}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-token", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	_, err = client.UserAuthn(traceid.With(context.Background(), "user-authn-denied-trace"), "header.payload.signature")
	if !errors.Is(err, ErrUnauthenticated) {
		t.Fatalf("UserAuthn error = %v, want %v", err, ErrUnauthenticated)
	}
	entries := recorded.FilterMessage("tidbcloud_inner_api_request_failed").AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("failure log count = %d, want 1", len(entries))
	}
	fields := entries[0].ContextMap()
	if fields["status"] != int64(http.StatusOK) || fields["result"] != "denied" || fields["message"] != "JWT session is invalid" {
		t.Fatalf("failure fields = %#v", fields)
	}
}

func TestClientAPIKeyAuthnReturnsScopesAndDerivesOrganization(t *testing.T) {
	core, recorded := observer.New(zap.InfoLevel)
	previousLogger := logger.L()
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != apiKeyAuthnPath {
			t.Fatalf("path = %q, want %q", r.URL.Path, apiKeyAuthnPath)
		}
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("X-Trace-ID", "account-iam-trace")
		_, _ = w.Write([]byte(`{"public_key":"public","auth_ret":true,"resource_infos":[{"scope_type":"ORG","resource_id":60205,"role":"org:member","role_id":30},{"scope_type":"PROJECT","resource_id":103547,"role":"project:owner","role_id":34},{"scope_type":"PROJECT","resource_id":103548,"role":"project:readonly","role_id":35}]}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	got, err := client.APIKeyAuthn(traceid.With(context.Background(), "drive9-iam-trace"), "encoded-key")
	if err != nil {
		t.Fatalf("APIKeyAuthn: %v", err)
	}
	if got.PublicKey != "public" || got.OrganizationID != 60205 {
		t.Fatalf("identity = %#v, want public key and organization 60205", got)
	}
	if len(got.ResourceInfos) != 3 || got.ResourceInfos[1].ResourceID != 103547 || got.ResourceInfos[2].Role != "project:readonly" {
		t.Fatalf("resource infos = %#v, want all active scopes", got.ResourceInfos)
	}
	entries := recorded.FilterMessage("tidbcloud_api_result").AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("IAM result log count = %d, want 1", len(entries))
	}
	fields := entries[0].ContextMap()
	if fields["mode"] != "inner" || fields["api"] != "iam" || fields["operation"] != "api_key_authn" ||
		fields["organization_id"] != uint64(60205) || fields["project_id"] != uint64(103547) || fields["project_role"] != "project:owner" ||
		fields["trace_id"] != "drive9-iam-trace" || fields["upstream_trace_id"] != "account-iam-trace" {
		t.Fatalf("IAM result fields = %#v", fields)
	}
}

func TestClientAPIKeyAuthnRejectsMissingOrganizationScope(t *testing.T) {
	core, recorded := observer.New(zap.ErrorLevel)
	previousLogger := logger.L()
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"public_key":"public","auth_ret":true,"resource_infos":[{"scope_type":"PROJECT","resource_id":103547,"role":"project:owner","role_id":34}]}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}
	ctx := traceid.With(context.Background(), "invalid-authn-trace")
	_, err = client.APIKeyAuthn(ctx, "encoded-key")
	if !errors.Is(err, ErrInvalidResponse) {
		t.Fatalf("APIKeyAuthn error = %v, want %v", err, ErrInvalidResponse)
	}
	if !strings.Contains(err.Error(), "organization scope is missing") {
		t.Fatalf("APIKeyAuthn error = %v, want organization scope reason", err)
	}
	entries := recorded.FilterMessage("tidbcloud_api_result").AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("IAM error log count = %d, want 1", len(entries))
	}
	fields := entries[0].ContextMap()
	if fields["mode"] != "inner" || fields["api"] != "iam" || fields["operation"] != "api_key_authn" ||
		fields["result"] != "protocol_error" || fields["trace_id"] != "invalid-authn-trace" {
		t.Fatalf("IAM error fields = %#v", fields)
	}
}

func TestNewClientRequiresHTTPSExceptForLoopback(t *testing.T) {
	tests := []struct {
		name    string
		baseURL string
		wantErr bool
	}{
		{name: "production HTTPS", baseURL: "https://inner-central.prod.tidbcloud.com"},
		{name: "production HTTP", baseURL: "http://inner-central.prod.tidbcloud.com", wantErr: true},
		{name: "localhost HTTP", baseURL: "http://localhost:9000"},
		{name: "IPv4 loopback HTTP", baseURL: "http://127.0.0.1:9000"},
		{name: "IPv6 loopback HTTP", baseURL: "http://[::1]:9000"},
		{name: "non-HTTP scheme", baseURL: "ftp://127.0.0.1:9000", wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := NewClient(ClientConfig{BaseURL: tt.baseURL, Token: "inner-secret"})
			if (err != nil) != tt.wantErr {
				t.Fatalf("NewClient error = %v, want error = %t", err, tt.wantErr)
			}
		})
	}
}

func TestClientMapsAccountProviderStatus(t *testing.T) {
	tests := []struct {
		name   string
		status int
		body   string
		want   error
	}{
		{name: "bad request", status: http.StatusBadRequest, body: `{"message":"invalid"}`, want: ErrBadRequest},
		{name: "unauthenticated", status: http.StatusUnauthorized, body: `{"message":"credential-value-must-not-leak"}`, want: ErrUnauthenticated},
		{name: "forbidden", status: http.StatusForbidden, body: `{"message":"denied"}`, want: ErrForbidden},
		{name: "oversized forbidden", status: http.StatusForbidden, body: strings.Repeat("x", maxResponseBytes+1), want: ErrForbidden},
		{name: "server failure", status: http.StatusInternalServerError, body: `{"message":"failed"}`, want: ErrUpstreamUnavailable},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(tt.status)
				_, _ = w.Write([]byte(tt.body))
			}))
			t.Cleanup(server.Close)
			client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "password", HTTPClient: server.Client()})
			if err != nil {
				t.Fatalf("NewClient: %v", err)
			}

			_, err = client.UserAuthn(context.Background(), "jwt-value-must-not-leak")
			if !errors.Is(err, tt.want) {
				t.Fatalf("error = %v, want errors.Is(..., %v)", err, tt.want)
			}
			for _, secret := range []string{"jwt-value-must-not-leak", "credential-value-must-not-leak", "password"} {
				if strings.Contains(err.Error(), secret) {
					t.Fatalf("error leaked secret %q: %v", secret, err)
				}
			}
		})
	}
}

func TestClientMapsNegativeSuccessPayloads(t *testing.T) {
	tests := []struct {
		name string
		path string
		body string
		call func(context.Context, *Client) error
		want error
	}{
		{
			name: "user authn false is unauthenticated",
			path: "/inner/account-provider/api/v1/user/authn",
			body: `{"check_result":false}`,
			call: func(ctx context.Context, client *Client) error {
				_, err := client.UserAuthn(ctx, "jwt")
				return err
			},
			want: ErrUnauthenticated,
		},
		{
			name: "api key authn false is unauthenticated",
			path: "/inner/account-provider/api/v1/api-key/authn",
			body: `{"auth_ret":false}`,
			call: func(ctx context.Context, client *Client) error {
				_, err := client.APIKeyAuthn(ctx, "key")
				return err
			},
			want: ErrUnauthenticated,
		},
		{
			name: "user authz false is forbidden",
			path: "/inner/account-provider/api/v1/user/authz",
			body: `{"has_permission":false}`,
			call: func(ctx context.Context, client *Client) error {
				return client.UserAuthz(ctx, 1, "/request", "/resource")
			},
			want: ErrForbidden,
		},
		{
			name: "api key authz false is forbidden",
			path: "/inner/account-provider/api/v1/api-key/authz",
			body: `{"auth_ret":false}`,
			call: func(ctx context.Context, client *Client) error {
				return client.APIKeyAuthz(ctx, "public", "/request", "/resource")
			},
			want: ErrForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.URL.Path != tt.path {
					t.Errorf("path = %s, want %s", r.URL.Path, tt.path)
				}
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write([]byte(tt.body))
			}))
			t.Cleanup(server.Close)
			client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "password", HTTPClient: server.Client()})
			if err != nil {
				t.Fatalf("NewClient: %v", err)
			}
			if err := tt.call(context.Background(), client); !errors.Is(err, tt.want) {
				t.Fatalf("error = %v, want errors.Is(..., %v)", err, tt.want)
			}
		})
	}
}

func TestClientRejectsSuccessfulAuthnWithoutCompleteIdentity(t *testing.T) {
	tests := []struct {
		name string
		path string
		body string
		call func(context.Context, *Client) error
	}{
		{
			name: "user missing non-zero ID",
			path: "/inner/account-provider/api/v1/user/authn",
			body: `{"check_result":true,"user_info":{"id":0}}`,
			call: func(ctx context.Context, client *Client) error {
				_, err := client.UserAuthn(ctx, "jwt")
				return err
			},
		},
		{
			name: "API key missing public key",
			path: "/inner/account-provider/api/v1/api-key/authn",
			body: `{"auth_ret":true,"resource_infos":[{"scope_type":"ORG","resource_id":42,"role":"org:owner","role_id":30}]}`,
			call: func(ctx context.Context, client *Client) error {
				_, err := client.APIKeyAuthn(ctx, "key")
				return err
			},
		},
		{
			name: "API key missing organization",
			path: "/inner/account-provider/api/v1/api-key/authn",
			body: `{"auth_ret":true,"public_key":"public","resource_infos":[{"scope_type":"PROJECT","resource_id":42,"role":"project:owner","role_id":34}]}`,
			call: func(ctx context.Context, client *Client) error {
				_, err := client.APIKeyAuthn(ctx, "key")
				return err
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.URL.Path != tt.path {
					t.Errorf("path = %s, want %s", r.URL.Path, tt.path)
				}
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write([]byte(tt.body))
			}))
			t.Cleanup(server.Close)
			client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "password", HTTPClient: server.Client()})
			if err != nil {
				t.Fatalf("NewClient: %v", err)
			}
			if err := tt.call(context.Background(), client); !errors.Is(err, ErrInvalidResponse) {
				t.Fatalf("error = %v, want errors.Is(..., %v)", err, ErrInvalidResponse)
			}
		})
	}
}

func TestClientDoesNotFollowRedirectsWithCredentials(t *testing.T) {
	targetCalled := false
	target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		targetCalled = true
	}))
	defer target.Close()

	source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Location", target.URL)
		w.WriteHeader(http.StatusTemporaryRedirect)
	}))
	defer source.Close()

	client, err := NewClient(ClientConfig{
		BaseURL: source.URL, Token: "inner-secret",
		HTTPClient: &http.Client{},
	})
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.UserAuthn(context.Background(), "caller-jwt")
	if !errors.Is(err, ErrUpstreamUnavailable) {
		t.Fatalf("UserAuthn error = %v, want upstream unavailable", err)
	}
	if targetCalled {
		t.Fatal("redirect target received caller credentials")
	}
}

func TestClientGetsBillingPlanWithBasicAuth(t *testing.T) {
	core, recorded := observer.New(zap.InfoLevel)
	previousLogger := logger.L()
	logger.Set(zap.New(core))
	t.Cleanup(func() { logger.Set(previousLogger) })

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			t.Errorf("method = %s, want GET", r.Method)
		}
		if r.URL.EscapedPath() != "/inner/billing/tenants/60205/plans" {
			t.Errorf("path = %s, want inner Billing plan path", r.URL.EscapedPath())
		}
		username, password, ok := r.BasicAuth()
		if !ok || username != "drive9" || password != "inner-secret" {
			t.Errorf("Basic Auth = (%q, %q, %t), want configured credentials", username, password, ok)
		}
		if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Basic ") {
			t.Errorf("Authorization = %q, want only Basic Auth", got)
		}
		w.Header().Set("X-Trace-ID", "billing-upstream-trace")
		_, _ = w.Write([]byte(`{"tenant_id_str":"60205","effective_plan":"poc","poc_credits":"1.25"}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{
		BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client(),
	})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	plan, err := client.BillingPlan(traceid.With(context.Background(), "drive9-billing-trace"), 60205)
	if err != nil {
		t.Fatalf("BillingPlan: %v", err)
	}
	if plan.OrganizationID != "60205" || plan.IsFree {
		t.Fatalf("plan = %#v, want non-free organization 60205", plan)
	}
	entries := recorded.FilterMessage("tidbcloud_api_result").AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("Billing result log count = %d, want 1", len(entries))
	}
	fields := entries[0].ContextMap()
	if fields["mode"] != "inner" || fields["api"] != "billing" || fields["operation"] != "get_plan" ||
		fields["organization_id"] != uint64(60205) || fields["effective_plan"] != "poc" || fields["poc_credits"] != "1.25" ||
		fields["is_free"] != false || fields["trace_id"] != "drive9-billing-trace" || fields["upstream_trace_id"] != "billing-upstream-trace" {
		t.Fatalf("Billing result fields = %#v", fields)
	}
}

func TestClientRecordsInnerBillingAPIMetrics(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"tenant_id_str":"60205","effective_plan":"on_demand"}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if _, err := client.BillingPlan(context.Background(), 60205); err != nil {
		t.Fatalf("BillingPlan: %v", err)
	}
	recorder := httptest.NewRecorder()
	metrics.WritePrometheus(recorder)
	text := recorder.Body.String()
	for _, want := range []string{
		`drive9_tidbcloud_openapi_requests_total{api="billing",operation="get_plan",result="ok"}`,
		`drive9_tidbcloud_openapi_request_duration_seconds_count{api="billing",operation="get_plan"}`,
	} {
		if !strings.Contains(text, want) {
			t.Fatalf("missing inner Billing metric %q:\n%s", want, text)
		}
	}
}

func TestClientRecordsMalformedInnerBillingResponseAsProtocolError(t *testing.T) {
	before := innerAPIMetricValue(t, `drive9_tidbcloud_openapi_requests_total{api="billing",operation="get_plan",result="protocol_error"}`)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"tenant_id_str":`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if _, err := client.BillingPlan(context.Background(), 60205); !errors.Is(err, tenant.ErrTiDBCloudBillingResponseInvalid) {
		t.Fatalf("BillingPlan error = %v, want %v", err, tenant.ErrTiDBCloudBillingResponseInvalid)
	}
	after := innerAPIMetricValue(t, `drive9_tidbcloud_openapi_requests_total{api="billing",operation="get_plan",result="protocol_error"}`)
	if after != before+1 {
		t.Fatalf("protocol-error metric delta = %v, want 1", after-before)
	}
}

func TestClientRecordsInnerIAMAPIMetrics(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"check_result":true,"user_info":{"id":123}}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if _, err := client.UserAuthn(context.Background(), "header.payload.signature"); err != nil {
		t.Fatalf("UserAuthn: %v", err)
	}
	recorder := httptest.NewRecorder()
	metrics.WritePrometheus(recorder)
	text := recorder.Body.String()
	for _, want := range []string{
		`drive9_tidbcloud_openapi_requests_total{api="iam",operation="user_authn",result="ok"}`,
		`drive9_tidbcloud_openapi_request_duration_seconds_count{api="iam",operation="user_authn"}`,
	} {
		if !strings.Contains(text, want) {
			t.Fatalf("missing inner IAM metric %q:\n%s", want, text)
		}
	}
}

func TestClientRecordsSemanticInnerIAMDenial(t *testing.T) {
	before := innerAPIMetricValue(t, `drive9_tidbcloud_openapi_requests_total{api="iam",operation="user_authn",result="denied"}`)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"check_result":false}`))
	}))
	t.Cleanup(server.Close)
	client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "inner-secret", HTTPClient: server.Client()})
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}

	if _, err := client.UserAuthn(context.Background(), "header.payload.signature"); !errors.Is(err, ErrUnauthenticated) {
		t.Fatalf("UserAuthn error = %v, want %v", err, ErrUnauthenticated)
	}
	after := innerAPIMetricValue(t, `drive9_tidbcloud_openapi_requests_total{api="iam",operation="user_authn",result="denied"}`)
	if after != before+1 {
		t.Fatalf("denied metric delta = %v, want 1", after-before)
	}
}

func innerAPIMetricValue(t *testing.T, prefix string) float64 {
	t.Helper()
	recorder := httptest.NewRecorder()
	metrics.WritePrometheus(recorder)
	for _, line := range strings.Split(recorder.Body.String(), "\n") {
		if !strings.HasPrefix(line, prefix+" ") {
			continue
		}
		var value float64
		if _, err := fmt.Sscanf(strings.TrimPrefix(line, prefix+" "), "%f", &value); err != nil {
			t.Fatalf("parse metric %q: %v", line, err)
		}
		return value
	}
	return 0
}

func TestInnerAPIResultClassifiesDenialsAndFailures(t *testing.T) {
	tests := []struct {
		err  error
		want string
	}{
		{want: "ok"},
		{err: ErrUnauthenticated, want: "denied"},
		{err: ErrForbidden, want: "denied"},
		{err: ErrBadRequest, want: "client_error"},
		{err: ErrInvalidResponse, want: "protocol_error"},
		{err: context.Canceled, want: "canceled"},
		{err: context.DeadlineExceeded, want: "timeout"},
		{err: ErrUpstreamUnavailable, want: "upstream_error"},
	}
	for _, tt := range tests {
		if got := innerAPIResult(tt.err); got != tt.want {
			t.Errorf("innerAPIResult(%v) = %q, want %q", tt.err, got, tt.want)
		}
	}
}

func TestClientBillingPlanMapsStatusAndBoundsResponse(t *testing.T) {
	tests := []struct {
		name   string
		status int
		body   string
		want   error
	}{
		{name: "forbidden", status: http.StatusForbidden, body: `{"secret":"must-not-leak"}`, want: ErrForbidden},
		{name: "oversized forbidden", status: http.StatusForbidden, body: strings.Repeat("x", maxResponseBytes+1), want: ErrForbidden},
		{name: "server error", status: http.StatusInternalServerError, body: `{}`, want: ErrUpstreamUnavailable},
		{name: "oversized", status: http.StatusOK, body: strings.Repeat("x", maxResponseBytes+1), want: ErrInvalidResponse},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(tt.status)
				_, _ = w.Write([]byte(tt.body))
			}))
			t.Cleanup(server.Close)
			client, err := NewClient(ClientConfig{BaseURL: server.URL, Token: "password", HTTPClient: server.Client()})
			if err != nil {
				t.Fatal(err)
			}
			_, err = client.BillingPlan(context.Background(), 60205)
			if !errors.Is(err, tt.want) {
				t.Fatalf("error = %v, want %v", err, tt.want)
			}
			if strings.Contains(err.Error(), "must-not-leak") {
				t.Fatalf("error leaked response body: %v", err)
			}
		})
	}
}

func TestSafeInnerMessagePreservesUTF8WhenTruncated(t *testing.T) {
	message := safeInnerMessage(strings.Repeat("界", 200))
	if len(message) > 512 {
		t.Fatalf("message length = %d, want at most 512 bytes", len(message))
	}
	if !utf8.ValidString(message) {
		t.Fatalf("message is not valid UTF-8: %q", message)
	}
}

func makeJWT(payload string) string {
	return base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`)) + "." +
		base64.RawURLEncoding.EncodeToString([]byte(payload)) + ".c2lnbmF0dXJl"
}
