package app

import (
	"context"
	"net/http"
	"net/url"
	"path/filepath"
	"testing"
	"time"

	"sub2api-cn-relay-manager/internal/routing"
)

func TestAPIResolveRouteReturnsSelectedRoute(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ResolveRoute: func(_ context.Context, req ResolveRouteRequest) (ResolveRouteInfo, error) {
			if req.LogicalGroupID != "gpt-shared" {
				t.Fatalf("LogicalGroupID = %q, want gpt-shared", req.LogicalGroupID)
			}
			return ResolveRouteInfo{
				RequestID:      "req-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",
			}, nil
		},
	})

	request := httptestRequest(t, http.MethodPost, "/api/routing/resolve", map[string]any{
		"logical_group_id": "gpt-shared",
		"public_model":     "gpt-5.4",
		"scope":            "conversation",
		"subject_id":       "conv-1",
		"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(), "resolve.sticky_action", "bind")
}

func TestNewActionSetResolveRouteFlow(t *testing.T) {
	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-resolve.db")) + "?_busy_timeout=5000"
	actions := NewActionSet(dsn)
	ctx := context.Background()

	_, 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,
	})
	if 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:            20,
		ShadowGroupID:       "gpt-shared__asxs",
		ShadowHostID:        "remote43",
		UpstreamBaseURLHint: "https://api.asxs.top/v1",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute(asxs) 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(asxs) error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID:      "gpt-shared",
		RouteID:             "codex2api",
		Name:                "Codex2API",
		Status:              "active",
		Priority:            10,
		ShadowGroupID:       "gpt-shared__codex2api",
		ShadowHostID:        "remote43",
		UpstreamBaseURLHint: "https://www.codex2api.com/v1",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute(codex2api) error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "gpt-shared",
		RouteID:        "codex2api",
		PublicModel:    "gpt-5.4",
		ShadowModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel(codex2api) error = %v", err)
	}
	if _, err := actions.SetRouteFailure(ctx, SetRouteFailureRequest{
		RouteID:        "codex2api",
		FailureCount:   2,
		LastErrorClass: "timeout",
		TTLSeconds:     600,
	}); err != nil {
		t.Fatalf("SetRouteFailure(codex2api) error = %v", err)
	}

	first, err := actions.ResolveRoute(ctx, ResolveRouteRequest{
		RequestID:      "req-resolve-1",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-1",
		Sync:           true,
	})
	if err != nil {
		t.Fatalf("ResolveRoute(first) error = %v", err)
	}
	if first.RouteID != "asxs" || first.StickyHit || first.StickyAction != "bind" {
		t.Fatalf("ResolveRoute(first) = %+v, want asxs bind miss", first)
	}

	second, err := actions.ResolveRoute(ctx, ResolveRouteRequest{
		RequestID:      "req-resolve-2",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-1",
		Sync:           true,
	})
	if err != nil {
		t.Fatalf("ResolveRoute(second) error = %v", err)
	}
	if second.RouteID != "asxs" || !second.StickyHit || second.StickyAction != "hit" {
		t.Fatalf("ResolveRoute(second) = %+v, want asxs sticky hit", second)
	}

	sticky, err := actions.GetStickyBinding(ctx, GetStickyBindingRequest{
		Scope:          "conversation",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		SubjectID:      "conv-1",
	})
	if err != nil {
		t.Fatalf("GetStickyBinding() error = %v", err)
	}
	if sticky.RouteID != "asxs" {
		t.Fatalf("GetStickyBinding() = %+v, want route asxs", sticky)
	}

	decisions, err := actions.ListRouteDecisionLogs(ctx, ListRouteDecisionLogsRequest{
		LogicalGroupID: "gpt-shared",
		StickyKey:      first.StickyKey,
		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].StickyHit || decisions[1].StickyHit {
		t.Fatalf("ListRouteDecisionLogs() = %+v, want latest hit then miss", decisions)
	}
	if !decisions[1].FallbackUsed {
		t.Fatalf("ListRouteDecisionLogs()[1] = %+v, want fallback_used true", decisions[1])
	}

	stickyAudits, err := actions.ListRouteStickyAudit(ctx, ListRouteStickyAuditRequest{
		StickyKey: first.StickyKey,
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("ListRouteStickyAudit() error = %v", err)
	}
	if len(stickyAudits) != 2 {
		t.Fatalf("ListRouteStickyAudit() len = %d, want 2", len(stickyAudits))
	}
	if stickyAudits[0].Action != "hit" || stickyAudits[1].Action != "bind" {
		t.Fatalf("ListRouteStickyAudit() = %+v, want latest hit then bind", stickyAudits)
	}

	failovers, err := actions.ListRouteFailoverEvents(ctx, ListRouteFailoverEventsRequest{
		RequestID: "req-resolve-1",
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("ListRouteFailoverEvents() error = %v", err)
	}
	if len(failovers) != 1 {
		t.Fatalf("ListRouteFailoverEvents() len = %d, want 1", len(failovers))
	}
	if failovers[0].FromRouteID != "codex2api" || failovers[0].ToRouteID != "asxs" || failovers[0].FailureCount != 2 {
		t.Fatalf("ListRouteFailoverEvents()[0] = %+v, want codex2api -> asxs failure_count 2", failovers[0])
	}
}

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

	req, stickyKey, requestID, err := normalizeResolveRouteRequest(ResolveRouteRequest{
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-1",
	})
	if err != nil {
		t.Fatalf("normalizeResolveRouteRequest() error = %v", err)
	}
	if req.Scope != "conversation" || stickyKey == "" || requestID == "" {
		t.Fatalf("normalizeResolveRouteRequest() = (%+v, %q, %q), want normalized values", req, stickyKey, requestID)
	}
	if _, _, _, err := normalizeResolveRouteRequest(ResolveRouteRequest{}); err == nil {
		t.Fatal("normalizeResolveRouteRequest(empty) error = nil, want error")
	}

	if got := resolveUserKey(ResolveRouteRequest{Scope: "user", SubjectID: "user-1"}); got != "user-1" {
		t.Fatalf("resolveUserKey(user) = %q, want user-1", got)
	}
	if got := resolveConversationKey(ResolveRouteRequest{Scope: "conversation", SubjectID: "conv-1"}); got != "conv-1" {
		t.Fatalf("resolveConversationKey(conversation) = %q, want conv-1", got)
	}

	if got := failureSkipReason(routing.RouteFailureState{LastErrorClass: "timeout"}); got != "failure_threshold_exceeded:timeout" {
		t.Fatalf("failureSkipReason() = %q, want timeout reason", got)
	}
	if got := cooldownSkipReason(routing.RouteCooldownState{Reason: "degraded"}); got != "active_cooldown:degraded" {
		t.Fatalf("cooldownSkipReason() = %q, want degraded reason", got)
	}
	if !routeExitsCooldown("") {
		t.Fatal("routeExitsCooldown(empty) = false, want true")
	}
	future := time.Now().UTC().Add(time.Minute).Format(time.RFC3339)
	if routeExitsCooldown(future) {
		t.Fatalf("routeExitsCooldown(%q) = true, want false", future)
	}
	if !isActiveStatus(" active ") {
		t.Fatal("isActiveStatus(active) = false, want true")
	}
}

func TestResolveRouteWithCooldownFallback(t *testing.T) {
	dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-resolve-cooldown.db")) + "?_busy_timeout=5000"
	actions := NewActionSet(dsn)
	ctx := context.Background()

	_, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
		LogicalGroupID:         "cooldown-group",
		DisplayName:            "Cooldown Group",
		Status:                 "active",
		RoutePolicy:            "priority",
		StickyMode:             "conversation_preferred",
		ConversationTTLSeconds: 1200,
		UserModelTTLSeconds:    600,
		FailoverThreshold:      2,
		CooldownSeconds:        300,
	})
	if err != nil {
		t.Fatalf("CreateLogicalGroup() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupModel(ctx, CreateLogicalGroupModelRequest{
		LogicalGroupID: "cooldown-group",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupModel() error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID: "cooldown-group",
		RouteID:        "route-a",
		Name:           "Route A",
		Status:         "active",
		Priority:       10,
		ShadowGroupID:  "cooldown-group__a",
		ShadowHostID:   "remote43",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute(route-a) error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "cooldown-group",
		RouteID:        "route-a",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel(route-a) error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
		LogicalGroupID: "cooldown-group",
		RouteID:        "route-b",
		Name:           "Route B",
		Status:         "active",
		Priority:       20,
		ShadowGroupID:  "cooldown-group__b",
		ShadowHostID:   "remote43",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRoute(route-b) error = %v", err)
	}
	if _, err := actions.CreateLogicalGroupRouteModel(ctx, CreateLogicalGroupRouteModelRequest{
		LogicalGroupID: "cooldown-group",
		RouteID:        "route-b",
		PublicModel:    "gpt-5.4",
		Status:         "active",
	}); err != nil {
		t.Fatalf("CreateLogicalGroupRouteModel(route-b) error = %v", err)
	}
	if _, err := actions.SetRouteCooldown(ctx, SetRouteCooldownRequest{
		RouteID:    "route-a",
		Reason:     "degraded",
		TTLSeconds: 600,
	}); err != nil {
		t.Fatalf("SetRouteCooldown(route-a) error = %v", err)
	}

	resolved, err := actions.ResolveRoute(ctx, ResolveRouteRequest{
		RequestID:      "req-cooldown-1",
		LogicalGroupID: "cooldown-group",
		PublicModel:    "gpt-5.4",
		Scope:          "conversation",
		SubjectID:      "conv-cooldown-1",
		Sync:           true,
	})
	if err != nil {
		t.Fatalf("ResolveRoute() error = %v", err)
	}
	if resolved.RouteID != "route-b" || resolved.StickyHit || resolved.StickyAction != "bind" {
		t.Fatalf("ResolveRoute() = %+v, want route-b bind miss", resolved)
	}

	failovers, err := actions.ListRouteFailoverEvents(ctx, ListRouteFailoverEventsRequest{
		RequestID: "req-cooldown-1",
		Limit:     10,
	})
	if err != nil {
		t.Fatalf("ListRouteFailoverEvents() error = %v", err)
	}
	if len(failovers) != 1 || failovers[0].FromRouteID != "route-a" || failovers[0].ToRouteID != "route-b" {
		t.Fatalf("ListRouteFailoverEvents() = %+v, want route-a -> route-b", failovers)
	}
	if failovers[0].Reason != "active_cooldown:degraded" {
		t.Fatalf("ListRouteFailoverEvents()[0].Reason = %q, want active_cooldown:degraded", failovers[0].Reason)
	}

	reqURL := &http.Request{URL: &url.URL{RawQuery: "route_id=route-a"}}
	cooldownReq, cooldownErr := decodeGetRouteCooldownRequest(reqURL)
	if cooldownErr != nil || cooldownReq.RouteID != "route-a" {
		t.Fatalf("decodeGetRouteCooldownRequest() = (%+v, %v), want route-a nil", cooldownReq, cooldownErr)
	}
}
