package api

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/ponzischeme89/memby/server/internal/recommend"
	"github.com/ponzischeme89/memby/server/internal/store"
)

func TestPersonalizeHomeRowsGraduallyDemotesIgnoredRows(t *testing.T) {
	rows := []recommend.Row{
		{ID: "continue"},
		{ID: "ignored"},
		{ID: "new-row"},
		{ID: "engaged"},
	}
	stats := []store.RowStat{
		{RowID: "ignored", Impressions: 20},
		// Under the five-impression confidence floor, new-row keeps its authored score.
		{RowID: "new-row", Impressions: 4},
		{RowID: "engaged", Impressions: 20, Focuses: 8, Selects: 3, DwellMs: 120_000},
	}

	got := personalizeHomeRows(rows, stats)
	want := []string{"continue", "engaged", "new-row", "ignored"}
	for i, id := range want {
		if got[i].ID != id {
			t.Fatalf("row %d = %q, want %q; rows=%+v", i, got[i].ID, id, got)
		}
	}
}

func TestContinueWatchingPromotesShowsAiringToday(t *testing.T) {
	raw := func(value string) json.RawMessage { return json.RawMessage(value) }
	items := []json.RawMessage{
		raw(`{"Id":"ended","Name":"Old episode","Type":"Episode","SeriesName":"Ended Rewatch"}`),
		raw(`{"Id":"movie","Name":"Paused Movie","Type":"Movie"}`),
		raw(`{"Id":"today-1","Name":"Episode 4","Type":"Episode","SeriesName":"The Bear"}`),
		raw(`{"Id":"today-2","Name":"Episode 2","Type":"Episode","SeriesName":"Abbott Elementary"}`),
	}
	schedule := &recommend.Row{Items: []json.RawMessage{
		raw(`{"Name":"The Bear","MembyAirDayLabel":"Today"}`),
		raw(`{"Name":"Tomorrow Show","MembyAirDayLabel":"Tomorrow"}`),
		raw(`{"Name":"Abbott Elementary","MembyAirDayLabel":"Today"}`),
	}}

	got := prioritizeAiringTodayContinue(items, schedule)
	want := []string{"today-1", "today-2", "ended", "movie"}
	for index, item := range recommend.Decode(got) {
		if item.ID != want[index] {
			t.Fatalf("item %d = %q, want %q", index, item.ID, want[index])
		}
	}
}

func TestDifferentHistoriesChangeRowAndPosterOrdering(t *testing.T) {
	now := time.Date(2026, 7, 31, 20, 0, 0, 0, time.UTC)
	item := func(id, name, genre string) recommend.Item {
		raw, _ := json.Marshal(map[string]any{
			"Id": id, "Name": name, "Type": "Movie", "Genres": []string{genre},
			"CommunityRating": 7.0,
		})
		return recommend.Item{
			ID: id, Name: name, Type: "Movie", Genres: []string{genre},
			CommunityRating: 7, Raw: raw,
		}
	}
	dramaA, dramaB := item("drama-a", "Drama A", "Drama"), item("drama-b", "Drama B", "Drama")
	comedyA, comedyB := item("comedy-a", "Comedy A", "Comedy"), item("comedy-b", "Comedy B", "Comedy")
	profileFor := func(genre string) recommend.WeightedProfile {
		return recommend.BuildWeightedProfile([]recommend.ViewingEvidence{
			{Item: item("seen-1", "Seen 1", genre), Completion: 1, OccurredAt: now.Add(-time.Hour)},
			{Item: item("seen-2", "Seen 2", genre), Completion: 1, OccurredAt: now.Add(-2 * time.Hour)},
		}, now, time.UTC)
	}
	pageFor := func(profile recommend.WeightedProfile) []recommend.Row {
		row := func(id string, items []recommend.Item) recommend.Row {
			ranked := recommend.WeightedRank(
				profile, items, nil, recommend.RankIntent{Now: now},
				recommend.DefaultWeightedConfig(), 10,
			)
			raws := make([]json.RawMessage, 0, len(ranked))
			for _, value := range ranked {
				raws = append(raws, recommend.EnrichRankedItem(value))
			}
			return recommend.Row{ID: id, Items: raws}
		}
		return personalizeRowsByTitleScores([]recommend.Row{
			row("drama-row", []recommend.Item{dramaA, dramaB}),
			row("comedy-row", []recommend.Item{comedyA, comedyB}),
			row("mixed-row", []recommend.Item{comedyA, dramaA}),
		})
	}
	dramaPage := pageFor(profileFor("Drama"))
	comedyPage := pageFor(profileFor("Comedy"))
	if dramaPage[0].ID != "drama-row" || comedyPage[0].ID != "comedy-row" {
		t.Fatalf("row orders did not personalize: drama=%s comedy=%s",
			dramaPage[0].ID, comedyPage[0].ID)
	}
	firstMixedID := func(rows []recommend.Row) string {
		for _, row := range rows {
			if row.ID == "mixed-row" {
				return recommend.Decode(row.Items)[0].ID
			}
		}
		return ""
	}
	if firstMixedID(dramaPage) != "drama-a" || firstMixedID(comedyPage) != "comedy-a" {
		t.Fatalf("poster orders did not personalize: drama=%s comedy=%s",
			firstMixedID(dramaPage), firstMixedID(comedyPage))
	}
}

// Continue Watching answers "what was I watching?", so its order belongs to Emby (and to
// mergeContinueWatching). Ranking it by taste buried episodes behind films and pushed a
// just-watched show past the visible cards, which read on a television as the series
// having vanished.
func TestProgressRowsKeepEmbyOrder(t *testing.T) {
	raw := func(id, name, itemType string) json.RawMessage {
		value, _ := json.Marshal(map[string]any{"Id": id, "Name": name, "Type": itemType})
		return value
	}
	items := func() []json.RawMessage {
		return []json.RawMessage{
			raw("episode", "Zulu Company", "Episode"),
			raw("movie", "A Film", "Movie"),
		}
	}
	rows := (&Server{}).personalizeTitles(
		context.Background(),
		store.Session{EmbyUserID: "user"},
		[]recommend.Row{
			{ID: "continue", Items: items()},
			{ID: "curated:movies:drama", Items: items()},
		},
	)
	if got := recommend.Decode(rows[0].Items); len(got) != 2 ||
		got[0].ID != "episode" || got[1].ID != "movie" {
		t.Fatalf("continue row was reordered: %+v", got)
	}
	// The same items in a discovery shelf are still ranked, which is what makes the
	// exemption above a deliberate choice rather than dead code.
	if discovery := recommend.Decode(rows[1].Items); discovery[0].ID != "movie" {
		t.Fatalf("discovery row was not ranked: %+v", discovery)
	}
}

func TestPersonalizeHomeRowsPreservesColdStartDefaults(t *testing.T) {
	rows := []recommend.Row{{ID: "continue"}, {ID: "favorites"}, {ID: "latest"}}
	got := personalizeHomeRows(rows, nil)
	for i := range rows {
		if got[i].ID != rows[i].ID {
			t.Fatalf("cold-start order changed: %+v", got)
		}
	}
}

func TestPreparedHomeRowsPromotesAbandonedShowsAndTimeAwarePicks(t *testing.T) {
	prepared := []recommend.Row{
		{
			ID: "for-you:pick-up", Title: "old pickup title", Kind: "for-you",
			Items: []json.RawMessage{json.RawMessage(`{"Id":"abandoned"}`)},
		},
		{
			ID: "for-you:picks", Title: "old picks title", Kind: "for-you",
			Items: []json.RawMessage{json.RawMessage(`{"Id":"pick"}`)},
		},
		{ID: "for-you:genre:drama", Title: "Drama", Kind: "for-you"},
	}

	rows := preparedHomeForYouRows(prepared, homeForYouWindow{
		ID: "afternoon", Title: "An hour for your afternoon", Minutes: 60,
	})
	if len(rows) != 2 {
		t.Fatalf("home For You rows = %+v", rows)
	}
	if rows[0].ID != "for-you:pick-up" ||
		rows[0].Title != "Pick this show up again" {
		t.Fatalf("pickup row = %+v", rows[0])
	}
	if rows[1].ID != "for-you:home:afternoon" ||
		rows[1].Title != "An hour for your afternoon" {
		t.Fatalf("time-aware row = %+v", rows[1])
	}
}
