package storage

import (
	"strings"
	"testing"
)

// seedStoryFull inserts a story with an explicit channel + source so ranking
// tests can build a skewed affinity profile.
func seedStoryFull(t *testing.T, guid, channel, source, headline, lede string) int64 {
	t.Helper()
	s := &Story{
		GUID:       guid,
		Headline:   headline,
		Lede:       lede,
		ArticleURL: "https://example.com/" + guid,
		Source:     source,
		Channel:    channel,
		Classified: true,
		SeenAt:     nowUnix(),
	}
	if err := InsertStory(s); err != nil {
		t.Fatalf("insert story %s: %v", guid, err)
	}
	var id int64
	if err := Get().QueryRow(`SELECT id FROM stories WHERE guid = ?`, guid).Scan(&id); err != nil {
		t.Fatalf("lookup id %s: %v", guid, err)
	}
	return id
}

func TestForYou_LeansToAffinityAndExcludesRead(t *testing.T) {
	setupTestDB(t)
	const sub = "sub-1"

	// A skewed history: the user reads/bookmarks gaming stories.
	g1 := seedStoryFull(t, "g1", "gaming", "GameSite", "Game one", "")
	g2 := seedStoryFull(t, "g2", "gaming", "GameSite", "Game two", "")
	// Candidates to rank (all unread until we mark some).
	gc := seedStoryFull(t, "gc", "gaming", "GameSite", "Fresh gaming story", "")
	tc := seedStoryFull(t, "tc", "tech", "TechSite", "Fresh tech story", "")

	if err := SetRead(sub, g1, true); err != nil {
		t.Fatalf("read g1: %v", err)
	}
	if err := SetBookmark(sub, g2, true); err != nil {
		t.Fatalf("bookmark g2: %v", err)
	}

	got, err := ForYou(sub, 10)
	if err != nil {
		t.Fatalf("ForYou: %v", err)
	}
	if len(got) == 0 {
		t.Fatal("expected results")
	}
	// Affinity leans gaming, so a gaming story tops the list and the tech story
	// sinks to the bottom. (gc and the unread-but-bookmarked g2 both qualify and
	// tie on score, so we assert on channel rather than a specific id.)
	if got[0].Channel != "gaming" {
		t.Fatalf("expected a gaming story first, got channel %q (id %d)", got[0].Channel, got[0].ID)
	}
	if got[len(got)-1].ID != tc {
		t.Fatalf("expected tech candidate ranked last, got id %d", got[len(got)-1].ID)
	}
	// Read stories must never appear.
	for _, s := range got {
		if s.ID == g1 {
			t.Fatalf("read story g1 leaked into ForYou")
		}
	}
	// Sanity: the gaming candidate that was never touched should be present.
	var sawGC bool
	for _, s := range got {
		if s.ID == gc {
			sawGC = true
		}
	}
	if !sawGC {
		t.Fatalf("expected unread gaming candidate gc in results")
	}
}

func TestForYou_NoHistoryReturnsNil(t *testing.T) {
	setupTestDB(t)
	seedStoryFull(t, "a", "tech", "TechSite", "Something", "")
	got, err := ForYou("nobody", 10)
	if err != nil {
		t.Fatalf("ForYou: %v", err)
	}
	if got != nil {
		t.Fatalf("expected nil for a user with no history, got %d rows", len(got))
	}
}

func TestRelatedStories_OnTopicExcludesSelf(t *testing.T) {
	setupTestDB(t)
	seed := seedStoryFull(t, "seed", "tech", "TechSite",
		"Apple unveils new iPhone camera", "The new camera sensor is larger.")
	rel := seedStoryFull(t, "rel", "tech", "OtherSite",
		"iPhone camera teardown reveals sensor", "A look at the new iPhone camera.")
	seedStoryFull(t, "off", "gaming", "GameSite",
		"Nintendo announces handheld", "A brand new portable console.")

	got, err := RelatedStories(seed, 5)
	if err != nil {
		t.Fatalf("RelatedStories: %v", err)
	}
	if len(got) == 0 {
		t.Fatal("expected at least one related story")
	}
	for _, s := range got {
		if s.ID == seed {
			t.Fatal("seed story returned as its own related")
		}
	}
	if got[0].ID != rel {
		t.Fatalf("expected the on-topic iPhone story first, got id %d", got[0].ID)
	}
}

func TestBuildRelatedFTSQuery(t *testing.T) {
	// Stopwords and 1-char tokens drop out; the rest become an OR of quoted terms.
	q := buildRelatedFTSQuery("The new iPhone camera is a big deal")
	if q == "" {
		t.Fatal("expected a non-empty query")
	}
	for _, bad := range []string{`"the"`, `"is"`, `"new"`} {
		if strings.Contains(q, bad) {
			t.Fatalf("stopword leaked into query: %s in %q", bad, q)
		}
	}
	for _, want := range []string{`"iphone"`, `"camera"`} {
		if !strings.Contains(q, want) {
			t.Fatalf("expected %s in query %q", want, q)
		}
	}
	if buildRelatedFTSQuery("the a of to") != "" {
		t.Fatal("expected empty query when only stopwords remain")
	}
}
