package api

import (
	"testing"

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

// Matching an Emby item onto a Bazarr id is where a wrong answer is worst: it downloads a
// subtitle for one film and writes it beside another. These pin the rule.

func TestBazarrMovieMatchesOnTitleAndYear(t *testing.T) {
	movies := []bazarr.Movie{
		{RadarrID: 1, Title: "The Thing", Year: "1982"},
		{RadarrID: 2, Title: "The Thing", Year: "2011"},
		{RadarrID: 3, Title: "Arrival", Year: "2016"},
	}
	if got := bazarrMovieFor("The Thing", 2011, movies); got == nil || got.RadarrID != 2 {
		t.Fatalf("remake matched %+v", got)
	}
	if got := bazarrMovieFor("the thing!", 1982, movies); got == nil || got.RadarrID != 1 {
		t.Fatalf("punctuation and case should not matter, got %+v", got)
	}
	if got := bazarrMovieFor("Arrival", 0, movies); got == nil || got.RadarrID != 3 {
		t.Fatalf("an unknown year should still match a unique title, got %+v", got)
	}
	if got := bazarrMovieFor("Dune", 2021, movies); got != nil {
		t.Fatalf("expected no match, got %+v", got)
	}
	if got := bazarrMovieFor("", 2016, movies); got != nil {
		t.Fatalf("a blank title must never match, got %+v", got)
	}
}

// Bazarr carries the year as a string and leaves it empty often enough that treating an
// empty one as a disagreement would reject correct matches.
func TestBazarrMovieAcceptsAnEntryWithNoYear(t *testing.T) {
	movies := []bazarr.Movie{{RadarrID: 4, Title: "Arrival", Year: ""}}
	if got := bazarrMovieFor("Arrival", 2016, movies); got == nil || got.RadarrID != 4 {
		t.Fatalf("got %+v", got)
	}
}

// With two entries under one title, a year that both sides know must win over the first
// row that merely failed to disagree.
func TestBazarrMoviePrefersAnExactYearOverAYearlessFallback(t *testing.T) {
	movies := []bazarr.Movie{
		{RadarrID: 5, Title: "The Thing", Year: ""},
		{RadarrID: 6, Title: "The Thing", Year: "1982"},
	}
	if got := bazarrMovieFor("The Thing", 1982, movies); got == nil || got.RadarrID != 6 {
		t.Fatalf("got %+v", got)
	}
}

func TestBazarrSeriesMatchesOnTitleAndYear(t *testing.T) {
	series := []bazarr.Series{
		{SonarrSeriesID: 1, Title: "Battlestar Galactica", Year: "1978"},
		{SonarrSeriesID: 2, Title: "Battlestar Galactica", Year: "2004"},
	}
	if got := bazarrSeriesFor("Battlestar Galactica", 2004, series); got == nil || got.SonarrSeriesID != 2 {
		t.Fatalf("got %+v", got)
	}
}

func TestBazarrEpisodeMatchesOnNumbersNotTitles(t *testing.T) {
	episodes := []bazarr.Episode{
		{SonarrEpisodeID: 10, Season: 3, Episode: 3, Title: "Something"},
		{SonarrEpisodeID: 11, Season: 3, Episode: 4, Title: "Quite different in Sonarr"},
		{SonarrEpisodeID: 12, Season: 0, Episode: 4, Title: "A special"},
	}
	if got := bazarrEpisodeFor(episodes, 3, 4); got == nil || got.SonarrEpisodeID != 11 {
		t.Fatalf("got %+v", got)
	}
	// Season 0 is specials, a real season — it must not be confused with "no season".
	if got := bazarrEpisodeFor(episodes, 0, 4); got == nil || got.SonarrEpisodeID != 12 {
		t.Fatalf("specials should match, got %+v", got)
	}
	if got := bazarrEpisodeFor(episodes, 3, 9); got != nil {
		t.Fatalf("expected no match, got %+v", got)
	}
	if got := bazarrEpisodeFor(episodes, 3, 0); got != nil {
		t.Fatalf("a missing episode number must never match, got %+v", got)
	}
}

// The viewer's language comes first because it is the only thing they asked for; within a
// language a plain track beats a forced or hearing-impaired one, for the same reason
// selectSubtitle ranks them that way.
func TestRankSubtitleCandidatesPutsTheChosenLanguageFirst(t *testing.T) {
	found := []bazarr.Subtitle{
		{Language: "eng", Score: 99, Token: "en-plain"},
		{Language: "ita", Score: 40, Token: "it-plain"},
		{Language: "ita", Score: 98, Token: "it-forced", Forced: true},
		{Language: "ita", Score: 95, Token: "it-sdh", HearingImpaired: true},
	}
	results := rankMergedCandidates(bazarrCandidates(found), "it")
	if len(results) != 4 {
		t.Fatalf("results = %+v", results)
	}
	order := []string{results[0].Token, results[1].Token, results[2].Token, results[3].Token}
	want := []string{"it-plain", "it-sdh", "it-forced", "en-plain"}
	for i := range want {
		if order[i] != want[i] {
			t.Fatalf("order = %v, want %v", order, want)
		}
	}
}

func TestRankSubtitleCandidatesFallsBackToScoreWithNoPreference(t *testing.T) {
	found := []bazarr.Subtitle{
		{Language: "eng", Score: 55, Token: "low"},
		{Language: "eng", Score: 92, Token: "high"},
	}
	for _, language := range []string{"", "auto"} {
		results := rankMergedCandidates(bazarrCandidates(found), language)
		if len(results) != 2 || results[0].Token != "high" {
			t.Fatalf("language %q gave %+v", language, results)
		}
	}
}

// A row with no token cannot be downloaded, so offering it would be a button that does
// nothing.
func TestRankSubtitleCandidatesDropsTokenlessRowsAndCaps(t *testing.T) {
	found := []bazarr.Subtitle{{Language: "eng", Token: "  "}}
	for i := 0; i < maxSubtitleResults+5; i++ {
		found = append(found, bazarr.Subtitle{Language: "eng", Score: i, Token: "t"})
	}
	results := rankMergedCandidates(bazarrCandidates(found), "en")
	if len(results) > maxSubtitleResults {
		t.Fatalf("len = %d", len(results))
	}
	for _, result := range results {
		if result.Token == "  " {
			t.Fatal("a tokenless row was offered")
		}
	}
}

func TestMergedCandidateLabelNamesTheLanguageAndKind(t *testing.T) {
	label := func(subtitle bazarr.Subtitle) string {
		return mergedCandidateLabel(bazarrCandidates([]bazarr.Subtitle{subtitle})[0])
	}
	for _, testCase := range []struct {
		subtitle bazarr.Subtitle
		want     string
	}{
		{bazarr.Subtitle{Language: "ita", Score: 97, Token: "t"}, "Italian · 97% match"},
		{bazarr.Subtitle{Language: "eng", Forced: true, Score: 80, Token: "t"}, "English · Forced · 80% match"},
		{bazarr.Subtitle{Language: "eng", HearingImpaired: true, Token: "t"}, "English · Hearing impaired"},
		// A language Memby has no entry for must still name itself rather than go blank.
		{bazarr.Subtitle{Language: "mi", Token: "t"}, "MI"},
		{bazarr.Subtitle{Token: "t"}, "Unknown"},
		// Bazarr has been seen to return scores above 100; a "112% match" reads as a bug.
		{bazarr.Subtitle{Language: "eng", Score: 112, Token: "t"}, "English · 100% match"},
	} {
		if got := label(testCase.subtitle); got != testCase.want {
			t.Errorf("label = %q, want %q", got, testCase.want)
		}
	}
}

// The downloaded track has no marker saying it is new, so it is identified by what was
// asked for. Failing to identify it is allowed — the client then applies its own rule.
func TestNewestSubtitleIDPrefersTheMatchingExternalTrack(t *testing.T) {
	subtitles := []playableSubtitle{
		{ID: "1", Language: "eng", DeliveryMethod: "External"},
		{ID: "2", Language: "ita", DeliveryMethod: "Encode"},
		{ID: "3", Language: "ita", DeliveryMethod: "External", IsForced: true},
		{ID: "4", Language: "ita", DeliveryMethod: "External"},
	}
	if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "it"}); got != "4" {
		t.Errorf("plain Italian = %q, want 4", got)
	}
	if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "it", Forced: true}); got != "3" {
		t.Errorf("forced Italian = %q, want 3", got)
	}
	if got := newestSubtitleID(subtitles, subtitleCandidate{Language: "de"}); got != "" {
		t.Errorf("absent language = %q, want empty", got)
	}
	if got := newestSubtitleID(subtitles, subtitleCandidate{}); got != "" {
		t.Errorf("no language = %q, want empty", got)
	}
}
