package api

import (
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/ponzischeme89/memby/server/internal/config"
)

func importPayload(movieID, fileID int, title string, year int) radarrWebhookPayload {
	var payload radarrWebhookPayload
	payload.EventType = "Download"
	payload.Movie.ID = movieID
	payload.Movie.Title = title
	payload.Movie.Year = year
	payload.MovieFile.ID = fileID
	return payload
}

func TestRadarrImportAlertAnnouncesANewFilm(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)

	alert, ok := radarrImportAlert(importPayload(412, 9001, "Mr. Smith Goes to Washington", 1939), now)
	if !ok {
		t.Fatal("expected an import to be announced")
	}
	if alert.ID != "radarr:412:file:9001" {
		t.Errorf("alert id = %q, want it keyed on the imported file", alert.ID)
	}
	if alert.Kind != alertKindRadarrImport {
		t.Errorf("kind = %q, want %q", alert.Kind, alertKindRadarrImport)
	}
	if alert.Label == "" {
		t.Error("want a label: the app cannot know the wording for a kind it predates")
	}
	if alert.Title != "Mr. Smith Goes to Washington (1939)" {
		t.Errorf("title = %q, want the year alongside it", alert.Title)
	}
	if !strings.Contains(alert.Message, "available in Emby shortly") {
		t.Errorf("message = %q, want it to say the film is on its way", alert.Message)
	}
	// The image proxy serves Radarr covers under this pair, so the banner has a poster
	// before Emby has scanned the film in.
	if alert.ItemID != "radarr:412" || alert.ImageTag != "radarr" {
		t.Errorf("artwork = %q/%q, want the radarr media cover", alert.ItemID, alert.ImageTag)
	}
	if alert.AiredAt != now.Format(time.RFC3339) {
		t.Errorf("airedAt = %q, want the import time so it sorts with the rest", alert.AiredAt)
	}
}

func TestRadarrImportAlertIgnoresEventsThatAreNotANewFilm(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)

	upgrade := importPayload(412, 9002, "Mr. Smith Goes to Washington", 1939)
	upgrade.IsUpgrade = true

	grab := importPayload(413, 0, "Some Film", 2024)
	grab.EventType = "Grab"

	untitled := importPayload(414, 9003, "   ", 2024)

	unknownMovie := importPayload(0, 9004, "No Id", 2024)

	for name, payload := range map[string]radarrWebhookPayload{
		"quality upgrade of a film already there": upgrade,
		"grabbed but not imported":                grab,
		"no title":                                untitled,
		"no movie id":                             unknownMovie,
	} {
		if _, ok := radarrImportAlert(payload, now); ok {
			t.Errorf("%s: expected no alert", name)
		}
	}
}

func TestRadarrImportAlertAcceptsTheNewerEventName(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	payload := importPayload(415, 9005, "Rear Window", 1954)
	payload.EventType = "MovieFileImported"

	if _, ok := radarrImportAlert(payload, now); !ok {
		t.Error("expected the alternate import event name to be announced")
	}
}

// A file id is what makes a repeated notification the same news; without one the id
// falls back to the clock so a re-import is not silently swallowed.
func TestRadarrImportAlertWithoutAFileIDIsStillAnnounced(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	alert, ok := radarrImportAlert(importPayload(416, 0, "Sabotage", 1936), now)
	if !ok {
		t.Fatal("expected an alert")
	}
	if !strings.HasPrefix(alert.ID, "radarr:416:imported:") {
		t.Errorf("alert id = %q, want a time-keyed fallback", alert.ID)
	}
}

func TestAppendAlertPrunesExpiredAndDeduplicates(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	existing := []storedAlert{
		{
			Alert:     clientAlert{ID: "radarr:1:file:1", AiredAt: now.Add(-4 * time.Hour).Format(time.RFC3339)},
			ExpiresAt: now.Add(-time.Minute),
		},
		{
			Alert:     clientAlert{ID: "radarr:2:file:2", AiredAt: now.Add(-time.Hour).Format(time.RFC3339)},
			ExpiresAt: now.Add(2 * time.Hour),
		},
		{
			Alert:     clientAlert{ID: "radarr:3:file:3", AiredAt: now.Add(-2 * time.Hour).Format(time.RFC3339)},
			ExpiresAt: now.Add(time.Hour),
		},
	}
	// Radarr delivering the same import twice must not stack two banners.
	repeat := clientAlert{ID: "radarr:3:file:3", AiredAt: now.Format(time.RFC3339)}

	stored := appendAlert(existing, repeat, now.Add(3*time.Hour), now)

	if len(stored) != 2 {
		t.Fatalf("expected 2 stored alerts, got %d: %+v", len(stored), stored)
	}
	if stored[0].Alert.ID != "radarr:3:file:3" {
		t.Errorf("newest first: got %q", stored[0].Alert.ID)
	}
	if stored[1].Alert.ID != "radarr:2:file:2" {
		t.Errorf("expected the unexpired older alert to survive, got %q", stored[1].Alert.ID)
	}
}

func TestAppendAlertKeepsOnlyTheNewestFew(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	var stored []storedAlert
	// A bulk import: many films land at once and every one of them is current.
	for i := 0; i < maxStoredAlerts+5; i++ {
		alert := clientAlert{
			ID:      "radarr:" + time.Duration(i).String(),
			AiredAt: now.Add(time.Duration(i) * time.Minute).Format(time.RFC3339),
		}
		stored = appendAlert(stored, alert, now.Add(3*time.Hour), now)
	}
	if len(stored) != maxStoredAlerts {
		t.Fatalf("stored %d alerts, want a cap of %d", len(stored), maxStoredAlerts)
	}
}

func TestLiveAlertsDropsClosedWindows(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	alerts := liveAlerts([]storedAlert{
		{Alert: clientAlert{ID: "current"}, ExpiresAt: now.Add(time.Minute)},
		{Alert: clientAlert{ID: "stale"}, ExpiresAt: now.Add(-time.Second)},
	}, now)

	if len(alerts) != 1 || alerts[0].ID != "current" {
		t.Fatalf("expected only the current alert, got %+v", alerts)
	}
}

func TestMergeAlertsOrdersNewestFirstAcrossSources(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	at := func(d time.Duration) string { return now.Add(d).Format(time.RFC3339) }

	merged := mergeAlerts(
		[]clientAlert{{ID: "movie-recent", AiredAt: at(-10 * time.Minute)}},
		[]clientAlert{
			{ID: "episode-older", AiredAt: at(-2 * time.Hour)},
			{ID: "episode-newest", AiredAt: at(-time.Minute)},
		},
	)

	want := []string{"episode-newest", "movie-recent", "episode-older"}
	for i, id := range want {
		if merged[i].ID != id {
			t.Fatalf("merged[%d] = %q, want %q (%+v)", i, merged[i].ID, id, merged)
		}
	}
}

func TestMergeAlertsCapsWhatOneTVIsShown(t *testing.T) {
	now := time.Date(2026, 7, 31, 19, 4, 0, 0, time.UTC)
	var many []clientAlert
	for i := 0; i < maxAlerts+3; i++ {
		many = append(many, clientAlert{
			ID:      "alert-" + time.Duration(i).String(),
			AiredAt: now.Add(-time.Duration(i) * time.Minute).Format(time.RFC3339),
		})
	}
	if got := len(mergeAlerts(many)); got != maxAlerts {
		t.Fatalf("merged %d alerts, want a cap of %d", got, maxAlerts)
	}
}

func TestWebhookTokenIsReadFromEveryFormRadarrCanSend(t *testing.T) {
	for name, build := range map[string]func() *http.Request{
		"header": func() *http.Request {
			r := newWebhookRequest("/hooks/radarr")
			r.Header.Set("X-Memby-Token", "secret")
			return r
		},
		"bearer": func() *http.Request {
			r := newWebhookRequest("/hooks/radarr")
			r.Header.Set("Authorization", "Bearer secret")
			return r
		},
		"basic auth": func() *http.Request {
			r := newWebhookRequest("/hooks/radarr")
			r.SetBasicAuth("memby", "secret")
			return r
		},
		"query": func() *http.Request {
			return newWebhookRequest("/hooks/radarr?token=secret")
		},
	} {
		if got := webhookToken(build()); got != "secret" {
			t.Errorf("%s: token = %q, want %q", name, got, "secret")
		}
	}
}

func newWebhookRequest(target string) *http.Request {
	return httptest.NewRequest(http.MethodPost, target, strings.NewReader("{}"))
}

func TestRadarrWebhookIsHiddenUntilATokenIsConfigured(t *testing.T) {
	s := &Server{cfg: config.Config{}, log: discardLogger()}
	rec := httptest.NewRecorder()

	s.handleRadarrWebhook(rec, newWebhookRequest("/hooks/radarr?token=anything"))

	if rec.Code != http.StatusNotFound {
		t.Fatalf("got %d, want 404 for an unconfigured hook", rec.Code)
	}
}

func TestRadarrWebhookRejectsAWrongToken(t *testing.T) {
	s := &Server{
		cfg: config.Config{RadarrWebhookToken: "hook-secret", RadarrAlertWindow: time.Hour},
		log: discardLogger(),
	}
	rec := httptest.NewRecorder()

	s.handleRadarrWebhook(rec, newWebhookRequest("/hooks/radarr?token=guess"))

	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("got %d, want 401", rec.Code)
	}
}

// Radarr's Test button has to succeed without putting a film that does not exist on
// every television in the house.
func TestRadarrWebhookTestEventAnnouncesNothing(t *testing.T) {
	s := &Server{
		cfg: config.Config{RadarrWebhookToken: "hook-secret", RadarrAlertWindow: time.Hour},
		log: discardLogger(),
	}
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(
		http.MethodPost, "/hooks/radarr?token=hook-secret",
		strings.NewReader(`{"eventType":"Test","movie":{"id":1,"title":"Test Title"}}`),
	)

	// A nil cache would panic if this reached the store, which is the assertion.
	s.handleRadarrWebhook(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("got %d, want 200", rec.Code)
	}
}

func discardLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}
