package handlers

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

// TestSearchStaticContent verifies the curated-content (non-DB) search paths.
// These execute against the static Go slices bundled with the binary, so the
// assertions below are stable as long as those slices contain the checked
// entries (enforced by the acceptance cases in task t-paliad-026).
func TestSearchStaticContent(t *testing.T) {
	cases := []struct {
		name      string
		query     string
		group     string
		wantTitle string
	}{
		{
			name:      "glossary finds Nichtigkeitsklage",
			query:     "nichtigkeits",
			group:     "glossary",
			wantTitle: "Nichtigkeitsklage",
		},
		{
			name:      "courts finds Munich",
			query:     "münchen",
			group:     "courts",
			wantTitle: "München",
		},
		{
			name:      "links finds Espacenet",
			query:     "espacenet",
			group:     "links",
			wantTitle: "Espacenet",
		},
		{
			name:      "checklists finds UPC statement of claim",
			query:     "statement of claim",
			group:     "checklists",
			wantTitle: "UPC",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/api/search?q="+url.QueryEscape(tc.query), nil)
			rec := httptest.NewRecorder()
			handleSearch(rec, req)

			if rec.Code != http.StatusOK {
				t.Fatalf("status = %d, want 200", rec.Code)
			}
			var resp searchResponse
			if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
				t.Fatalf("decode: %v", err)
			}
			got := groupByName(&resp, tc.group)
			if len(got) == 0 {
				t.Fatalf("no results in group %q for query %q", tc.group, tc.query)
			}
			found := false
			for _, r := range got {
				if strings.Contains(strings.ToLower(r.Title), strings.ToLower(tc.wantTitle)) ||
					strings.Contains(strings.ToLower(r.Subtitle), strings.ToLower(tc.wantTitle)) {
					found = true
					break
				}
			}
			if !found {
				t.Errorf("no result containing %q in group %q; got %+v",
					tc.wantTitle, tc.group, got)
			}
		})
	}
}

// TestSearchEmptyQuery guarantees we never leak the entire catalog on an
// empty or whitespace-only query.
func TestSearchEmptyQuery(t *testing.T) {
	for _, q := range []string{"", "   ", "\t\n"} {
		req := httptest.NewRequest(http.MethodGet, "/api/search?q="+url.QueryEscape(q), nil)
		rec := httptest.NewRecorder()
		handleSearch(rec, req)

		var resp searchResponse
		if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
			t.Fatalf("decode: %v", err)
		}
		total := len(resp.Projects) + len(resp.Deadlines) + len(resp.Appointments) +
			len(resp.Glossary) + len(resp.Courts) + len(resp.Checklists) +
			len(resp.Links) + len(resp.Users)
		if total != 0 {
			t.Errorf("empty query returned %d results; want 0", total)
		}
	}
}

// TestSearchCapsTotalBudget asserts the overall ceiling is enforced. With a
// broad query like "e" every category contributes something, so the raw
// total would exceed maxTotalResults and the cap must kick in.
func TestSearchCapsTotalBudget(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/search?q=e", nil)
	rec := httptest.NewRecorder()
	handleSearch(rec, req)

	var resp searchResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	total := len(resp.Projects) + len(resp.Deadlines) + len(resp.Appointments) +
		len(resp.Glossary) + len(resp.Courts) + len(resp.Checklists) +
		len(resp.Links) + len(resp.Users)
	if total > maxTotalResults {
		t.Errorf("total %d exceeds ceiling %d", total, maxTotalResults)
	}
}

func groupByName(r *searchResponse, name string) []SearchResult {
	switch name {
	case "projects":
		return r.Projects
	case "deadlines":
		return r.Deadlines
	case "appointments":
		return r.Appointments
	case "glossary":
		return r.Glossary
	case "courts":
		return r.Courts
	case "checklists":
		return r.Checklists
	case "links":
		return r.Links
	case "users":
		return r.Users
	}
	return nil
}
