package metadata

import (
	"bytes"
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
)

// fakeTVDB — стенд v4: /login выдаёт токен, остальное требует Bearer.
func fakeTVDB(t *testing.T, logins *atomic.Int32) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()
	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		var body map[string]string
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["apikey"] != "k" {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		if logins != nil {
			logins.Add(1)
		}
		_, _ = w.Write([]byte(`{"status":"success","data":{"token":"tok"}}`))
	})
	authed := func(next http.HandlerFunc) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			if r.Header.Get("Authorization") != "Bearer tok" {
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			next(w, r)
		}
	}
	mux.HandleFunc("/search", authed(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("type") != "series" || r.URL.Query().Get("query") != "Fargo" {
			t.Errorf("query = %v", r.URL.Query())
		}
		_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"269613","name":"Fargo","year":"2014",
			"translations":{"rus":"Фарго","eng":"Fargo"}}]}`))
	}))
	mux.HandleFunc("/series/269613/extended", authed(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"data":{"episodes":[
			{"seasonNumber":1},{"seasonNumber":1},{"seasonNumber":2}
		]}}`))
	}))
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv
}

func newTVDB(t *testing.T, url string) *TVDB {
	t.Helper()
	return newTVDBLang(t, url, "")
}

func newTVDBLang(t *testing.T, url, lang string) *TVDB {
	t.Helper()
	c, err := NewTVDB(TVDBConfig{APIKey: "k", BaseURL: url, Language: lang}, nil)
	if err != nil {
		t.Fatalf("NewTVDB: %v", err)
	}
	return c
}

// searchStand — стенд с одной записью поиска: тело ответа задаётся тестом,
// строка запроса и число обращений к /search записываются для проверок.
type searchStand struct {
	srv      *httptest.Server
	mu       sync.Mutex
	queries  []string
	searches atomic.Int32
}

func (s *searchStand) recorded() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return append([]string(nil), s.queries...)
}

func newSearchStand(t *testing.T, body string) *searchStand {
	t.Helper()
	s := &searchStand{}
	mux := http.NewServeMux()
	mux.HandleFunc("/login", func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"data":{"token":"tok"}}`))
	})
	mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
		s.searches.Add(1)
		s.mu.Lock()
		s.queries = append(s.queries, r.URL.RawQuery)
		s.mu.Unlock()
		_, _ = w.Write([]byte(body))
	})
	s.srv = httptest.NewServer(mux)
	t.Cleanup(s.srv.Close)
	return s
}

// Локализованное название кандидата: перевод, фолбэк во всех его формах и
// OriginalTitle, который равен primary name всегда.
func TestTVDB_SearchTranslations(t *testing.T) {
	const primary = "哪吒之魔童降世"
	cases := []struct {
		name      string
		lang      string
		record    string
		wantTitle string
	}{
		{"перевод есть", "ru", `"translations":{"rus":"Нэчжа","eng":"Ne Zha"}`, "Нэчжа"},
		{"перевода на язык нет", "ru", `"translations":{"eng":"Ne Zha"}`, primary},
		{"перевод из пробелов", "ru", `"translations":{"rus":"   "}`, primary},
		{"ключ в другом регистре", "ru", `"translations":{"RUS":"Нэчжа"}`, "Нэчжа"},
		{"блока переводов нет", "ru", `"year":"2019"`, primary},
		{"блок пустой", "ru", `"translations":{}`, primary},
		{"блок не карта", "ru", `"translations":["Нэчжа"]`, primary},
		{"блок null", "ru", `"translations":null`, primary},
		{"язык по умолчанию — eng", "", `"translations":{"rus":"Нэчжа","eng":"Ne Zha"}`, "Ne Zha"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			body := `{"data":[{"tvdb_id":"131155","name":"` + primary + `","year":"2019",` + tc.record + `}]}`
			stand := newSearchStand(t, body)
			got, err := newTVDBLang(t, stand.srv.URL, tc.lang).
				Search(context.Background(), Query{Type: Movie, Title: "Ne Zha"})
			if err != nil {
				t.Fatalf("Search: %v", err)
			}
			if len(got) != 1 {
				t.Fatalf("кандидатов = %d, want 1 (негодный перевод не должен ронять выдачу)", len(got))
			}
			if got[0].Title != tc.wantTitle {
				t.Errorf("Title = %q, want %q", got[0].Title, tc.wantTitle)
			}
			if got[0].OriginalTitle != primary {
				t.Errorf("OriginalTitle = %q, want primary name %q", got[0].OriginalTitle, primary)
			}
			if n := stand.searches.Load(); n != 1 {
				t.Errorf("обращений к /search = %d, want 1 (лимит ключа не растёт)", n)
			}
		})
	}
}

// Сигнал «форма ответа не та, что записана в разведке». Он единственный, кто
// отличает неверное предположение о чужом API от штатного «перевода нет», —
// поэтому проверяется поимённо, а не через покрытие.
func TestTVDB_SignalOnUnexpectedTranslationForm(t *testing.T) {
	cases := []struct {
		name       string
		record     string
		wantSignal bool
	}{
		{"двухбуквенные коды", `"translations":{"ru":"Дюна","en":"Dune"}`, true},
		{"блок null", `"translations":null`, true},
		{"блок пустой", `"translations":{}`, true},
		{"блока нет", `"year":"2021"`, true},
		{"блок не карта", `"translations":["Дюна"]`, true},
		{"вложенные объекты", `"translations":{"rus":{"name":"Дюна"}}`, true},
		// Штатный случай: коды трёхбуквенные, нужного среди них нет — не сигнал.
		{"перевода на язык нет", `"translations":{"eng":"Dune"}`, false},
		{"перевод есть", `"translations":{"rus":"Дюна","eng":"Dune"}`, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			stand := newSearchStand(t,
				`{"data":[{"tvdb_id":"1","name":"Dune","year":"2021",`+tc.record+`}]}`)
			var buf bytes.Buffer
			log := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))
			c, err := NewTVDB(TVDBConfig{APIKey: "k", BaseURL: stand.srv.URL, Language: "ru"}, log)
			if err != nil {
				t.Fatalf("NewTVDB: %v", err)
			}
			got, err := c.Search(context.Background(), Query{Type: Movie, Title: "Dune"})
			if err != nil {
				t.Fatalf("Search: %v", err)
			}
			if len(got) != 1 {
				t.Fatalf("кандидатов = %d, want 1", len(got))
			}
			signal := strings.Contains(buf.String(), "no language-coded translations")
			if signal != tc.wantSignal {
				t.Errorf("сигнал = %v, want %v; лог: %q", signal, tc.wantSignal, buf.String())
			}
		})
	}
}

// Выбор среди EqualFold-совпавших ключей детерминирован: иначе один и тот же
// ответ давал бы разное имя папки от прогона к прогону.
func TestTVDB_TranslationKeyPickIsDeterministic(t *testing.T) {
	cases := []struct{ name, block, want string }{
		{"точное совпадение сильнее регистра", `{"rus":"Дюна","RUS":"HACK"}`, "Дюна"},
		{"без точного — лексикографически меньший", `{"RUS":"A","Rus":"B"}`, "A"},
		{"юникод-эквивалент case-folding", `{"rus":"Дюна","ruſ":"HACK"}`, "Дюна"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			for i := 0; i < 200; i++ {
				got, _ := translatedName(json.RawMessage(tc.block), "rus")
				if got != tc.want {
					t.Fatalf("прогон %d: got %q, want %q", i, got, tc.want)
				}
			}
		})
	}
}

// Локаль работает только на разборе ответа: параметр языка в запрос не уходит,
// и строка запроса не зависит от настройки.
func TestTVDB_SearchQueryHasNoLanguage(t *testing.T) {
	const body = `{"data":[{"tvdb_id":"1","name":"X","year":"2000"}]}`
	var got []string
	for _, lang := range []string{"ru", "en", ""} {
		stand := newSearchStand(t, body)
		if _, err := newTVDBLang(t, stand.srv.URL, lang).
			Search(context.Background(), Query{Type: Movie, Title: "X", Year: 2000}); err != nil {
			t.Fatalf("Search(%q): %v", lang, err)
		}
		recorded := stand.recorded()
		if len(recorded) != 1 {
			t.Fatalf("запросов = %d", len(recorded))
		}
		q, err := url.ParseQuery(recorded[0])
		if err != nil {
			t.Fatalf("ParseQuery: %v", err)
		}
		if _, ok := q["language"]; ok {
			t.Errorf("language=%q в запросе при lang=%q: параметр сужает выдачу, слать его нельзя",
				q.Get("language"), lang)
		}
		got = append(got, recorded[0])
	}
	if got[0] != got[1] || got[1] != got[2] {
		t.Errorf("строка запроса зависит от языка: %q", got)
	}
}

func TestTVDB_SearchAndLoginCached(t *testing.T) {
	var logins atomic.Int32
	srv := fakeTVDB(t, &logins)
	c := newTVDB(t, srv.URL)

	got, err := c.Search(context.Background(), Query{Type: Series, Title: "Fargo", Year: 2014})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(got) != 1 || got[0].ID != "269613" || got[0].Provider != "tvdb" || got[0].Year != 2014 {
		t.Fatalf("candidate = %+v", got)
	}
	if got[0].URL != "https://www.thetvdb.com/dereferrer/series/269613" {
		t.Fatalf("URL = %q", got[0].URL)
	}
	// Второй запрос переиспользует токен — повторного логина нет.
	if _, err := c.Search(context.Background(), Query{Type: Series, Title: "Fargo"}); err != nil {
		t.Fatal(err)
	}
	if logins.Load() != 1 {
		t.Errorf("logins = %d, want 1 (token cached)", logins.Load())
	}
}

// Для фильма ссылка-dereferrer должна вести на /movie/, а не /series/.
func TestTVDB_MovieDereferrerURL(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/login", func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"data":{"token":"tok"}}`))
	})
	mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("type") != "movie" {
			t.Errorf("type = %q, want movie", r.URL.Query().Get("type"))
		}
		_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"3015","name":"The Last Unicorn","year":"1982"}]}`))
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()

	got, err := newTVDB(t, srv.URL).Search(context.Background(), Query{Type: Movie, Title: "The Last Unicorn"})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(got) != 1 || got[0].URL != "https://www.thetvdb.com/dereferrer/movie/3015" {
		t.Fatalf("candidate = %+v", got)
	}
}

func TestTVDB_SeasonEpisodeCounts(t *testing.T) {
	srv := fakeTVDB(t, nil)
	counts, err := newTVDB(t, srv.URL).SeasonEpisodeCounts(context.Background(), "269613")
	if err != nil {
		t.Fatalf("SeasonEpisodeCounts: %v", err)
	}
	if counts[1] != 2 || counts[2] != 1 {
		t.Errorf("counts = %v", counts)
	}
}

func TestTVDB_ReloginOn401(t *testing.T) {
	var logins atomic.Int32
	var token atomic.Value
	token.Store("tok")
	mux := http.NewServeMux()
	mux.HandleFunc("/login", func(w http.ResponseWriter, _ *http.Request) {
		logins.Add(1)
		_, _ = w.Write([]byte(`{"data":{"token":"tok"}}`))
	})
	var firstCall atomic.Bool
	mux.HandleFunc("/search", func(w http.ResponseWriter, _ *http.Request) {
		// Первый авторизованный запрос отдаёт 401 (токен «протух»).
		if firstCall.CompareAndSwap(false, true) {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"1","name":"X","year":"2000"}]}`))
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()

	c := newTVDB(t, srv.URL)
	got, err := c.Search(context.Background(), Query{Type: Series, Title: "X"})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("got %d", len(got))
	}
	if logins.Load() != 2 {
		t.Errorf("logins = %d, want 2 (initial + relogin)", logins.Load())
	}
}

func TestNewTVDB_RequiresKey(t *testing.T) {
	if _, err := NewTVDB(TVDBConfig{}, nil); err == nil {
		t.Fatal("want error without api_key")
	}
}
