package main

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

	"github.com/kami/maven/internal/config"
	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/kiwix"
	"github.com/kami/maven/internal/phraser"
	"github.com/kami/maven/internal/router"
	"github.com/kami/maven/internal/voice"
)

// searchRSS is what kiwix-serve answers a /search with, trimmed to the fields
// ParseSearchRSS reads.
func searchRSS(items ...string) string {
	return `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel>` +
		strings.Join(items, "") + `</channel></rss>`
}

func rssItem(title, snippet string) string {
	return "<item><title>" + title + "</title><link>/x</link><description>" + snippet + "</description></item>"
}

// stubKiwixServer answers every search with the given body and records the
// pattern it was asked for, so a test can assert on what left the process.
type stubKiwixServer struct {
	*httptest.Server
	lastPattern string
	lastBook    string
}

func newStubKiwix(t *testing.T, body string, status int) *stubKiwixServer {
	t.Helper()
	s := &stubKiwixServer{}
	s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if status != 0 && status != http.StatusOK {
			w.WriteHeader(status)
			return
		}
		// Two endpoints on one server: /search answers the RSS, everything else
		// is an article read. Only the search is recorded — an article fetch
		// carries no query string and would blank the assertions.
		if r.URL.Path != "/search" {
			w.Header().Set("Content-Type", "text/html")
			_, _ = w.Write([]byte("<html><title>Article</title><body><p>the lead paragraph</p></body></html>"))
			return
		}
		s.lastPattern = r.URL.Query().Get("pattern")
		s.lastBook = r.URL.Query().Get("books.name")
		w.Header().Set("Content-Type", "application/xml")
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(s.Close)
	return s
}

// buildKiwixHandler wires the source with no rewriter: the question is searched
// verbatim, which keeps the assertion about what was sent unambiguous.
func buildKiwixHandler(base string) *reactiveHandler {
	return &reactiveHandler{
		replier: voice.NewStubReplier(),
		phraser: phraser.NewStub(),
		kiwix: &kiwixWiring{
			client: kiwix.New(base),
			book:   "wikipedia_en_all_maxi",
			max:    config.DefaultKiwixResults,
			runes:  config.DefaultKiwixSnippetRunes,
		},
	}
}

func askKiwix(h *reactiveHandler, q string) (string, bool) {
	return h.queryKiwix(context.Background(), &queryTurn{
		dec: router.Decision{Intent: router.IntentQuery, Utterance: q},
	})
}

// The default daemon has no `kiwix` block, and a source that is off must not
// claim the turn — the model answers next, exactly as it did before.
func TestQueryKiwixOffPassesThrough(t *testing.T) {
	h := &reactiveHandler{replier: voice.NewStubReplier(), phraser: phraser.NewStub()}
	if reply, ok := askKiwix(h, "почему небо синее?"); ok {
		t.Errorf("an unconfigured kiwix claimed the turn: %q", reply)
	}
}

func TestQueryKiwixAnswersFromSnippets(t *testing.T) {
	s := newStubKiwix(t, searchRSS(rssItem("Rayleigh scattering", "shorter wavelengths scatter more")), 0)
	h := buildKiwixHandler(s.URL)

	reply, ok := askKiwix(h, "почему небо синее?")
	if !ok {
		t.Fatal("kiwix found a hit and did not claim the turn")
	}
	if reply == "" {
		t.Error("claimed the turn with an empty reply")
	}
	if s.lastBook != "wikipedia_en_all_maxi" {
		t.Errorf("books.name = %q, want the configured book", s.lastBook)
	}
}

// No hit is not a failure worth announcing: the ZIM does not cover it, and the
// model answering next beats "ничего не нашла".
func TestQueryKiwixNoHitsPassesThrough(t *testing.T) {
	s := newStubKiwix(t, searchRSS(), 0)
	if reply, ok := askKiwix(buildKiwixHandler(s.URL), "почему небо синее?"); ok {
		t.Errorf("an empty result set claimed the turn: %q", reply)
	}
}

// A dead or misconfigured server must degrade to the model, not to an error
// spoken out loud. A turn never breaks on a capability.
func TestQueryKiwixServerErrorPassesThrough(t *testing.T) {
	s := newStubKiwix(t, "", http.StatusBadRequest)
	if reply, ok := askKiwix(buildKiwixHandler(s.URL), "почему небо синее?"); ok {
		t.Errorf("a 400 claimed the turn: %q", reply)
	}
}

// The privacy rule in CLAUDE.md, asserted rather than assumed: only the
// utterance is searched. No note, no fact, no persona block travels with it.
func TestQueryKiwixSendsOnlyTheQuestion(t *testing.T) {
	s := newStubKiwix(t, searchRSS(rssItem("X", "y")), 0)
	h := buildKiwixHandler(s.URL)
	// A turn carrying notes an earlier source already pulled. They must not
	// reach the query string.
	_, _ = h.queryKiwix(context.Background(), &queryTurn{
		dec:   router.Decision{Intent: router.IntentQuery, Utterance: "почему небо синее?"},
		notes: []ipc.Note{{Text: "пароль от роутера hunter2"}},
	})
	if strings.Contains(s.lastPattern, "hunter2") {
		t.Fatalf("a stored note leaked into the search query: %q", s.lastPattern)
	}
	if s.lastPattern != "почему небо синее?" {
		t.Errorf("pattern = %q, want the utterance verbatim", s.lastPattern)
	}
}

func TestWireKiwixOffWithoutABlock(t *testing.T) {
	if w := wireKiwix(&config.Config{}, nil); w != nil {
		t.Error("wireKiwix built a source with no config block")
	}
}

// No llama-server means no rewriter, but the source still works: searching the
// question verbatim is the honest degraded mode, not a reason to stay dark.
func TestWireKiwixWithoutAnLLMHasNoRewriter(t *testing.T) {
	w := wireKiwix(&config.Config{Kiwix: &config.KiwixConfig{
		URL: "http://kiwix:8080", Book: "b", MaxResults: 5, SnippetRunes: 1500,
	}}, nil)
	if w == nil {
		t.Fatal("wireKiwix returned nil for a configured block")
	}
	if w.rewriter != nil {
		t.Error("built a rewriter with no llm client")
	}
	if w.book != "b" {
		t.Errorf("book = %q", w.book)
	}
}

// The whole point of reading the article: kiwix's own snippet is usually the
// navigation box at the foot of the page, so the lead paragraph must be what
// reaches the phraser.
func TestQueryKiwixReadsTheArticleNotTheSnippet(t *testing.T) {
	junk := "Ecological economics Ecological footprint Ecological forecasting"
	s := newStubKiwix(t, searchRSS(rssItem("Photosynthesis", junk)), 0)
	h := buildKiwixHandler(s.URL)
	h.phraser = nil // no phraser ⇒ the fallback reads back what it was given

	reply, ok := askKiwix(h, "что такое фотосинтез?")
	if !ok {
		t.Fatal("did not claim the turn")
	}
	if !strings.Contains(reply, "the lead paragraph") {
		t.Errorf("reply did not come from the article: %q", reply)
	}
	if strings.Contains(reply, "Ecological economics") {
		t.Errorf("recited the navigation-box snippet: %q", reply)
	}
}
