package shared

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/mostlygeek/llama-swap/internal/config"
)

func TestShared_ParsePriority(t *testing.T) {
	tests := []struct {
		name  string
		value string
		want  int
	}{
		{"absent", "", PriorityNormal},
		{"zero", "0", 0},
		{"positive", "100", 100},
		{"negative", "-100", -100},
		{"explicit plus", "+42", 42},
		{"surrounding space", "  100  ", 100},
		{"large value is not clamped", "100000", 100000},
		{"alias interactive", "interactive", PriorityInteractive},
		{"alias normal", "normal", PriorityNormal},
		{"alias batch", "batch", PriorityBatch},
		{"alias is case insensitive", "Batch", PriorityBatch},
		{"tier offset on a band", "-98", -98},
		{"unknown alias falls back to normal", "urgent", PriorityNormal},
		{"garbage falls back to normal", "!!", PriorityNormal},
		{"float falls back to normal", "1.5", PriorityNormal},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := ParsePriority(tc.value); got != tc.want {
				t.Errorf("ParsePriority(%q)=%d want %d", tc.value, got, tc.want)
			}
		})
	}
}

func TestShared_RequestPriority(t *testing.T) {
	r := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	if got := RequestPriority(r); got != PriorityNormal {
		t.Errorf("no header: got %d want %d", got, PriorityNormal)
	}

	r.Header.Set(PriorityHeader, "batch")
	if got := RequestPriority(r); got != PriorityBatch {
		t.Errorf("batch header: got %d want %d", got, PriorityBatch)
	}
}

// TestShared_PriorityBand pins the bucketing the metrics labels rely on. The
// important cases are the tier offsets: a max member's batch job at -98 must
// still report as batch, not normal.
func TestShared_PriorityBand(t *testing.T) {
	tests := []struct {
		priority int
		want     string
	}{
		{PriorityInteractive, BandInteractive},
		{PriorityInteractive + 2, BandInteractive}, // max tier, interactive
		{1000, BandInteractive},                    // saturates
		{50, BandInteractive},                      // lower edge
		{49, BandNormal},
		{PriorityNormal, BandNormal},
		{2, BandNormal}, // max tier, normal
		{-49, BandNormal},
		{-50, BandBatch}, // upper edge
		{PriorityBatch + 2, BandBatch},
		{PriorityBatch, BandBatch},
		{-1000, BandBatch}, // saturates
	}
	for _, tc := range tests {
		if got := PriorityBand(tc.priority); got != tc.want {
			t.Errorf("PriorityBand(%d)=%q want %q", tc.priority, got, tc.want)
		}
	}
}

// TestShared_FetchContext_Priority verifies the header reaches the request
// context, which is what carries it to the scheduler. Both entry points into
// FetchContext are covered: the normal body-parsed path and /upstream/<model>.
func TestShared_FetchContext_Priority(t *testing.T) {
	cfg := config.Config{Models: map[string]config.ModelConfig{"m1": {}}}

	cases := []struct {
		name   string
		path   string
		header string
		want   int
	}{
		{"body path, no header", "/v1/chat/completions", "", PriorityNormal},
		{"body path, alias", "/v1/chat/completions", "interactive", PriorityInteractive},
		{"body path, number", "/v1/chat/completions", "-98", -98},
		{"upstream path", "/upstream/m1/v1/chat/completions", "batch", PriorityBatch},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			r := httptest.NewRequest(http.MethodPost, c.path, strings.NewReader(`{"model":"m1"}`))
			r.Header.Set("Content-Type", "application/json")
			if c.header != "" {
				r.Header.Set(PriorityHeader, c.header)
			}

			data, err := FetchContext(r, cfg)
			if err != nil {
				t.Fatalf("FetchContext: %v", err)
			}
			if data.Priority != c.want {
				t.Errorf("Priority=%d want %d", data.Priority, c.want)
			}
		})
	}
}
