package app

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"sub2api-cn-relay-manager/internal/store/sqlite"
)

func TestAPIProxyRouteChatCompletionsReturnsResolveAndForward(t *testing.T) {
	t.Parallel()

	handler := NewAPIHandler("secret-token", ActionSet{
		ProxyRouteChatCompletions: func(_ context.Context, req ProxyRouteChatCompletionsRequest) (ProxyRouteChatCompletionsResult, error) {
			if req.LogicalGroupID != "gpt-shared" {
				t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
			}
			if req.GatewayAPIKey != "gateway-key" {
				t.Fatalf("GatewayAPIKey = %q, want gateway-key", req.GatewayAPIKey)
			}
			return ProxyRouteChatCompletionsResult{
				Resolve: ResolveRouteInfo{
					RequestID:      "req-proxy-1",
					Backend:        "memory",
					LogicalGroupID: req.LogicalGroupID,
					PublicModel:    req.PublicModel,
					StickyKey:      "lg:gpt-shared:m:gpt-5.4:conv:conv-1",
					StickyHit:      false,
					StickyAction:   "bind",
					RouteID:        "asxs",
					ShadowGroupID:  "gpt-shared__asxs",
					ShadowHostID:   "remote43",
					ShadowModel:    "gpt-5.4-asxs",
				},
				Forward: RouteChatCompletionsForwardInfo{
					OK:             true,
					HostID:         "remote43",
					HostBaseURL:    "https://sub2api.example.com",
					ShadowGroupID:  "gpt-shared__asxs",
					ShadowModel:    "gpt-5.4-asxs",
					UpstreamPath:   "/v1/chat/completions",
					UpstreamStatus: 200,
					LatencyMS:      12,
					ContentType:    "application/json",
					Response: map[string]any{
						"id": "chatcmpl_proxy",
					},
				},
			}, nil
		},
	})

	request := httptestRequest(t, http.MethodPost, "/api/routing/proxy/chat/completions", map[string]any{
		"logical_group_id": "gpt-shared",
		"public_model":     "gpt-5.4",
		"scope":            "conversation",
		"subject_id":       "conv-1",
		"gateway_api_key":  "gateway-key",
		"sync":             true,
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "resolve.route_id", "asxs")
	assertJSONContains(t, response.Body().Bytes(), "forward.shadow_model", "gpt-5.4-asxs")
	assertJSONContains(t, response.Body().Bytes(), "forward.upstream_status", float64(200))
}

func TestAPIRouteChatCompletionsReturnsFormalResult(t *testing.T) {
	t.Parallel()

	handler := NewAPIHandler("secret-token", ActionSet{
		RouteChatCompletions: func(_ context.Context, req RouteChatCompletionsRequest) (RouteChatCompletionsResult, error) {
			if req.LogicalGroupID != "gpt-shared" {
				t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
			}
			if req.Model != "gpt-5.4" {
				t.Fatalf("Model = %q, want gpt-5.4", req.Model)
			}
			return RouteChatCompletionsResult{
				RequestID:      "req-route-1",
				Backend:        "memory",
				LogicalGroupID: req.LogicalGroupID,
				Model:          req.Model,
				Scope:          req.Scope,
				SubjectID:      req.SubjectID,
				StickyKey:      "lg:gpt-shared:m:gpt-5.4:conv:conv-1",
				StickyHit:      false,
				StickyAction:   "bind",
				FallbackUsed:   true,
				SelectedRoute: RouteChatCompletionsRouteInfo{
					RouteID:       "asxs",
					RouteName:     "ASXS",
					ShadowHostID:  "remote43",
					ShadowGroupID: "9",
					ShadowModel:   "gpt-5.4",
					Priority:      10,
					Weight:        100,
				},
				Forward: RouteChatCompletionsForwardInfo{
					OK:             true,
					HostID:         "remote43",
					HostBaseURL:    "https://sub2api.example.com",
					ShadowGroupID:  "9",
					ShadowModel:    "gpt-5.4",
					UpstreamPath:   "/v1/chat/completions",
					UpstreamStatus: 200,
					LatencyMS:      12,
					ContentType:    "application/json",
				},
			}, nil
		},
	})

	request := httptestRequest(t, http.MethodPost, "/api/routing/chat/completions", map[string]any{
		"logical_group_id": "gpt-shared",
		"model":            "gpt-5.4",
		"scope":            "conversation",
		"subject_id":       "conv-1",
		"gateway_api_key":  "gateway-key",
		"sync":             true,
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "selected_route.route_id", "asxs")
	assertJSONContains(t, response.Body().Bytes(), "selected_route.shadow_model", "gpt-5.4")
	assertJSONContains(t, response.Body().Bytes(), "fallback_used", true)
	assertJSONContains(t, response.Body().Bytes(), "forward.upstream_status", float64(200))
}

func TestNewActionSetProxyRouteChatCompletionsFlow(t *testing.T) {
	t.Parallel()

	var (
		gotAuthHeader string
		gotModel      string
		gotPrompt     string
	)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/v1/chat/completions" {
			t.Fatalf("URL.Path = %q, want /v1/chat/completions", r.URL.Path)
		}
		gotAuthHeader = r.Header.Get("Authorization")

		var payload struct {
			Model    string `json:"model"`
			Messages []struct {
				Role    string `json:"role"`
				Content string `json:"content"`
			} `json:"messages"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("json.Decode() error = %v", err)
		}
		gotModel = payload.Model
		if len(payload.Messages) > 0 {
			gotPrompt = payload.Messages[0].Content
		}

		writeJSON(w, http.StatusOK, map[string]any{
			"id":     "chatcmpl_proxy",
			"object": "chat.completion",
			"choices": []map[string]any{
				{
					"index": 0,
					"message": map[string]any{
						"role":    "assistant",
						"content": "pong",
					},
				},
			},
		})
	}))
	defer server.Close()

	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-proxy.db")) + "?_busy_timeout=5000"
	actions := NewActionSet(dsn)
	ctx := context.Background()

	store, err := sqlite.Open(ctx, dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	if _, err := store.Hosts().Create(ctx, sqlite.Host{
		HostID:      "remote43",
		BaseURL:     server.URL,
		HostVersion: "0.1.126",
		AuthType:    "apikey",
		AuthToken:   "host-admin-token",
	}); err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
		LogicalGroupID:         "gpt-shared",
		DisplayName:            "GPT Shared",
		Status:                 "active",
		RoutePolicy:            "priority",
		StickyMode:             "conversation_preferred",
		ConversationTTLSeconds: 1200,
		UserModelTTLSeconds:    600,
		FailoverThreshold:      2,
		CooldownSeconds:        300,
	}); err != nil {
		t.Fatalf("CreateLogicalGroup() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupModel() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID:      "gpt-shared",
		RouteID:             "asxs",
		Name:                "ASXS",
		Status:              "active",
		Priority:            10,
		ShadowGroupID:       "gpt-shared__asxs",
		ShadowHostID:        "remote43",
		UpstreamBaseURLHint: "https://api.asxs.top/v1",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "gpt-shared",
		RouteID:        "asxs",
		PublicModel:    "gpt-5.4",
		ShadowModel:    "gpt-5.4-asxs",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel() error = %v", err)
	}

	result, err := actions.ProxyRouteChatCompletions(ctx, ProxyRouteChatCompletionsRequest{
		RequestID:      "req-proxy-1",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-1",
		GatewayAPIKey:  "gateway-key",
		Sync:           true,
	})
	if err != nil {
		t.Fatalf("ProxyRouteChatCompletions() error = %v", err)
	}
	if gotAuthHeader != "Bearer gateway-key" {
		t.Fatalf("Authorization header = %q, want Bearer gateway-key", gotAuthHeader)
	}
	if gotModel != "gpt-5.4-asxs" {
		t.Fatalf("forwarded model = %q, want gpt-5.4-asxs", gotModel)
	}
	if gotPrompt != "ping" {
		t.Fatalf("forwarded prompt = %q, want ping", gotPrompt)
	}
	if result.Resolve.RouteID != "asxs" || result.Resolve.ShadowModel != "gpt-5.4-asxs" {
		t.Fatalf("Resolve = %+v, want selected asxs route with shadow model", result.Resolve)
	}
	if !result.Forward.OK || result.Forward.UpstreamStatus != http.StatusOK {
		t.Fatalf("Forward = %+v, want successful 200 forward", result.Forward)
	}
	if result.Forward.HostID != "remote43" || result.Forward.HostBaseURL != server.URL {
		t.Fatalf("Forward = %+v, want host remote43 and server URL", result.Forward)
	}

	decisions, err := actions.ListRouteDecisionLogs(ctx, ListRouteDecisionLogsRequest{
		RequestID: "req-proxy-1",
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("ListRouteDecisionLogs() error = %v", err)
	}
	if len(decisions) != 2 {
		t.Fatalf("ListRouteDecisionLogs() len = %d, want 2", len(decisions))
	}
	if decisions[0].UpstreamStatus != http.StatusOK || decisions[0].SelectedRouteID != "asxs" {
		t.Fatalf("latest decision log = %+v, want upstream_status 200 on asxs", decisions[0])
	}
	if decisions[1].UpstreamStatus != 0 {
		t.Fatalf("initial decision log = %+v, want upstream_status 0 before forward", decisions[1])
	}
}

func TestNewActionSetRouteChatCompletionsFlow(t *testing.T) {
	t.Parallel()

	var (
		gotAuthHeader string
		gotModel      string
		gotPrompt     string
	)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/v1/chat/completions" {
			t.Fatalf("URL.Path = %q, want /v1/chat/completions", r.URL.Path)
		}
		gotAuthHeader = r.Header.Get("Authorization")

		var payload struct {
			Model    string `json:"model"`
			Messages []struct {
				Role    string `json:"role"`
				Content string `json:"content"`
			} `json:"messages"`
		}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("json.Decode() error = %v", err)
		}
		gotModel = payload.Model
		if len(payload.Messages) > 0 {
			gotPrompt = payload.Messages[0].Content
		}

		writeJSON(w, http.StatusOK, map[string]any{
			"id":     "chatcmpl_route",
			"object": "chat.completion",
			"choices": []map[string]any{
				{
					"index": 0,
					"message": map[string]any{
						"role":    "assistant",
						"content": "pong-route",
					},
				},
			},
		})
	}))
	defer server.Close()

	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-formal.db")) + "?_busy_timeout=5000"
	actions := NewActionSet(dsn)
	ctx := context.Background()

	store, err := sqlite.Open(ctx, dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	if _, err := store.Hosts().Create(ctx, sqlite.Host{
		HostID:      "remote43",
		BaseURL:     server.URL,
		HostVersion: "0.1.126",
		AuthType:    "apikey",
		AuthToken:   "host-admin-token",
	}); err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
		LogicalGroupID:         "gpt-shared",
		DisplayName:            "GPT Shared",
		Status:                 "active",
		RoutePolicy:            "priority",
		StickyMode:             "conversation_preferred",
		ConversationTTLSeconds: 1200,
		UserModelTTLSeconds:    600,
		FailoverThreshold:      2,
		CooldownSeconds:        300,
	}); err != nil {
		t.Fatalf("CreateLogicalGroup() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupModel() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID:      "gpt-shared",
		RouteID:             "asxs",
		Name:                "ASXS",
		Status:              "active",
		Priority:            10,
		ShadowGroupID:       "gpt-shared__asxs",
		ShadowHostID:        "remote43",
		UpstreamBaseURLHint: "https://api.asxs.top/v1",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "gpt-shared",
		RouteID:        "asxs",
		PublicModel:    "gpt-5.4",
		ShadowModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel() error = %v", err)
	}

	result, err := actions.RouteChatCompletions(ctx, RouteChatCompletionsRequest{
		RequestID:      "req-route-1",
		LogicalGroupID: "gpt-shared",
		Model:          "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-1",
		GatewayAPIKey:  "gateway-key",
		Sync:           true,
	})
	if err != nil {
		t.Fatalf("RouteChatCompletions() error = %v", err)
	}
	if gotAuthHeader != "Bearer gateway-key" {
		t.Fatalf("Authorization header = %q, want Bearer gateway-key", gotAuthHeader)
	}
	if gotModel != "gpt-5.4" {
		t.Fatalf("forwarded model = %q, want gpt-5.4", gotModel)
	}
	if gotPrompt != "ping" {
		t.Fatalf("forwarded prompt = %q, want ping", gotPrompt)
	}
	if result.SelectedRoute.RouteID != "asxs" || result.SelectedRoute.ShadowModel != "gpt-5.4" {
		t.Fatalf("SelectedRoute = %+v, want asxs route with shadow model gpt-5.4", result.SelectedRoute)
	}
	if !result.Forward.OK || result.Forward.UpstreamStatus != http.StatusOK {
		t.Fatalf("Forward = %+v, want successful 200 forward", result.Forward)
	}
	if result.Model != "gpt-5.4" || result.RequestID != "req-route-1" {
		t.Fatalf("RouteChatCompletions() = %+v, want model gpt-5.4 and request id req-route-1", result)
	}
}

func TestNewActionSetProxyRouteChatCompletionsManagedSubscriptionFlow(t *testing.T) {
	t.Parallel()

	var (
		gotAuthHeader string
		gotModel      string
	)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/users?"):
			_, _ = w.Write([]byte(`{"data":{"items":[]}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users":
			_, _ = w.Write([]byte(`{"data":{"id":84,"email":"relay-sub-managed-user@sub2api.local"}}`))
		case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/users/84":
			_, _ = w.Write([]byte(`{"data":{"id":84}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users/84/balance":
			_, _ = w.Write([]byte(`{"data":{"id":84}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/subscriptions/assign":
			_, _ = w.Write([]byte(`{"data":{"id":401}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/auth/login":
			_, _ = w.Write([]byte(`{"data":{"access_token":"user-jwt"}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/keys":
			_, _ = w.Write([]byte(`{"data":{"id":501,"key":"sk-relay-key","name":"managed-key"}}`))
		case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/api-keys/501":
			_, _ = w.Write([]byte(`{"data":{"api_key":{"id":501}}}`))
		case r.Method == http.MethodPost && r.URL.Path == "/v1/chat/completions":
			gotAuthHeader = r.Header.Get("Authorization")
			var payload struct {
				Model string `json:"model"`
			}
			if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
				t.Fatalf("json.Decode() error = %v", err)
			}
			gotModel = payload.Model
			writeJSON(w, http.StatusOK, map[string]any{
				"id": "chatcmpl_proxy_managed",
				"choices": []map[string]any{
					{
						"message": map[string]any{
							"role":    "assistant",
							"content": "pong-managed",
						},
					},
				},
			})
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer server.Close()

	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-proxy-managed.db")) + "?_busy_timeout=5000"
	actions := NewActionSet(dsn)
	ctx := context.Background()

	store, err := sqlite.Open(ctx, dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	hostID, err := store.Hosts().Create(ctx, sqlite.Host{
		HostID:      "remote43-managed",
		BaseURL:     server.URL,
		HostVersion: "0.1.126",
		AuthType:    "bearer",
		AuthToken:   "host-admin-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packID, err := store.Packs().Create(ctx, sqlite.Pack{
		PackID:       "managed-pack",
		Version:      "1.0.0",
		Checksum:     "sha256-managed-pack",
		Vendor:       "tksea",
		ManifestJSON: "{}",
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerID, err := store.Providers().Create(ctx, sqlite.Provider{
		PackID:      packID,
		ProviderID:  "managed-provider",
		DisplayName: "Managed Provider",
		BaseURL:     "https://api.asxs.top/v1",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{
		HostID:       hostID,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         "strict",
		BatchStatus:  "succeeded",
		AccessStatus: "subscription_ready",
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
		LogicalGroupID:         "gpt-shared-managed",
		DisplayName:            "GPT Shared Managed",
		Status:                 "active",
		RoutePolicy:            "priority",
		StickyMode:             "conversation_preferred",
		ConversationTTLSeconds: 1200,
		UserModelTTLSeconds:    600,
		FailoverThreshold:      2,
		CooldownSeconds:        300,
	}); err != nil {
		t.Fatalf("CreateLogicalGroup() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
		LogicalGroupID: "gpt-shared-managed",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupModel() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID:      "gpt-shared-managed",
		RouteID:             "asxs-managed",
		Name:                "ASXS Managed",
		Status:              "active",
		Priority:            10,
		ShadowGroupID:       "101",
		ShadowHostID:        "remote43-managed",
		UpstreamBaseURLHint: "https://api.asxs.top/v1",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "gpt-shared-managed",
		RouteID:        "asxs-managed",
		PublicModel:    "gpt-5.4",
		ShadowModel:    "gpt-5.4-asxs",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel() error = %v", err)
	}
	if _, err := store.ManagedResources().Create(ctx, sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostID,
		ResourceType:   "group",
		HostResourceID: "101",
		ResourceName:   "shadow-group-asxs",
	}); err != nil {
		t.Fatalf("ManagedResources().Create() error = %v", err)
	}

	result, err := actions.ProxyRouteChatCompletions(ctx, ProxyRouteChatCompletionsRequest{
		RequestID:          "req-proxy-managed-1",
		LogicalGroupID:     "gpt-shared-managed",
		PublicModel:        "gpt-5.4",
		Scope:              "conversation",
		SubjectID:          "conv-managed-1",
		SubscriptionUserID: "crm-user-1",
		Sync:               true,
	})
	if err != nil {
		t.Fatalf("ProxyRouteChatCompletions() error = %v", err)
	}
	if !strings.HasPrefix(gotAuthHeader, "Bearer sk-relay-") {
		t.Fatalf("Authorization header = %q, want Bearer sk-relay-*", gotAuthHeader)
	}
	if gotModel != "gpt-5.4-asxs" {
		t.Fatalf("forwarded model = %q, want gpt-5.4-asxs", gotModel)
	}
	if result.Forward.EffectiveGatewayKeySource != "managed_subscription" {
		t.Fatalf("EffectiveGatewayKeySource = %q, want managed_subscription", result.Forward.EffectiveGatewayKeySource)
	}
	if result.Forward.EffectiveGatewayKeyFingerprint == "" {
		t.Fatal("EffectiveGatewayKeyFingerprint = empty, want hashed managed key fingerprint")
	}
	if result.Forward.ManagedUserID != "84" {
		t.Fatalf("ManagedUserID = %q, want 84", result.Forward.ManagedUserID)
	}
}

func TestProxyChatCompletionToShadowHostReportsNon2xx(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusTooManyRequests, map[string]any{
			"error": map[string]any{
				"message": "rate limited",
			},
		})
	}))
	defer server.Close()

	info := proxyChatCompletionToShadowHost(context.Background(), server.URL, "gateway-key", "gpt-5.4-asxs", nil, 0, nil)
	if info.OK {
		t.Fatalf("proxyChatCompletionToShadowHost() = %+v, want non-ok result", info)
	}
	if info.UpstreamStatus != http.StatusTooManyRequests || info.ErrorClass != "gateway_rate_limited" {
		t.Fatalf("proxyChatCompletionToShadowHost() = %+v, want 429 gateway_rate_limited", info)
	}
	response, ok := info.Response.(map[string]any)
	if !ok || response["error"] == nil {
		t.Fatalf("proxyChatCompletionToShadowHost() response = %#v, want decoded json body", info.Response)
	}
}

func TestRouteProxyHelpers(t *testing.T) {
	t.Parallel()

	if got := normalizeProxyMaxTokens(0); got != 8 {
		t.Fatalf("normalizeProxyMaxTokens(0) = %d, want 8", got)
	}
	if got := normalizeProxyTemperature(nil); got != 0 {
		t.Fatalf("normalizeProxyTemperature(nil) = %v, want 0", got)
	}
	if got := normalizeProxyChatMessages(nil); len(got) != 1 || got[0]["content"] != "ping" {
		t.Fatalf("normalizeProxyChatMessages(nil) = %#v, want default ping message", got)
	}
	if got := classifyProxyUpstreamStatus(http.StatusForbidden); got != "gateway_auth_error" {
		t.Fatalf("classifyProxyUpstreamStatus(403) = %q, want gateway_auth_error", got)
	}
	if _, err := joinRouteProxyPath("://bad-url", routeChatCompletionsPath); err == nil {
		t.Fatal("joinRouteProxyPath(invalid) error = nil, want error")
	}
	if got := resolveProxyUserKey(ProxyRouteChatCompletionsRequest{Scope: "user", SubjectID: "user-1"}); got != "user-1" {
		t.Fatalf("resolveProxyUserKey(user) = %q, want user-1", got)
	}
	if got := resolveProxyConversationKey(ProxyRouteChatCompletionsRequest{Scope: "conversation", SubjectID: "conv-1"}); got != "conv-1" {
		t.Fatalf("resolveProxyConversationKey(conversation) = %q, want conv-1", got)
	}
}

func TestResolveShadowGroupHostResourceID(t *testing.T) {
	t.Parallel()

	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-proxy-helper.db")) + "?_busy_timeout=5000"
	ctx := context.Background()
	store, err := sqlite.Open(ctx, dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	hostID, err := store.Hosts().Create(ctx, sqlite.Host{
		HostID:      "helper-host",
		BaseURL:     "https://helper.example.com",
		HostVersion: "0.1.126",
		AuthType:    "bearer",
		AuthToken:   "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	hostRow, err := store.Hosts().GetByID(ctx, hostID)
	if err != nil {
		t.Fatalf("Hosts().GetByID() error = %v", err)
	}

	if got, err := resolveShadowGroupHostResourceID(ctx, store, hostRow, nil, "101"); err != nil || got != "101" {
		t.Fatalf("resolveShadowGroupHostResourceID(numeric) = (%q, %v), want 101", got, err)
	}

	packID, err := store.Packs().Create(ctx, sqlite.Pack{
		PackID:       "helper-pack",
		Version:      "1.0.0",
		Checksum:     "sha256-helper",
		Vendor:       "tksea",
		ManifestJSON: "{}",
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerID, err := store.Providers().Create(ctx, sqlite.Provider{
		PackID:      packID,
		ProviderID:  "helper-provider",
		DisplayName: "Helper Provider",
		BaseURL:     "https://helper.example.com/v1",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{
		HostID:       hostID,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         "strict",
		BatchStatus:  "succeeded",
		AccessStatus: "subscription_ready",
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	if _, err := store.ManagedResources().Create(ctx, sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostID,
		ResourceType:   "group",
		HostResourceID: "202",
		ResourceName:   "shadow-group-name",
	}); err != nil {
		t.Fatalf("ManagedResources().Create() error = %v", err)
	}
	if got, err := resolveShadowGroupHostResourceID(ctx, store, hostRow, nil, "202"); err != nil || got != "202" {
		t.Fatalf("resolveShadowGroupHostResourceID(store identity) = (%q, %v), want 202", got, err)
	}
}

func TestResolveShadowGroupHostResourceIDFallsBackToHostList(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/groups"):
			_, _ = w.Write([]byte(`{"data":[{"id":"303","name":"shadow-group-remote"}]}`))
		case strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/channels"):
			_, _ = w.Write([]byte(`{"data":[]}`))
		case strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/payment/plans"):
			_, _ = w.Write([]byte(`{"data":[]}`))
		case strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/accounts"):
			_, _ = w.Write([]byte(`{"data":{"items":[],"pages":1}}`))
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer server.Close()

	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-proxy-fallback.db")) + "?_busy_timeout=5000"
	ctx := context.Background()
	store, err := sqlite.Open(ctx, dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	hostID, err := store.Hosts().Create(ctx, sqlite.Host{
		HostID:      "fallback-host",
		BaseURL:     server.URL,
		HostVersion: "0.1.126",
		AuthType:    "bearer",
		AuthToken:   "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	hostRow, err := store.Hosts().GetByID(ctx, hostID)
	if err != nil {
		t.Fatalf("Hosts().GetByID() error = %v", err)
	}
	hostClient, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "bearer", Token: "host-token"})
	if err != nil {
		t.Fatalf("newSub2APIClient() error = %v", err)
	}

	got, err := resolveShadowGroupHostResourceID(ctx, store, hostRow, hostClient, "shadow-group-remote")
	if err != nil {
		t.Fatalf("resolveShadowGroupHostResourceID(host fallback) error = %v", err)
	}
	if got != "303" {
		t.Fatalf("resolveShadowGroupHostResourceID(host fallback) = %q, want 303", got)
	}
}

func TestAPIProxyRouteChatCompletionsRejectsMissingGatewayAndSubscriptionUser(t *testing.T) {
	t.Parallel()

	handler := NewAPIHandler("secret-token", ActionSet{
		ProxyRouteChatCompletions: buildProxyRouteChatCompletionsAction("file::memory:?cache=shared", func(context.Context, ResolveRouteRequest) (ResolveRouteInfo, error) {
			t.Fatal("ResolveRoute should not be called when auth inputs are missing")
			return ResolveRouteInfo{}, nil
		}, newLazyRouteLogWriter("file::memory:?cache=shared")),
	})

	request := httptestRequest(t, http.MethodPost, "/api/routing/proxy/chat/completions", map[string]any{
		"logical_group_id": "gpt-shared",
		"public_model":     "gpt-5.4",
		"scope":            "conversation",
		"subject_id":       "conv-1",
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
	assertJSONContains(t, response.Body().Bytes(), "error.message", "gateway_api_key or subscription_user_id is required")
}
