package search

import (
	"strings"
	"testing"
	"time"

	"github.com/ultisuite/ulti-backend/internal/api/query"
)

func TestNewExternalSearchClient(t *testing.T) {
	tests := []struct {
		name   string
		engine string
		opts   ServiceOptions
		wantOK bool
	}{
		{
			name:   "meilisearch with url",
			engine: "meilisearch",
			opts:   ServiceOptions{MeilisearchURL: "http://localhost:7700"},
			wantOK: true,
		},
		{
			name:   "meilisearch empty url",
			engine: "meilisearch",
			opts:   ServiceOptions{MeilisearchURL: "   "},
			wantOK: false,
		},
		{
			name:   "typesense with url",
			engine: "typesense",
			opts:   ServiceOptions{TypesenseURL: "http://localhost:8108"},
			wantOK: true,
		},
		{
			name:   "typesense empty url",
			engine: "typesense",
			opts:   ServiceOptions{TypesenseURL: ""},
			wantOK: false,
		},
		{
			name:   "postgres engine",
			engine: "postgres",
			opts:   ServiceOptions{MeilisearchURL: "http://localhost:7700"},
			wantOK: false,
		},
		{
			name:   "unknown engine",
			engine: "elasticsearch",
			opts:   ServiceOptions{TypesenseURL: "http://localhost:8108"},
			wantOK: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := newExternalSearchClient(tt.engine, tt.opts)
			if tt.wantOK && got == nil {
				t.Fatal("expected non-nil client")
			}
			if !tt.wantOK && got != nil {
				t.Fatal("expected nil client")
			}
		})
	}
}

func TestMapExternalHits(t *testing.T) {
	hits := []map[string]any{
		{
			"id":      "mail-42",
			"type":    "mail",
			"title":   "Invoice from Acme",
			"snippet": "Your Acme invoice is ready",
		},
	}

	results := mapExternalHits(hits, "acme")
	if len(results) != 1 {
		t.Fatalf("len(results) = %d, want 1", len(results))
	}

	r := results[0]
	if r.Type != "mail" {
		t.Fatalf("Type = %q, want mail", r.Type)
	}
	if r.ID != "mail-42" {
		t.Fatalf("ID = %q, want mail-42", r.ID)
	}
	if r.Title == "" {
		t.Fatal("Title is empty")
	}
	if r.Snippet == "" {
		t.Fatal("Snippet is empty")
	}
	if r.Score <= 0 {
		t.Fatalf("Score = %v, want positive fallback score", r.Score)
	}
	if !strings.Contains(r.Title, "<mark>") {
		t.Fatalf("Title missing highlight: %q", r.Title)
	}
	if !strings.Contains(r.Snippet, "<mark>") {
		t.Fatalf("Snippet missing highlight: %q", r.Snippet)
	}
}

func TestMapExternalHitsMinimalDefaults(t *testing.T) {
	hits := []map[string]any{
		{"title": "Budget report"},
	}

	results := mapExternalHits(hits, "budget")
	if len(results) != 1 {
		t.Fatalf("len(results) = %d, want 1", len(results))
	}

	r := results[0]
	if r.Type != "mail" {
		t.Fatalf("Type = %q, want default mail", r.Type)
	}
	if r.ID != "mail:Budget report" {
		t.Fatalf("ID = %q, want mail:Budget report", r.ID)
	}
	if r.Snippet == "" {
		t.Fatal("Snippet is empty")
	}
}

func TestFinalizeExternalResultsDateFiltering(t *testing.T) {
	mid := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
	from := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)
	to := time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC)

	results := []Result{
		{ID: "before", Title: "Old mail", Snippet: "Old mail", Date: time.Date(2024, 5, 15, 12, 0, 0, 0, time.UTC), Score: 5},
		{ID: "in-range", Title: "Current mail", Snippet: "Current mail", Date: mid, Score: 5},
		{ID: "after", Title: "Future mail", Snippet: "Future mail", Date: time.Date(2024, 7, 15, 12, 0, 0, 0, time.UTC), Score: 5},
	}

	params := query.ListParams{From: &from, To: &to}
	finalizeExternalResults(results, "mail", params)

	if results[0].Score >= 0 {
		t.Fatalf("before-range Score = %v, want negative", results[0].Score)
	}
	if results[1].Score <= 0 {
		t.Fatalf("in-range Score = %v, want positive with recency boost", results[1].Score)
	}
	if results[2].Score >= 0 {
		t.Fatalf("after-range Score = %v, want negative", results[2].Score)
	}
}

func TestFinalizeExternalResultsFallbackScore(t *testing.T) {
	results := []Result{
		{ID: "1", Title: "Invoice details", Snippet: "Invoice details", Score: 0},
	}

	finalizeExternalResults(results, "invoice", query.ListParams{})
	if results[0].Score <= 0 {
		t.Fatalf("Score = %v, want positive fallback score", results[0].Score)
	}
}

func TestAsStringNil(t *testing.T) {
	if got := asString(nil); got != "" {
		t.Fatalf("asString(nil) = %q, want empty string", got)
	}
}
