package rss

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"
)

// undatedFeed — a feed whose items carry no pubDate. Plenty of real ones do not.
func undatedFeed(titles ...string) string {
	var b strings.Builder
	b.WriteString(`<?xml version="1.0"?><rss version="2.0"><channel><title>u</title>`)
	for _, t := range titles {
		fmt.Fprintf(&b, `<item><title>%s</title><link>https://example.org/%s</link><guid>%s</guid></item>`, t, t, t)
	}
	b.WriteString(`</channel></rss>`)
	return b.String()
}

// datedFeed — newest first, one hour apart, the standard shape.
func datedFeed(base time.Time, n int) string {
	var b strings.Builder
	b.WriteString(`<?xml version="1.0"?><rss version="2.0"><channel><title>d</title>`)
	for i := 0; i < n; i++ {
		ts := base.Add(-time.Duration(i) * time.Hour)
		fmt.Fprintf(&b, `<item><title>item-%d</title><link>https://example.org/%d</link><guid>g%d</guid><pubDate>%s</pubDate></item>`,
			i, i, i, ts.Format(time.RFC1123Z))
	}
	b.WriteString(`</channel></rss>`)
	return b.String()
}

func TestPoll_UndatedFeedIsNotReNotedAfterARestart(t *testing.T) {
	// The seen-IDs map dies with the process, so before the durable mark was
	// consulted every boot re-noted the whole front page, stamped now, on top of
	// the recent-notes window. A crash loop made that a flood.
	feed := FeedConfig{Name: "u", URL: "https://example.org/rss"}
	fetch := &fakeFetch{body: undatedFeed("a", "b", "c")}
	marks := newMarks()

	notes1 := &fakeNotes{}
	p1 := NewPoller([]FeedConfig{feed}, fetch, notes1, marks, nil, nil, Config{})
	p1.PollDue(context.Background(), now)
	if len(notes1.notes) != 3 {
		t.Fatalf("first boot wrote %d notes, want 3", len(notes1.notes))
	}
	if marks.m["u"].IsZero() {
		t.Fatal("an undated feed left no mark, so the next process cannot tell it has been read")
	}

	// Restart. Same process-lifetime dedup map, gone.
	notes2 := &fakeNotes{}
	p2 := NewPoller([]FeedConfig{feed}, fetch, notes2, marks, nil, nil, Config{})
	p2.PollDue(context.Background(), now.Add(time.Hour))
	if len(notes2.notes) != 0 {
		t.Fatalf("a restart re-noted %d undated items: %v", len(notes2.notes), notes2.notes)
	}
	// And the same process still notices something genuinely new.
	fetch.body = undatedFeed("a", "b", "c", "d")
	p2.PollDue(context.Background(), now.Add(2*time.Hour))
	if len(notes2.notes) != 1 {
		t.Fatalf("a new undated item after the resync wrote %d notes, want 1", len(notes2.notes))
	}
}

func TestPoll_BurstLargerThanMaxItemsIsPacedNotDropped(t *testing.T) {
	// max_items reads as pacing in the config doc. Marking the newest item
	// written put everything below the cap behind the mark, permanently.
	feed := FeedConfig{Name: "d", URL: "https://example.org/rss"}
	fetch := &fakeFetch{body: datedFeed(now.Add(-time.Minute), 12)}
	notes := &fakeNotes{}
	marks := newMarks()
	p := NewPoller([]FeedConfig{feed}, fetch, notes, marks, nil, nil, Config{MaxItems: 5, MaxAge: 48 * time.Hour})

	at := now
	for i := 0; i < 3; i++ {
		if _, err := p.PollFeed(context.Background(), feed, at); err != nil {
			t.Fatal(err)
		}
		at = at.Add(time.Hour)
	}
	seen := map[string]bool{}
	for _, n := range notes.notes {
		title := strings.SplitN(n.text, "\n", 2)[0]
		if seen[title] {
			t.Errorf("item %q was noted twice", title)
		}
		seen[title] = true
	}
	if len(seen) != 12 {
		t.Errorf("after three polls of a 12-item burst she has %d of them; the rest were dropped for good", len(seen))
	}
}

func TestNoteHeadlineAndCategory(t *testing.T) {
	text := NoteText(FeedConfig{Category: "технологии"}, Item{
		Title: "Новая уязвимость", Summary: "Патч вышел", Link: "https://example.org/a",
	})
	if got := NoteHeadline(text); got != "Новая уязвимость" {
		t.Errorf("NoteHeadline = %q; she reads the brackets out loud", got)
	}
	if got := NoteCategory(text); got != "технологии" {
		t.Errorf("NoteCategory = %q, want технологии", got)
	}
	// A feed's own leading tag stays part of the title.
	if got := NoteHeadline("[перевод] Что-то"); got != "[перевод] Что-то" {
		t.Errorf("NoteHeadline stripped the feed's own tag: %q", got)
	}
	if got := NoteCategory("Без категории"); got != "" {
		t.Errorf("NoteCategory on an untagged note = %q, want empty", got)
	}
}
