package main

import (
	"bytes"
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-chi/chi/v5"

	"git.agentview.dev/profit/golangLAKEHOUSE/internal/embed"
)

// Closes R-005 for embedd: cmd-level tests for the /embed handler's
// decode + validation paths (empty texts → 400, body cap → 413,
// upstream error → 502). Provider semantics live in
// internal/embed/ollama_test.go.

// stubProvider implements embed.Provider with deterministic stubs.
type stubProvider struct {
	result embed.Result
	err    error
}

func (s *stubProvider) Embed(_ context.Context, _ []string, _ string) (embed.Result, error) {
	return s.result, s.err
}

func mountWithProvider(p embed.Provider) chi.Router {
	h := &handlers{provider: p}
	r := chi.NewRouter()
	h.register(r)
	return r
}

func TestRoutesMounted(t *testing.T) {
	r := mountWithProvider(&stubProvider{})
	found := false
	chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
		if method == "POST" && route == "/embed" {
			found = true
		}
		return nil
	})
	if !found {
		t.Error("POST /embed not mounted")
	}
}

func TestHandleEmbed_BodyTooLarge(t *testing.T) {
	// MaxBytesReader trips during JSON decode. Depending on whether
	// the decoder unwrapping surfaces MaxBytesError or wraps it as a
	// generic decode error, the response is either 413 or 400. Both
	// are valid "client error, fails loud" contracts; the harness's
	// proof_assert_status_4xx covers either at the integration level.
	r := mountWithProvider(&stubProvider{})
	srv := httptest.NewServer(r)
	defer srv.Close()

	big := bytes.Repeat([]byte("x"), maxRequestBytes+(1<<20))
	resp, err := http.Post(srv.URL+"/embed", "application/json", bytes.NewReader(big))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 400 || resp.StatusCode >= 500 {
		t.Errorf("expected 4xx on oversize, got %d", resp.StatusCode)
	}
}

func TestHandleEmbed_MalformedJSON_400(t *testing.T) {
	r := mountWithProvider(&stubProvider{})
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/embed", "application/json", strings.NewReader("not json"))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on malformed, got %d", resp.StatusCode)
	}
}

func TestHandleEmbed_EmptyTextRejected_400(t *testing.T) {
	// Per scrum O-W3 (Opus): reject empty strings up front.
	r := mountWithProvider(&stubProvider{})
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/embed", "application/json",
		strings.NewReader(`{"texts":["valid",""]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on empty text in batch, got %d", resp.StatusCode)
	}
}

func TestHandleEmbed_UpstreamError_502(t *testing.T) {
	// Provider returns a generic error → handler maps to 502 (the
	// "embedding backend was wrong" case, distinct from 400 = your
	// input was wrong).
	r := mountWithProvider(&stubProvider{err: errors.New("ollama is down")})
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/embed", "application/json",
		strings.NewReader(`{"texts":["hello"]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadGateway {
		t.Errorf("expected 502 on provider error, got %d", resp.StatusCode)
	}
}

func TestHandleEmbed_HappyPath_ProviderEcho(t *testing.T) {
	stub := &stubProvider{result: embed.Result{
		Model:     "test-model",
		Dimension: 3,
		Vectors:   [][]float32{{0.1, 0.2, 0.3}},
	}}
	r := mountWithProvider(stub)
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/embed", "application/json",
		strings.NewReader(`{"texts":["hello"],"model":"test-model"}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200 happy path, got %d", resp.StatusCode)
	}
}

func TestStatsEndpoint_CacheDisabled(t *testing.T) {
	r := mountWithProvider(&stubProvider{})
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Get(srv.URL + "/embed/stats")
	if err != nil {
		t.Fatalf("GET: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("status = %d, want 200", resp.StatusCode)
	}
	if !strings.Contains(getBody(t, resp), `"enabled":false`) {
		t.Errorf("expected enabled:false when no cache wired")
	}
}

func TestStatsEndpoint_CacheEnabled_TracksHitsAndMisses(t *testing.T) {
	stub := &stubProvider{result: embed.Result{
		Model:     "test-model",
		Dimension: 3,
		Vectors:   [][]float32{{0.1, 0.2, 0.3}},
	}}
	cache, err := embed.NewCachedProvider(stub, "test-model", 100)
	if err != nil {
		t.Fatalf("NewCachedProvider: %v", err)
	}
	h := &handlers{provider: cache, cache: cache}
	r := chi.NewRouter()
	h.register(r)
	srv := httptest.NewServer(r)
	defer srv.Close()

	// First call → miss.
	http.Post(srv.URL+"/embed", "application/json",
		strings.NewReader(`{"texts":["hello"],"model":"test-model"}`))
	// Second call same text → hit.
	http.Post(srv.URL+"/embed", "application/json",
		strings.NewReader(`{"texts":["hello"],"model":"test-model"}`))

	resp, err := http.Get(srv.URL + "/embed/stats")
	if err != nil {
		t.Fatalf("stats GET: %v", err)
	}
	defer resp.Body.Close()
	body := getBody(t, resp)
	if !strings.Contains(body, `"enabled":true`) {
		t.Errorf("expected enabled:true, body=%s", body)
	}
	if !strings.Contains(body, `"hits":1`) {
		t.Errorf("expected hits:1, body=%s", body)
	}
	if !strings.Contains(body, `"misses":1`) {
		t.Errorf("expected misses:1, body=%s", body)
	}
}

func getBody(t *testing.T, resp *http.Response) string {
	t.Helper()
	buf := make([]byte, 4096)
	n, _ := resp.Body.Read(buf)
	return string(buf[:n])
}

func TestItoa(t *testing.T) {
	cases := []struct {
		in  int
		out string
	}{
		{0, "0"},
		{1, "1"},
		{42, "42"},
		{1000, "1000"},
		{99, "99"},
	}
	for _, tc := range cases {
		if got := itoa(tc.in); got != tc.out {
			t.Errorf("itoa(%d) = %q, want %q", tc.in, got, tc.out)
		}
	}
}
