package websearch

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

const sampleJSON = `{
  "query": "почему небо голубое",
  "answers": ["Rayleigh scattering makes the sky blue."],
  "results": [
    {"title": "Рэлеевское рассеяние", "url": "https://ru.wikipedia.org/x", "content": "Рассеяние\n  света  на молекулах.", "engine": "wikipedia"},
    {"title": "", "url": "https://example.org/empty", "content": "", "engine": "duckduckgo"},
    {"title": "Why is the sky blue", "url": "https://example.org/2", "content": "Short answer.", "engine": "duckduckgo"}
  ]
}`

func TestParseResponse(t *testing.T) {
	got, err := ParseResponse([]byte(sampleJSON), 5)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(got.Answers) != 1 || got.Answers[0] != "Rayleigh scattering makes the sky blue." {
		t.Fatalf("answers = %#v", got.Answers)
	}
	// The textless middle hit is dropped: it is a link with nothing to read.
	if len(got.Results) != 2 {
		t.Fatalf("results = %#v", got.Results)
	}
	if got.Results[0].Content != "Рассеяние света на молекулах." {
		t.Fatalf("whitespace not collapsed: %q", got.Results[0].Content)
	}
	if got.Empty() {
		t.Fatal("Empty() on a response with hits")
	}
}

// The limit counts usable hits, not raw ones — a textless entry must not push a
// real snippet out of the reply.
func TestParseResponseLimitSkipsEmpty(t *testing.T) {
	got, err := ParseResponse([]byte(sampleJSON), 2)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(got.Results) != 2 {
		t.Fatalf("results = %d, want 2", len(got.Results))
	}
	if got.Results[1].Title != "Why is the sky blue" {
		t.Fatalf("second hit = %q", got.Results[1].Title)
	}
}

// Newer SearXNG emits answers as objects; older ones as bare strings. Both are
// in the wild and both must read.
func TestParseResponseObjectAnswers(t *testing.T) {
	got, err := ParseResponse([]byte(`{"answers":[{"answer":"42","url":"x"}],"results":[]}`), 5)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(got.Answers) != 1 || got.Answers[0] != "42" {
		t.Fatalf("answers = %#v", got.Answers)
	}
}

func TestResponseEmpty(t *testing.T) {
	got, err := ParseResponse([]byte(`{"answers":[],"results":[]}`), 5)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if !got.Empty() {
		t.Fatal("Empty() = false on a reply with nothing in it")
	}
}

func TestSnippetsAnswersFirst(t *testing.T) {
	got, _ := ParseResponse([]byte(sampleJSON), 5)
	lines := got.Snippets()
	if len(lines) != 3 {
		t.Fatalf("lines = %#v", lines)
	}
	if lines[0] != "Rayleigh scattering makes the sky blue." {
		t.Fatalf("answer did not lead: %q", lines[0])
	}
	if !strings.Contains(lines[1], " — ") {
		t.Fatalf("result line = %q", lines[1])
	}
	// No URL travels into the evidence: piper reads one out character by
	// character and it is not evidence anyway.
	for _, l := range lines {
		if strings.Contains(l, "http") {
			t.Fatalf("url leaked into evidence: %q", l)
		}
	}
}

// The query goes out verbatim, and the JSON format is always asked for.
func TestSearchRequest(t *testing.T) {
	var gotQuery, gotFormat, gotLang, gotEngines string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotQuery = r.URL.Query().Get("q")
		gotFormat = r.URL.Query().Get("format")
		gotLang = r.URL.Query().Get("language")
		gotEngines = r.URL.Query().Get("engines")
		w.Write([]byte(sampleJSON))
	}))
	defer srv.Close()

	c := New(srv.URL, Options{Language: "ru", Engines: "duckduckgo"})
	got, err := c.Search(context.Background(), "почему небо голубое", 3)
	if err != nil {
		t.Fatalf("search: %v", err)
	}
	if gotQuery != "почему небо голубое" {
		t.Fatalf("query was rewritten: %q", gotQuery)
	}
	if gotFormat != "json" || gotLang != "ru" || gotEngines != "duckduckgo" {
		t.Fatalf("format=%q language=%q engines=%q", gotFormat, gotLang, gotEngines)
	}
	if len(got.Results) != 2 {
		t.Fatalf("results = %#v", got.Results)
	}
}

// A stock SearXNG answers 403 to format=json. The error must say so, because
// that is the one misconfiguration this client cannot work around.
func TestSearchHTTPError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "forbidden", http.StatusForbidden)
	}))
	defer srv.Close()

	_, err := New(srv.URL, Options{}).Search(context.Background(), "x", 3)
	if err == nil || !strings.Contains(err.Error(), "403") {
		t.Fatalf("err = %v", err)
	}
}

func TestSearchEmptyQuery(t *testing.T) {
	if _, err := New("http://example.invalid", Options{}).Search(context.Background(), "  ", 3); err == nil {
		t.Fatal("empty query accepted")
	}
}

// A host that never answers a SYN must not hold the turn open for the whole
// request budget: the ZIM behind this search is the answer, and he waits
// through every second of the delay (V-508). 192.0.2.1 is TEST-NET-1, which is
// reserved for documentation and routed nowhere.
func TestSearchGivesUpOnAnUnreachableHostFast(t *testing.T) {
	c := New("http://192.0.2.1:9563", Options{Timeout: 8 * time.Second})
	start := time.Now()
	_, err := c.Search(context.Background(), "фотосинтез", 4)
	elapsed := time.Since(start)
	if err == nil {
		t.Fatal("Search reached 192.0.2.1; the address is routed nowhere")
	}
	if elapsed > 4*time.Second {
		t.Errorf("Search took %v to give up; the dial cap is %v", elapsed, dialTimeout)
	}
}

// The dial cap must not shorten a request to an instance that did connect. A
// slow SearXNG is fanning out to real engines, which is worth waiting for.
func TestSlowButReachableInstanceKeepsTheFullBudget(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * dialTimeout)
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"answers":[],"results":[{"title":"Фотосинтез","url":"http://x","content":"процесс","engine":"test"}]}`))
	}))
	defer srv.Close()
	c := New(srv.URL, Options{Timeout: 8 * time.Second})
	resp, err := c.Search(context.Background(), "фотосинтез", 4)
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(resp.Results) != 1 {
		t.Errorf("results = %d, want 1", len(resp.Results))
	}
}
