package web

import (
	"encoding/json"
	"encoding/xml"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"pete/internal/config"
	"pete/internal/storage"
)

// TestFeeds exercises the outbound RSS and JSON feeds end to end: a classified
// story appears in both formats, carrying its canonical link, full content, and
// a real pubDate; a channel-scoped feed excludes stories from other channels.
func TestFeeds(t *testing.T) {
	storage.Close()
	if err := storage.Init(filepath.Join(t.TempDir(), "feed.db")); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { storage.Close() })

	pub := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC).Unix()
	tech := &storage.Story{
		GUID:         "feed-tech-1",
		Headline:     "Chips & Dips: A Tech Tale",
		Lede:         "The lede for the tech story.",
		Content:      "First paragraph.\n\nSecond paragraph with <html> & symbols.",
		ArticleURL:   "https://example.com/tech/story?utm=x",
		URLCanonical: "https://example.com/tech/story",
		Source:       "Example Wire",
		Channel:      "tech",
		Classified:   true,
		SeenAt:       time.Now().Unix(),
		PublishedAt:  pub,
	}
	gaming := &storage.Story{
		GUID:       "feed-gaming-1",
		Headline:   "A Gaming Headline",
		Lede:       "Gaming lede.",
		ArticleURL: "https://example.com/gaming/story",
		Source:     "Play Wire",
		Channel:    "gaming",
		Classified: true,
		SeenAt:     time.Now().Unix(),
	}
	for _, st := range []*storage.Story{tech, gaming} {
		if err := storage.InsertStory(st); err != nil {
			t.Fatal(err)
		}
	}

	s, err := New(config.WebConfig{SiteTitle: "Pete", ListenAddr: ":0", BaseURL: "https://news.example/"}, nil, true, config.AdventureConfig{}, nil)
	if err != nil {
		t.Fatal(err)
	}

	// --- Site-wide RSS ---
	rw := httptest.NewRecorder()
	s.handleFeedXML(rw, httptest.NewRequest("GET", "/feed.xml", nil), "")
	if rw.Code != 200 {
		t.Fatalf("rss status = %d", rw.Code)
	}
	if ct := rw.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/rss+xml") {
		t.Errorf("rss content-type = %q", ct)
	}
	var rss struct {
		Channel struct {
			Title    string `xml:"title"`
			AtomLink struct {
				Href string `xml:"href,attr"`
			} `xml:"link"`
			Items []struct {
				Title   string `xml:"title"`
				Link    string `xml:"link"`
				GUID    string `xml:"guid"`
				PubDate string `xml:"pubDate"`
				Content string `xml:"encoded"` // content:encoded
			} `xml:"item"`
		} `xml:"channel"`
	}
	if err := xml.Unmarshal(rw.Body.Bytes(), &rss); err != nil {
		t.Fatalf("rss did not parse as XML: %v\n%s", err, rw.Body.String())
	}
	if len(rss.Channel.Items) != 2 {
		t.Fatalf("rss items = %d, want 2", len(rss.Channel.Items))
	}
	// Newest-first: PublishedAt March 2026 vs gaming's seen_at (now) — gaming is
	// newer, so it sorts first. Find the tech item to assert on.
	var techItem *struct {
		Title   string `xml:"title"`
		Link    string `xml:"link"`
		GUID    string `xml:"guid"`
		PubDate string `xml:"pubDate"`
		Content string `xml:"encoded"`
	}
	for i := range rss.Channel.Items {
		if rss.Channel.Items[i].GUID == "feed-tech-1" {
			techItem = &rss.Channel.Items[i]
		}
	}
	if techItem == nil {
		t.Fatal("tech item missing from rss")
	}
	if techItem.Link != "https://example.com/tech/story" {
		t.Errorf("rss link = %q, want canonical", techItem.Link)
	}
	if techItem.Title != "Chips & Dips: A Tech Tale" {
		t.Errorf("rss title = %q", techItem.Title)
	}
	if !strings.Contains(techItem.Content, "<p>First paragraph.</p>") {
		t.Errorf("rss content missing paragraph markup: %q", techItem.Content)
	}
	if !strings.Contains(techItem.Content, "&lt;html&gt;") {
		t.Errorf("rss content did not escape embedded markup: %q", techItem.Content)
	}
	if !strings.Contains(techItem.PubDate, "2026") {
		t.Errorf("rss pubDate = %q, want the published date", techItem.PubDate)
	}
	if !strings.Contains(rw.Body.String(), `href="https://news.example/feed.xml"`) {
		t.Errorf("rss missing atom self link with base_url")
	}

	// --- Site-wide JSON Feed ---
	rw = httptest.NewRecorder()
	s.handleFeedJSON(rw, httptest.NewRequest("GET", "/feed.json", nil), "")
	if rw.Code != 200 {
		t.Fatalf("json status = %d", rw.Code)
	}
	if ct := rw.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/feed+json") {
		t.Errorf("json content-type = %q", ct)
	}
	var jf struct {
		Version     string `json:"version"`
		FeedURL     string `json:"feed_url"`
		HomePageURL string `json:"home_page_url"`
		Items       []struct {
			ID          string   `json:"id"`
			URL         string   `json:"url"`
			Title       string   `json:"title"`
			ContentText string   `json:"content_text"`
			Tags        []string `json:"tags"`
			Authors     []struct {
				Name string `json:"name"`
			} `json:"authors"`
		} `json:"items"`
	}
	if err := json.Unmarshal(rw.Body.Bytes(), &jf); err != nil {
		t.Fatalf("json feed did not parse: %v", err)
	}
	if !strings.Contains(jf.Version, "jsonfeed.org") {
		t.Errorf("json version = %q", jf.Version)
	}
	if jf.FeedURL != "https://news.example/feed.json" {
		t.Errorf("json feed_url = %q", jf.FeedURL)
	}
	if len(jf.Items) != 2 {
		t.Fatalf("json items = %d, want 2", len(jf.Items))
	}
	var jTech *struct {
		ID          string   `json:"id"`
		URL         string   `json:"url"`
		Title       string   `json:"title"`
		ContentText string   `json:"content_text"`
		Tags        []string `json:"tags"`
		Authors     []struct {
			Name string `json:"name"`
		} `json:"authors"`
	}
	for i := range jf.Items {
		if jf.Items[i].ID == "feed-tech-1" {
			jTech = &jf.Items[i]
		}
	}
	if jTech == nil {
		t.Fatal("tech item missing from json feed")
	}
	if jTech.URL != "https://example.com/tech/story" {
		t.Errorf("json url = %q, want canonical", jTech.URL)
	}
	if !strings.Contains(jTech.ContentText, "Second paragraph") {
		t.Errorf("json content_text = %q", jTech.ContentText)
	}
	if len(jTech.Tags) != 1 || jTech.Tags[0] != "tech" {
		t.Errorf("json tags = %v", jTech.Tags)
	}
	if len(jTech.Authors) != 1 || jTech.Authors[0].Name != "Example Wire" {
		t.Errorf("json authors = %v", jTech.Authors)
	}

	// --- Channel-scoped RSS excludes other channels ---
	rw = httptest.NewRecorder()
	s.handleFeedXML(rw, httptest.NewRequest("GET", "/tech/feed.xml", nil), "tech")
	rss.Channel.Items = nil // xml.Unmarshal appends; clear the site-wide items
	if err := xml.Unmarshal(rw.Body.Bytes(), &rss); err != nil {
		t.Fatalf("channel rss parse: %v", err)
	}
	if len(rss.Channel.Items) != 1 {
		t.Fatalf("channel rss items = %d, want 1", len(rss.Channel.Items))
	}
	if rss.Channel.Items[0].GUID != "feed-tech-1" {
		t.Errorf("channel rss wrong item: %q", rss.Channel.Items[0].GUID)
	}
	if !strings.Contains(rss.Channel.Title, "Tech") {
		t.Errorf("channel rss title = %q, want channel name", rss.Channel.Title)
	}
}
