internal/index/search_test.go

package index

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"

	"mercemay.top/src/tilstream/internal/render"
)

func sample() []render.Post {
	return []render.Post{
		{Slug: "bravo", Title: "Bravo", Date: day("2024-12-01"), Tags: []string{"go", "lambda"}, Body: "Bravo notes about *emphasis* and `code` blocks."},
		{Slug: "alpha", Title: "Alpha", Date: day("2024-11-20"), Tags: []string{"go"}, Body: "# Alpha\n\nSome [anchor](http://x) text."},
		{Slug: "charlie", Title: "Charlie", Date: day("2024-10-02"), Tags: []string{"hugo"}, Body: "draft-only content", Draft: true},
	}
}

func day(s string) time.Time {
	t, err := time.Parse("2006-01-02", s)
	if err != nil {
		panic(err)
	}
	return t
}

func TestBuild_SortsAndDropsDrafts(t *testing.T) {
	t.Parallel()

	docs := Build(sample())

	want := []Doc{
		{ID: "alpha", Title: "Alpha", Tags: []string{"go"}, Body: "# alpha some anchor text."},
		{ID: "bravo", Title: "Bravo", Tags: []string{"go", "lambda"}, Body: "bravo notes about emphasis and  blocks."},
	}
	if diff := cmp.Diff(want, docs); diff != "" {
		t.Fatalf("Build(-want +got):\n%s", diff)
	}
}

func TestBuild_NormalisesCodeFences(t *testing.T) {
	t.Parallel()

	posts := []render.Post{{
		Slug: "z", Title: "Z",
		Body: "pre\n```go\nsecret := 42\n```\npost",
	}}
	docs := Build(posts)
	if strings.Contains(docs[0].Body, "secret") {
		t.Fatalf("code fence body should be stripped: %q", docs[0].Body)
	}
	if !strings.Contains(docs[0].Body, "pre") || !strings.Contains(docs[0].Body, "post") {
		t.Fatalf("surrounding text dropped: %q", docs[0].Body)
	}
}

func TestWriteJSON_RoundTrip(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	path := filepath.Join(dir, "search.json")

	if err := WriteJSON(path, Build(sample())); err != nil {
		t.Fatalf("WriteJSON: %v", err)
	}

	t.Cleanup(func() { _ = os.Remove(path) })

	raw, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	var rt []Doc
	if err := json.Unmarshal(raw, &rt); err != nil {
		t.Fatalf("search.json is not valid JSON: %v", err)
	}
	if len(rt) != 2 {
		t.Fatalf("expected 2 docs on disk, got %d", len(rt))
	}
}

func TestTagCounts(t *testing.T) {
	t.Parallel()

	counts := TagCounts(sample())
	want := map[string]int{"go": 2, "lambda": 1}
	if diff := cmp.Diff(want, counts); diff != "" {
		t.Fatalf("TagCounts(-want +got):\n%s", diff)
	}
}

func TestRelated_PrefersOverlapThenDate(t *testing.T) {
	t.Parallel()

	posts := []render.Post{
		{Slug: "a", Tags: []string{"go", "lambda"}, Date: day("2024-12-01")},
		{Slug: "b", Tags: []string{"go"}, Date: day("2024-11-20")},
		{Slug: "c", Tags: []string{"lambda"}, Date: day("2024-10-02")},
		{Slug: "d", Tags: []string{"hugo"}, Date: day("2024-09-01")},
	}

	got := Related(posts[0], posts, 5)
	if len(got) != 2 {
		t.Fatalf("expected 2 related posts, got %d: %+v", len(got), got)
	}
	if got[0].Slug != "b" {
		t.Fatalf("higher overlap should rank first, got order: %s, %s", got[0].Slug, got[1].Slug)
	}
}

func TestRelated_DropsDrafts(t *testing.T) {
	t.Parallel()

	posts := []render.Post{
		{Slug: "a", Tags: []string{"go"}, Date: day("2024-12-01")},
		{Slug: "b", Tags: []string{"go"}, Date: day("2024-11-20"), Draft: true},
	}
	got := Related(posts[0], posts, 5)
	if len(got) != 0 {
		t.Fatalf("drafts must not appear in related: %+v", got)
	}
}

func TestNormalise_StripsCurlyQuotes(t *testing.T) {
	t.Parallel()

	got := normalise(`He said “hello” and ‘goodbye’.`)
	if strings.ContainsAny(got, `“”‘’`) {
		t.Fatalf("curly quotes leaked: %q", got)
	}
}