package search

import (
	"strings"
	"testing"
)

func TestNormalizeEngine(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		want string
	}{
		{name: "postgres explicit", raw: "postgres", want: "postgres"},
		{name: "meilisearch", raw: "meilisearch", want: "meilisearch"},
		{name: "meilisearch mixed case", raw: "MeiliSearch", want: "meilisearch"},
		{name: "meilisearch trimmed", raw: "  meilisearch  ", want: "meilisearch"},
		{name: "typesense", raw: "typesense", want: "typesense"},
		{name: "typesense mixed case", raw: "TypeSense", want: "typesense"},
		{name: "empty defaults postgres", raw: "", want: "postgres"},
		{name: "unknown defaults postgres", raw: "elasticsearch", want: "postgres"},
		{name: "whitespace defaults postgres", raw: "   ", want: "postgres"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := normalizeEngine(tt.raw); got != tt.want {
				t.Fatalf("normalizeEngine(%q) = %q, want %q", tt.raw, got, tt.want)
			}
		})
	}
}

func TestParseTypes(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		want []string
	}{
		{
			name: "empty defaults all",
			raw:  "",
			want: []string{"mail", "contacts", "files", "events"},
		},
		{
			name: "whitespace defaults all",
			raw:  "   ",
			want: []string{"mail", "contacts", "files", "events"},
		},
		{
			name: "explicit subset",
			raw:  "mail,contacts",
			want: []string{"mail", "contacts"},
		},
		{
			name: "deduplicates",
			raw:  "mail,mail,contacts,contacts",
			want: []string{"mail", "contacts"},
		},
		{
			name: "trims and skips empty parts",
			raw:  " mail , , contacts ",
			want: []string{"mail", "contacts"},
		},
		{
			name: "preserves order",
			raw:  "events,mail,contacts",
			want: []string{"events", "mail", "contacts"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := parseTypes(tt.raw)
			if len(got) != len(tt.want) {
				t.Fatalf("parseTypes(%q) = %v, want %v", tt.raw, got, tt.want)
			}
			for i := range tt.want {
				if got[i] != tt.want[i] {
					t.Fatalf("parseTypes(%q)[%d] = %q, want %q (full: %v)", tt.raw, i, got[i], tt.want[i], got)
				}
			}
		})
	}
}

func TestSplitSearchTerms(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  []string
	}{
		{name: "empty", input: "", want: nil},
		{name: "whitespace", input: "   ", want: nil},
		{name: "simple words", input: "hello world", want: []string{"hello", "world"}},
		{name: "strips punctuation", input: "hello, world!", want: []string{"hello", "world"}},
		{name: "deduplicates", input: "foo foo bar", want: []string{"foo", "bar"}},
		{name: "mixed punctuation", input: "can't re-read", want: []string{"can", "t", "re", "read"}},
		{name: "case normalized", input: "Hello HELLO", want: []string{"hello"}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := splitSearchTerms(tt.input)
			if len(got) == 0 && len(tt.want) == 0 {
				return
			}
			if len(got) != len(tt.want) {
				t.Fatalf("splitSearchTerms(%q) = %v, want %v", tt.input, got, tt.want)
			}
			for i := range tt.want {
				if got[i] != tt.want[i] {
					t.Fatalf("splitSearchTerms(%q)[%d] = %q, want %q (full: %v)", tt.input, i, got[i], tt.want[i], got)
				}
			}
		})
	}
}

func TestHighlightTerms(t *testing.T) {
	tests := []struct {
		name string
		text string
		q    string
		want string
	}{
		{name: "empty text", text: "", q: "hello", want: ""},
		{name: "empty query unchanged", text: "Hello World", q: "", want: "Hello World"},
		{name: "case insensitive preserves match casing", text: "Hello World", q: "hello", want: "<mark>Hello</mark> World"},
		{name: "multiple terms", text: "Meet Alice and Bob", q: "alice bob", want: "Meet <mark>Alice</mark> and <mark>Bob</mark>"},
		{name: "word boundary only", text: "hellish hello", q: "hello", want: "hellish <mark>hello</mark>"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := highlightTerms(tt.text, tt.q); got != tt.want {
				t.Fatalf("highlightTerms(%q, %q) = %q, want %q", tt.text, tt.q, got, tt.want)
			}
		})
	}
}

func TestContextualSnippet(t *testing.T) {
	t.Run("highlights match in short text", func(t *testing.T) {
		got := contextualSnippet("Invoice from Acme Corp", "acme", 180)
		want := "Invoice from <mark>Acme</mark> Corp"
		if got != want {
			t.Fatalf("contextualSnippet short = %q, want %q", got, want)
		}
	})

	t.Run("trims long content around match", func(t *testing.T) {
		prefix := strings.Repeat("word ", 30)
		text := prefix + "needle here " + strings.Repeat("tail ", 30)
		got := contextualSnippet(text, "needle", 40)

		if !strings.Contains(got, "<mark>needle</mark>") {
			t.Fatalf("expected highlighted needle, got %q", got)
		}
		if !strings.HasPrefix(got, "...") {
			t.Fatalf("expected leading ellipsis for trimmed snippet, got %q", got)
		}
		if !strings.HasSuffix(got, "...") {
			t.Fatalf("expected trailing ellipsis for trimmed snippet, got %q", got)
		}
		if len(got) > 80 {
			t.Fatalf("snippet too long (%d chars): %q", len(got), got)
		}
	})

	t.Run("empty text", func(t *testing.T) {
		if got := contextualSnippet("   ", "foo", 100); got != "" {
			t.Fatalf("contextualSnippet empty = %q, want empty", got)
		}
	})

	t.Run("default maxLen when zero", func(t *testing.T) {
		long := strings.Repeat("x", 300) + " findme"
		got := contextualSnippet(long, "findme", 0)
		if !strings.Contains(got, "<mark>findme</mark>") {
			t.Fatalf("expected highlighted findme with default maxLen, got %q", got)
		}
	})
}
