package api

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

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

	"git.fabledsword.com/bvandeusen/minstrel/internal/recsettings"
)

func newTuningRouter(h *handlers) chi.Router {
	r := chi.NewRouter()
	r.Get("/api/admin/recommendation-tuning", h.handleGetRecommendationTuning)
	r.Patch("/api/admin/recommendation-tuning/{scope}", h.handlePatchRecommendationTuning)
	r.Post("/api/admin/recommendation-tuning/{scope}/reset", h.handleResetRecommendationTuning)
	return r
}

func decodeTuning(t *testing.T, rec *httptest.ResponseRecorder) tuningSnapshot {
	t.Helper()
	var snap tuningSnapshot
	if err := json.Unmarshal(rec.Body.Bytes(), &snap); err != nil {
		t.Fatalf("decode: %v", err)
	}
	return snap
}

func TestRecommendationTuning_GetReturnsShippedDefaults(t *testing.T) {
	h, _ := testHandlers(t)
	req := httptest.NewRequest(http.MethodGet, "/api/admin/recommendation-tuning", nil)
	rec := httptest.NewRecorder()
	newTuningRouter(h).ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", rec.Code)
	}
	snap := decodeTuning(t, rec)
	if snap.Profiles["radio"].TasteWeight != 1.0 || snap.Profiles["daily_mix"].TasteWeight != 1.5 {
		t.Errorf("profiles = %+v, want shipped taste weights 1.0 / 1.5", snap.Profiles)
	}
	if snap.Taste.HalfLifeDays != 75 {
		t.Errorf("taste half-life = %v, want shipped 75", snap.Taste.HalfLifeDays)
	}
	if snap.Shipped.Profiles["radio"] != snap.Profiles["radio"] {
		t.Error("untouched values must equal shipped defaults")
	}
}

func TestRecommendationTuning_PatchAndReset(t *testing.T) {
	h, _ := testHandlers(t)
	r := newTuningRouter(h)

	req := httptest.NewRequest(http.MethodPatch, "/api/admin/recommendation-tuning/radio",
		strings.NewReader(`{"values":{"taste_weight": 2.5}}`))
	rec := httptest.NewRecorder()
	r.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("patch status = %d, want 200 (%s)", rec.Code, rec.Body.String())
	}
	snap := decodeTuning(t, rec)
	if snap.Profiles["radio"].TasteWeight != 2.5 {
		t.Errorf("patched taste_weight = %v, want 2.5", snap.Profiles["radio"].TasteWeight)
	}
	// The change is live for the radio scoring path.
	if got := h.recSettings.Weights(recsettings.ScopeRadio).TasteWeight; got != 2.5 {
		t.Errorf("service taste_weight = %v, want 2.5 (live effect)", got)
	}

	req = httptest.NewRequest(http.MethodPost, "/api/admin/recommendation-tuning/radio/reset", nil)
	rec = httptest.NewRecorder()
	r.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("reset status = %d, want 200", rec.Code)
	}
	snap = decodeTuning(t, rec)
	if snap.Profiles["radio"].TasteWeight != 1.0 {
		t.Errorf("reset taste_weight = %v, want shipped 1.0", snap.Profiles["radio"].TasteWeight)
	}
}

func TestRecommendationTuning_PatchErrors(t *testing.T) {
	h, _ := testHandlers(t)
	r := newTuningRouter(h)
	cases := []struct {
		name, path, body string
		want             int
	}{
		{"unknown scope", "/api/admin/recommendation-tuning/banana",
			`{"values":{"taste_weight":1}}`, http.StatusNotFound},
		{"unknown field", "/api/admin/recommendation-tuning/radio",
			`{"values":{"vibes":1}}`, http.StatusBadRequest},
		{"out of range", "/api/admin/recommendation-tuning/taste",
			`{"values":{"engagement_neutral":2}}`, http.StatusBadRequest},
		{"empty values", "/api/admin/recommendation-tuning/radio",
			`{"values":{}}`, http.StatusBadRequest},
		{"bad json", "/api/admin/recommendation-tuning/radio",
			`{`, http.StatusBadRequest},
	}
	for _, c := range cases {
		req := httptest.NewRequest(http.MethodPatch, c.path, strings.NewReader(c.body))
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)
		if rec.Code != c.want {
			t.Errorf("%s: status = %d, want %d", c.name, rec.Code, c.want)
		}
	}
}
