package main

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
)

// ok is what the boundary is protecting: anything that reaches it has spent
// the card.
func ok(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }

func TestRequireTokenRefusesEveryWrongCredential(t *testing.T) {
	h := requireToken("s3cret", 1<<20, http.HandlerFunc(ok))
	cases := []struct {
		name string
		auth string
		want int
	}{
		{"no header", "", http.StatusUnauthorized},
		{"wrong token", "Bearer wrong", http.StatusUnauthorized},
		{"prefix of the token", "Bearer s3cre", http.StatusUnauthorized},
		{"token with no scheme", "s3cret", http.StatusUnauthorized},
		{"basic auth", "Basic czNjcmV0", http.StatusUnauthorized},
		{"right token", "Bearer s3cret", http.StatusTeapot},
		{"scheme is case-insensitive", "bearer s3cret", http.StatusTeapot},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			r := httptest.NewRequest(http.MethodGet, "/health", nil)
			if tc.auth != "" {
				r.Header.Set("Authorization", tc.auth)
			}
			w := httptest.NewRecorder()
			h.ServeHTTP(w, r)
			if w.Code != tc.want {
				t.Errorf("status %d, want %d", w.Code, tc.want)
			}
		})
	}
}

// The 401 must not say which part was wrong. A probe that can tell a malformed
// header from a wrong token learns the header shape for free.
func TestUnauthorizedSaysNothingUseful(t *testing.T) {
	h := requireToken("s3cret", 1<<20, http.HandlerFunc(ok))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/health", nil))
	if got := strings.TrimSpace(w.Body.String()); got != "unauthorized" {
		t.Errorf("body %q, want %q", got, "unauthorized")
	}
	if got := w.Header().Get("WWW-Authenticate"); got != "Bearer" {
		t.Errorf("WWW-Authenticate %q, want Bearer", got)
	}
}

// The body cap applies to an authenticated request. An unauthenticated one
// never gets far enough to allocate anything.
func TestRequireTokenCapsTheBody(t *testing.T) {
	var read error
	h := requireToken("t", 8, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		buf := make([]byte, 64)
		for read == nil {
			if _, read = r.Body.Read(buf); read != nil {
				break
			}
		}
	}))
	r := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(strings.Repeat("x", 4096)))
	r.Header.Set("Authorization", "Bearer t")
	h.ServeHTTP(httptest.NewRecorder(), r)
	if read == nil || !strings.Contains(read.Error(), "too large") {
		t.Errorf("read error %v, want the body cap", read)
	}
}

func TestAllowlistRefusesWhatMavenDoesNotCall(t *testing.T) {
	h := allowlist(http.HandlerFunc(ok))
	cases := []struct {
		method, path string
		want         int
	}{
		{http.MethodPost, "/v1/chat/completions", http.StatusTeapot},
		{http.MethodGet, "/v1/models", http.StatusTeapot},
		// /slots returns the prompts of whoever else is using the card, and
		// its actions write files the request names.
		{http.MethodGet, "/slots", http.StatusNotFound},
		{http.MethodPost, "/slots/0?action=save", http.StatusNotFound},
		{http.MethodGet, "/", http.StatusNotFound},
		{http.MethodGet, "/v1/chat/completions", http.StatusMethodNotAllowed},
	}
	for _, tc := range cases {
		t.Run(tc.method+" "+tc.path, func(t *testing.T) {
			w := httptest.NewRecorder()
			h.ServeHTTP(w, httptest.NewRequest(tc.method, tc.path, nil))
			if w.Code != tc.want {
				t.Errorf("status %d, want %d", w.Code, tc.want)
			}
		})
	}
}

func TestLimitInflightCapsConcurrency(t *testing.T) {
	const cap = 2
	var mu sync.Mutex
	now, peak := 0, 0
	release := make(chan struct{})
	h := limitInflight(cap, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		now++
		if now > peak {
			peak = now
		}
		mu.Unlock()
		<-release
		mu.Lock()
		now--
		mu.Unlock()
	}))

	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil))
		}()
	}
	// Let the first wave arrive, then drain. The assertion is the peak, and a
	// peak that never reached the cap still cannot exceed it.
	close(release)
	wg.Wait()
	if peak > cap {
		t.Errorf("%d requests in flight at once, cap is %d", peak, cap)
	}
}

func TestLoopbackListen(t *testing.T) {
	cases := map[string]bool{
		":8080":              false, // the shipped default, and the whole LAN
		"0.0.0.0:8080":       false,
		"[::]:8080":          false,
		"192.168.1.105:8080": false,
		"127.0.0.1:8080":     true,
		"[::1]:8080":         true,
		"localhost:8080":     true,
	}
	for addr, want := range cases {
		if got := loopbackListen(addr); got != want {
			t.Errorf("loopbackListen(%q) = %v, want %v", addr, got, want)
		}
	}
}

func TestReadToken(t *testing.T) {
	dir := t.TempDir()
	good := filepath.Join(dir, "tok")
	if err := os.WriteFile(good, []byte("  abc123\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	got, err := readToken(good)
	if err != nil || got != "abc123" {
		t.Errorf("readToken = %q, %v; want abc123", got, err)
	}

	blank := filepath.Join(dir, "blank")
	if err := os.WriteFile(blank, []byte("\n\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if _, err := readToken(blank); err == nil {
		t.Error("an empty token file is not a token")
	}
	if _, err := readToken(filepath.Join(dir, "absent")); err == nil {
		t.Error("a missing token file is not a token")
	}
}
