package app

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

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

func TestAPISetStickyBindingReturnsCreated(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		SetStickyBinding: func(_ context.Context, req SetStickyBindingRequest) (StickyBindingInfo, error) {
			if req.Scope != "conversation" {
				t.Fatalf("Scope = %q, want conversation", req.Scope)
			}
			return StickyBindingInfo{
				Backend: "memory",
				Key:     "lg:gpt-shared:m:gpt-5.4:conv:conv-1",
				Scope:   req.Scope,
				RouteID: req.RouteID,
			}, nil
		},
	})

	request := httptestRequest(t, http.MethodPost, "/api/routing/sticky/bindings", map[string]any{
		"scope":            "conversation",
		"logical_group_id": "gpt-shared",
		"public_model":     "gpt-5.4",
		"subject_id":       "conv-1",
		"route_id":         "asxs",
		"shadow_group_id":  "gpt-shared__asxs",
		"ttl_seconds":      600,
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusCreated)
	assertJSONContains(t, response.Body().Bytes(), "binding.backend", "memory")
	assertJSONContains(t, response.Body().Bytes(), "binding.route_id", "asxs")
}

func TestAPIGetStickyBindingRejectsMissingQuery(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		GetStickyBinding: func(context.Context, GetStickyBindingRequest) (StickyBindingInfo, error) {
			t.Fatal("GetStickyBinding should not be called")
			return StickyBindingInfo{}, nil
		},
	})

	request := httptestRequest(t, http.MethodGet, "/api/routing/sticky/bindings?scope=conversation", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
}

func TestAPISetAndGetRouteFailureAndCooldown(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		SetRouteFailure: func(_ context.Context, req SetRouteFailureRequest) (RouteFailureInfo, error) {
			return RouteFailureInfo{
				Backend:      "memory",
				Key:          "routefail:" + req.RouteID,
				RouteID:      req.RouteID,
				FailureCount: req.FailureCount,
			}, nil
		},
		GetRouteFailure: func(_ context.Context, req GetRouteFailureRequest) (RouteFailureInfo, error) {
			return RouteFailureInfo{
				Backend:      "memory",
				Key:          "routefail:" + req.RouteID,
				RouteID:      req.RouteID,
				FailureCount: 2,
			}, nil
		},
		SetRouteCooldown: func(_ context.Context, req SetRouteCooldownRequest) (RouteCooldownInfo, error) {
			return RouteCooldownInfo{
				Backend: "memory",
				Key:     "routecool:" + req.RouteID,
				RouteID: req.RouteID,
				Reason:  req.Reason,
			}, nil
		},
		GetRouteCooldown: func(_ context.Context, req GetRouteCooldownRequest) (RouteCooldownInfo, error) {
			return RouteCooldownInfo{
				Backend: "memory",
				Key:     "routecool:" + req.RouteID,
				RouteID: req.RouteID,
				Reason:  "cooldown",
			}, nil
		},
	})

	setFailureReq := httptestRequest(t, http.MethodPost, "/api/routing/sticky/route-failures", map[string]any{
		"route_id":      "asxs",
		"failure_count": 2,
		"ttl_seconds":   600,
	}, "secret-token")
	setFailureResp := httptestRecorder(handler, setFailureReq)
	assertStatusCode(t, setFailureResp, http.StatusCreated)
	assertJSONContains(t, setFailureResp.Body().Bytes(), "route_failure.key", "routefail:asxs")

	getFailureReq := httptestRequest(t, http.MethodGet, "/api/routing/sticky/route-failures?route_id=asxs", nil, "secret-token")
	getFailureResp := httptestRecorder(handler, getFailureReq)
	assertStatusCode(t, getFailureResp, http.StatusOK)
	assertJSONContains(t, getFailureResp.Body().Bytes(), "route_failure.failure_count", float64(2))

	setCooldownReq := httptestRequest(t, http.MethodPost, "/api/routing/sticky/cooldowns", map[string]any{
		"route_id":    "asxs",
		"reason":      "cooldown",
		"ttl_seconds": 600,
	}, "secret-token")
	setCooldownResp := httptestRecorder(handler, setCooldownReq)
	assertStatusCode(t, setCooldownResp, http.StatusCreated)
	assertJSONContains(t, setCooldownResp.Body().Bytes(), "route_cooldown.key", "routecool:asxs")

	getCooldownReq := httptestRequest(t, http.MethodGet, "/api/routing/sticky/cooldowns?route_id=asxs", nil, "secret-token")
	getCooldownResp := httptestRecorder(handler, getCooldownReq)
	assertStatusCode(t, getCooldownResp, http.StatusOK)
	assertJSONContains(t, getCooldownResp.Body().Bytes(), "route_cooldown.reason", "cooldown")
}

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

	binding, err := actions.SetStickyBinding(ctx, SetStickyBindingRequest{
		Scope:          "conversation",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		SubjectID:      "conv-1",
		RouteID:        "asxs",
		ShadowGroupID:  "gpt-shared__asxs",
		TTLSeconds:     600,
	})
	if err != nil {
		t.Fatalf("SetStickyBinding() error = %v", err)
	}
	if binding.Backend != "memory" || binding.RouteID != "asxs" {
		t.Fatalf("SetStickyBinding() = %+v, want memory/asxs", binding)
	}

	loadedBinding, 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 loadedBinding.Key != binding.Key {
		t.Fatalf("GetStickyBinding().Key = %q, want %q", loadedBinding.Key, binding.Key)
	}

	failure, err := actions.SetRouteFailure(ctx, SetRouteFailureRequest{
		RouteID:        "asxs",
		FailureCount:   2,
		LastErrorClass: "timeout",
		TTLSeconds:     600,
	})
	if err != nil {
		t.Fatalf("SetRouteFailure() error = %v", err)
	}
	if failure.FailureCount != 2 {
		t.Fatalf("SetRouteFailure() = %+v, want count 2", failure)
	}
	loadedFailure, err := actions.GetRouteFailure(ctx, GetRouteFailureRequest{RouteID: "asxs"})
	if err != nil {
		t.Fatalf("GetRouteFailure() error = %v", err)
	}
	if loadedFailure.Key != failure.Key {
		t.Fatalf("GetRouteFailure().Key = %q, want %q", loadedFailure.Key, failure.Key)
	}

	cooldown, err := actions.SetRouteCooldown(ctx, SetRouteCooldownRequest{
		RouteID:    "asxs",
		Reason:     "degraded",
		TTLSeconds: 600,
	})
	if err != nil {
		t.Fatalf("SetRouteCooldown() error = %v", err)
	}
	if cooldown.Reason != "degraded" {
		t.Fatalf("SetRouteCooldown() = %+v, want reason degraded", cooldown)
	}
	loadedCooldown, err := actions.GetRouteCooldown(ctx, GetRouteCooldownRequest{RouteID: "asxs"})
	if err != nil {
		t.Fatalf("GetRouteCooldown() error = %v", err)
	}
	if loadedCooldown.Key != cooldown.Key {
		t.Fatalf("GetRouteCooldown().Key = %q, want %q", loadedCooldown.Key, cooldown.Key)
	}
}

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

	runtime, err := newStickyStoreRuntime(context.Background(), config.RouteRuntimeConfig{
		Backend: "memory",
	})
	if err != nil {
		t.Fatalf("newStickyStoreRuntime(memory) error = %v", err)
	}
	if runtime.backend != "memory" || runtime.store == nil {
		t.Fatalf("newStickyStoreRuntime(memory) = %+v, want memory backend with store", runtime)
	}

	if _, err := newStickyStoreRuntime(context.Background(), config.RouteRuntimeConfig{
		Backend: "bad",
	}); err == nil {
		t.Fatal("newStickyStoreRuntime(bad) error = nil, want error")
	}

	if got := defaultStickyStoreRuntime(); got.backend != "memory" || got.store == nil {
		t.Fatalf("defaultStickyStoreRuntime() = %+v, want memory backend with store", got)
	}
}

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

	req := &http.Request{URL: &url.URL{RawQuery: "scope=conversation&logical_group_id=gpt-shared&public_model=gpt-5.4&subject_id=conv-1"}}
	stickyReq, stickyErr := decodeGetStickyBindingRequest(req)
	if stickyErr != nil {
		t.Fatalf("decodeGetStickyBindingRequest() error = %v", stickyErr)
	}
	if stickyReq.SubjectID != "conv-1" {
		t.Fatalf("decodeGetStickyBindingRequest() = %+v, want subject_id conv-1", stickyReq)
	}

	req = &http.Request{URL: &url.URL{RawQuery: "route_id=asxs"}}
	failureReq, failureErr := decodeGetRouteFailureRequest(req)
	if failureErr != nil {
		t.Fatalf("decodeGetRouteFailureRequest() error = %v", failureErr)
	}
	if failureReq.RouteID != "asxs" {
		t.Fatalf("decodeGetRouteFailureRequest() = %+v, want route_id asxs", failureReq)
	}

	cooldownReq, cooldownErr := decodeGetRouteCooldownRequest(req)
	if cooldownErr != nil {
		t.Fatalf("decodeGetRouteCooldownRequest() error = %v", cooldownErr)
	}
	if cooldownReq.RouteID != "asxs" {
		t.Fatalf("decodeGetRouteCooldownRequest() = %+v, want route_id asxs", cooldownReq)
	}

	if _, err := secondsToDuration(-1, defaultStickyTTLSeconds); err == nil {
		t.Fatal("secondsToDuration(-1) error = nil, want error")
	}
	duration, err := secondsToDuration(0, defaultStickyTTLSeconds)
	if err != nil {
		t.Fatalf("secondsToDuration(default) error = %v", err)
	}
	if duration <= 0 {
		t.Fatalf("secondsToDuration(default) = %s, want positive", duration)
	}

	if _, err := parseTTLQuery("bad"); err == nil {
		t.Fatal("parseTTLQuery(bad) error = nil, want error")
	}
}
