package opensubtitles

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestSearchParamsPrefersItsOwnIDOverTheParents(t *testing.T) {
	params := searchParams(Query{
		IMDBID: "tt0903747", ParentIMDBID: "tt0999999", Season: 1, Episode: 2,
		Languages: []string{"it", "en", "en"}, Type: "episode",
	})
	if got := params.Get("imdb_id"); got != "0903747" {
		t.Fatalf("imdb_id = %q, want the tt stripped", got)
	}
	if params.Has("parent_imdb_id") {
		t.Fatal("parent_imdb_id was sent alongside the episode's own id")
	}
	if got := params.Get("season_number"); got != "1" {
		t.Fatalf("season_number = %q", got)
	}
	// Sorted and deduplicated: the API rejects the list in any other shape.
	if got := params.Get("languages"); got != "en,it" {
		t.Fatalf("languages = %q, want %q", got, "en,it")
	}
}

func TestSearchParamsFallsBackToTheSeriesForAnEpisode(t *testing.T) {
	params := searchParams(Query{ParentIMDBID: "tt0903747", Season: 5, Episode: 14})
	if got := params.Get("parent_imdb_id"); got != "0903747" {
		t.Fatalf("parent_imdb_id = %q", got)
	}
	if got := params.Get("episode_number"); got != "14" {
		t.Fatalf("episode_number = %q", got)
	}
}

// A film must never be sent a season number: an id plus a season is how a search comes
// back empty for a title that plainly exists.
func TestSearchParamsSendsNoSeasonForAFilm(t *testing.T) {
	params := searchParams(Query{TMDBID: "550", Type: "movie"})
	if params.Has("season_number") || params.Has("episode_number") {
		t.Fatal("a film was searched for with episode numbers")
	}
	if got := params.Get("tmdb_id"); got != "550" {
		t.Fatalf("tmdb_id = %q", got)
	}
}

func TestSearchParamsRefusesAQueryItCannotIdentify(t *testing.T) {
	if params := searchParams(Query{Languages: []string{"en"}}); len(params) != 0 {
		t.Fatalf("searchParams answered %v for a query with no identity", params)
	}
}

func TestSearchDropsRowsWithNoFileToFetch(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Api-Key") != "key" {
			t.Errorf("api key header = %q", r.Header.Get("Api-Key"))
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{
			map[string]any{"attributes": map[string]any{
				"language": "en", "ratings": 8.5, "download_count": 120,
				"foreign_parts_only": true, "release": "BluRay",
				"files": []any{map[string]any{"file_id": 42, "file_name": "x.srt"}},
			}},
			map[string]any{"attributes": map[string]any{"language": "it", "files": []any{}}},
		}})
	}))
	defer server.Close()

	client := New("key", "Memby/test", "", "", 5*time.Second)
	client.SetBaseURL(server.URL)
	found, err := client.Search(context.Background(), Query{IMDBID: "tt1", Languages: []string{"en"}})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(found) != 1 {
		t.Fatalf("got %d candidates, want the one with a file", len(found))
	}
	if found[0].FileID != 42 || !found[0].Forced || found[0].Downloads != 120 {
		t.Fatalf("candidate decoded as %+v", found[0])
	}
}

func TestDownloadFollowsTheLinkWithoutTheCredentials(t *testing.T) {
	var files *httptest.Server
	api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/login":
			_ = json.NewEncoder(w).Encode(map[string]string{"token": "jwt"})
		case "/download":
			if r.Header.Get("Authorization") != "Bearer jwt" {
				t.Errorf("download authorization = %q", r.Header.Get("Authorization"))
			}
			_ = json.NewEncoder(w).Encode(map[string]any{
				"link": files.URL + "/f.srt", "file_name": "f.srt", "remaining": 19,
			})
		default:
			t.Errorf("unexpected path %s", r.URL.Path)
		}
	}))
	defer api.Close()
	files = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// The CDN is not the API: neither the key nor the token belongs on this request.
		if r.Header.Get("Api-Key") != "" || r.Header.Get("Authorization") != "" {
			t.Error("credentials were sent to the download host")
		}
		_, _ = w.Write([]byte("1\n00:00:01,000 --> 00:00:02,000\nhello\n"))
	}))
	defer files.Close()

	client := New("key", "Memby/test", "someone", "secret", 5*time.Second)
	client.SetBaseURL(api.URL)
	name, content, err := client.Download(context.Background(), 42)
	if err != nil {
		t.Fatalf("Download: %v", err)
	}
	if name != "f.srt" || len(content) == 0 {
		t.Fatalf("Download returned %q / %d bytes", name, len(content))
	}
}

// The allowance running out is the one failure a viewer can act on, so it must not be
// flattened into "the provider did not answer".
func TestDownloadReportsAnExhaustedQuota(t *testing.T) {
	api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotAcceptable)
		_, _ = w.Write([]byte(`{"message":"quota exceeded"}`))
	}))
	defer api.Close()

	client := New("key", "Memby/test", "", "", 5*time.Second)
	client.SetBaseURL(api.URL)
	_, _, err := client.Download(context.Background(), 7)
	if _, ok := err.(*QuotaError); !ok {
		t.Fatalf("Download error = %v, want a QuotaError", err)
	}
}

func TestLoginTokenIsFetchedOnce(t *testing.T) {
	logins := 0
	api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/login" {
			logins++
			_ = json.NewEncoder(w).Encode(map[string]string{"token": "jwt"})
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{}})
	}))
	defer api.Close()

	client := New("key", "Memby/test", "someone", "secret", 5*time.Second)
	client.SetBaseURL(api.URL)
	for range 3 {
		if _, err := client.Search(context.Background(), Query{IMDBID: "tt1"}); err != nil {
			t.Fatalf("Search: %v", err)
		}
	}
	if logins != 1 {
		t.Fatalf("logged in %d times, want once", logins)
	}
}

func TestFormatFromNameFallsBackToSubRip(t *testing.T) {
	for name, want := range map[string]string{
		"a.srt": "srt", "b.VTT": "vtt", "c.ass": "ass", "d": "srt", "e.zip": "srt",
	} {
		if got := formatFromName(name); got != want {
			t.Errorf("formatFromName(%q) = %q, want %q", name, got, want)
		}
	}
}
