package api

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

// The intro rule, pinned against the same cases as the television's copy
// (`IntroTest` in app/src/test). The two exist separately because with no gateway there is
// nobody to ask, and a skip must not land somewhere different depending on whether the
// container is up — so when one of these changes, the other has to change with it.
//
// The cases are taken from what Emby actually writes: markers arrive as ordinary chapters
// carrying a MarkerType, interleaved with the real ones in playback order.

func chapter(seconds int64, marker string) embyChapter {
	return embyChapter{
		StartPositionTicks: seconds * 1_000 * ticksPerMillisecond,
		MarkerType:         marker,
		Name:               marker,
	}
}

func TestIntroFromChapters(t *testing.T) {
	cases := []struct {
		name     string
		chapters []embyChapter
		want     introSegment
		ok       bool
	}{
		{
			// FROM S01E01 as the server actually holds it: two ordinary chapters, then the
			// pair, then the rest of the chapters.
			name: "a real episode",
			chapters: []embyChapter{
				chapter(0, "Chapter"),
				chapter(300, "Chapter"),
				chapter(463, "IntroStart"),
				chapter(583, "IntroEnd"),
				chapter(600, "Chapter"),
			},
			want: introSegment{StartMs: 463_000, EndMs: 583_000},
			ok:   true,
		},
		{
			name: "an intro that starts at the very beginning",
			chapters: []embyChapter{
				chapter(0, "IntroStart"),
				chapter(95, "IntroEnd"),
			},
			want: introSegment{StartMs: 0, EndMs: 95_000},
			ok:   true,
		},
		{
			name:     "no markers at all",
			chapters: []embyChapter{chapter(0, "Chapter"), chapter(300, "Chapter")},
		},
		{
			name:     "no chapters at all",
			chapters: nil,
		},
		{
			// Half a pair is not a segment. There is no honest end to skip to, and
			// guessing one is how a viewer lands in the middle of a scene.
			name:     "a start with no end",
			chapters: []embyChapter{chapter(112, "IntroStart"), chapter(300, "Chapter")},
		},
		{
			name:     "an end with no start",
			chapters: []embyChapter{chapter(0, "Chapter"), chapter(246, "IntroEnd")},
		},
		{
			name:     "an end before its start",
			chapters: []embyChapter{chapter(300, "IntroStart"), chapter(120, "IntroEnd")},
		},
		{
			// Emby occasionally writes a pair seconds apart on a title whose opening it
			// half-recognised. A button that moves the picture imperceptibly reads as
			// broken, so there is deliberately no button at all.
			name:     "a segment too short to be an intro",
			chapters: []embyChapter{chapter(100, "IntroStart"), chapter(103, "IntroEnd")},
		},
		{
			// Far more likely two unrelated markers read as a range than a ten-minute
			// title sequence, and honouring it would throw the viewer past the story.
			name:     "a segment too long to be an intro",
			chapters: []embyChapter{chapter(60, "IntroStart"), chapter(660, "IntroEnd")},
		},
		{
			// The first start wins. Two starts mean the markers are already untrustworthy,
			// and taking the later one would pick the larger, more damaging skip.
			name: "two starts before one end",
			chapters: []embyChapter{
				chapter(100, "IntroStart"),
				chapter(160, "IntroStart"),
				chapter(220, "IntroEnd"),
			},
			want: introSegment{StartMs: 100_000, EndMs: 220_000},
			ok:   true,
		},
		{
			name: "a later pair is ignored once one has been found",
			chapters: []embyChapter{
				chapter(100, "IntroStart"),
				chapter(220, "IntroEnd"),
				chapter(1800, "IntroStart"),
				chapter(1900, "IntroEnd"),
			},
			want: introSegment{StartMs: 100_000, EndMs: 220_000},
			ok:   true,
		},
		{
			// A credits marker is a different feature that does not exist here yet, and
			// must never be mistaken for an intro.
			name: "credit markers are not intros",
			chapters: []embyChapter{
				chapter(2800, "CreditsStart"),
				chapter(2900, "CreditsEnd"),
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, ok := introFromChapters(tc.chapters)
			if ok != tc.ok {
				t.Fatalf("available = %v, want %v (segment %+v)", ok, tc.ok, got)
			}
			if ok && got != tc.want {
				t.Fatalf("segment = %+v, want %+v", got, tc.want)
			}
		})
	}
}

// A gateway with no Emby behind it — and one whose operator has turned the feature off —
// answers "no intro" rather than an error. The button is an optional convenience on a film
// that is already playing, and a 502 here would be a red line in the log for every episode
// anybody watched.
func TestIntroWithoutEmbyAnswersUnavailable(t *testing.T) {
	server := testServer(config.Config{})

	request := httptest.NewRequest(http.MethodGet, "/v1/items/1304864/intro", nil)
	request.SetPathValue("id", "1304864")
	rec := httptest.NewRecorder()
	server.handleIntro(rec, request, store.Session{})

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200 — trouble is answered with silence here", rec.Code)
	}
	var body introResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("body: %v", err)
	}
	if body.Available {
		t.Fatal("a gateway with nothing to ask must never claim an intro")
	}
}

// The mode a television is told to use has to be one this build knows, or a set could be
// pushed a value it silently does nothing with.
func TestSkipIntroPreferenceIsCatalogued(t *testing.T) {
	definition, ok := preferenceDefinitionFor("skipIntroMode")
	if !ok {
		t.Fatal("skipIntroMode is missing from the preference catalogue")
	}
	if definition.Default != skipIntroPrompt {
		t.Fatalf("default = %v, want %q — a button is the asked-for behaviour, not a silent seek",
			definition.Default, skipIntroPrompt)
	}
	wanted := []string{skipIntroPrompt, skipIntroAuto, skipIntroOff}
	if len(definition.Options) != len(wanted) {
		t.Fatalf("options = %+v, want the three modes", definition.Options)
	}
	for i, value := range wanted {
		if definition.Options[i].Value != value {
			t.Fatalf("option %d = %q, want %q", i, definition.Options[i].Value, value)
		}
	}

	// An illegal value must come back as the default rather than reaching a player.
	normalised := normalizePreferences(map[string]any{"skipIntroMode": "immediately"})
	if normalised["skipIntroMode"] != skipIntroPrompt {
		t.Fatalf("normalised = %v, want %q", normalised["skipIntroMode"], skipIntroPrompt)
	}
}
