package api

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

func continueRaw(t *testing.T, fields map[string]any) json.RawMessage {
	t.Helper()
	raw, err := json.Marshal(fields)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	return raw
}

func resumeItem(t *testing.T, id, seriesID, playedAt string) json.RawMessage {
	t.Helper()
	return continueRaw(t, map[string]any{
		"Id": id, "SeriesId": seriesID, "Type": "Episode",
		"UserData": map[string]any{"LastPlayedDate": playedAt},
	})
}

func upNextItem(t *testing.T, id, seriesID string) json.RawMessage {
	t.Helper()
	return continueRaw(t, map[string]any{
		"Id": id, "SeriesId": seriesID, "Type": "Episode",
		"UserData": map[string]any{},
	})
}

func mergedIDs(t *testing.T, items []json.RawMessage) []string {
	t.Helper()
	ids := make([]string, 0, len(items))
	for _, raw := range items {
		var item struct {
			ID string `json:"Id"`
		}
		if err := json.Unmarshal(raw, &item); err != nil {
			t.Fatalf("unmarshal: %v", err)
		}
		ids = append(ids, item.ID)
	}
	return ids
}

func equalIDs(got, want []string) bool {
	if len(got) != len(want) {
		return false
	}
	for i := range got {
		if got[i] != want[i] {
			return false
		}
	}
	return true
}

// The case the merged row exists for: an episode was just finished, so it has left the
// resume list, and the next one must be the first card rather than sitting in a second
// row further down the launcher.
func TestMergeContinueWatchingLeadsWithTheJustFinishedShow(t *testing.T) {
	resume := []json.RawMessage{
		resumeItem(t, "film", "", "2026-08-01T20:00:00.0000000Z"),
	}
	nextUp := []json.RawMessage{upNextItem(t, "s2e5", "severance")}
	played := map[string]time.Time{
		"severance": time.Date(2026, time.August, 6, 21, 0, 0, 0, time.UTC),
	}

	got := mergedIDs(t, mergeContinueWatching(resume, nextUp, played))
	if want := []string{"s2e5", "film"}; !equalIDs(got, want) {
		t.Fatalf("merged = %v, want %v", got, want)
	}
}

// A show being watched right now must not appear twice — once part-way through an
// episode and once as the episode after it.
func TestMergeContinueWatchingPrefersTheResumeEpisodeOfASeries(t *testing.T) {
	resume := []json.RawMessage{resumeItem(t, "s1e3", "show", "2026-08-06T19:00:00Z")}
	nextUp := []json.RawMessage{
		upNextItem(t, "s1e4", "show"),
		upNextItem(t, "other-e1", "other"),
	}
	played := map[string]time.Time{
		"show":  time.Date(2026, time.August, 6, 19, 0, 0, 0, time.UTC),
		"other": time.Date(2026, time.August, 2, 19, 0, 0, 0, time.UTC),
	}

	got := mergedIDs(t, mergeContinueWatching(resume, nextUp, played))
	if want := []string{"s1e3", "other-e1"}; !equalIDs(got, want) {
		t.Fatalf("merged = %v, want %v", got, want)
	}
}

// Each source keeps its own order. Emby's ordering within a list is the useful part of
// both, so this must be a merge of two sorted lists and never a sort of their union.
func TestMergeContinueWatchingKeepsEachSourceOrder(t *testing.T) {
	resume := []json.RawMessage{
		resumeItem(t, "r1", "", "2026-08-06T10:00:00Z"),
		resumeItem(t, "r2", "", "2026-08-04T10:00:00Z"),
		resumeItem(t, "r3", "", "2026-08-01T10:00:00Z"),
	}
	nextUp := []json.RawMessage{
		upNextItem(t, "n1", "alpha"),
		upNextItem(t, "n2", "beta"),
	}
	played := map[string]time.Time{
		"alpha": time.Date(2026, time.August, 5, 10, 0, 0, 0, time.UTC),
		"beta":  time.Date(2026, time.August, 3, 10, 0, 0, 0, time.UTC),
	}

	got := mergedIDs(t, mergeContinueWatching(resume, nextUp, played))
	if want := []string{"r1", "n1", "r2", "n2", "r3"}; !equalIDs(got, want) {
		t.Fatalf("merged = %v, want %v", got, want)
	}
}

// With no play dates to go on — the lookup failed, or Emby wrote something unparseable —
// the row is the resume list followed by Next Up, which is the order the launcher had
// when they were two rows. Nothing is dropped.
func TestMergeContinueWatchingFallsBackToResumeFirst(t *testing.T) {
	resume := []json.RawMessage{
		resumeItem(t, "r1", "", "not a date"),
		resumeItem(t, "r2", "", ""),
	}
	nextUp := []json.RawMessage{
		upNextItem(t, "n1", "alpha"),
		upNextItem(t, "n2", "beta"),
	}

	got := mergedIDs(t, mergeContinueWatching(resume, nextUp, nil))
	if want := []string{"r1", "r2", "n1", "n2"}; !equalIDs(got, want) {
		t.Fatalf("merged = %v, want %v", got, want)
	}
}

func TestParsePlayedAtAcceptsEmbyTimestamps(t *testing.T) {
	for _, value := range []string{
		"2026-08-06T21:04:05.1234567Z",
		"2026-08-06T21:04:05Z",
		"2026-08-06T21:04:05+12:00",
		"2026-08-06T21:04:05",
	} {
		if _, ok := parsePlayedAt(value); !ok {
			t.Errorf("parsePlayedAt(%q) failed", value)
		}
	}
	for _, value := range []string{"", "   ", "0001-01-01", "yesterday"} {
		if _, ok := parsePlayedAt(value); ok {
			t.Errorf("parsePlayedAt(%q) unexpectedly succeeded", value)
		}
	}
}
