package naming

import (
	"context"
	"errors"
	"log/slog"
	"testing"

	"git.vakhrushev.me/av/jellybit/internal/llm"
)

func testLogger() *slog.Logger {
	return slog.New(slog.DiscardHandler)
}

// fakeProvider отдаёт заранее заданные ответы по очереди; считает вызовы.
type fakeProvider struct {
	responses []string
	err       error
	calls     int
}

func (f *fakeProvider) Complete(_ context.Context, _ llm.Request) (llm.Response, error) {
	f.calls++
	if f.err != nil {
		return llm.Response{}, f.err
	}
	idx := f.calls - 1
	if idx >= len(f.responses) {
		idx = len(f.responses) - 1
	}
	return llm.Response{Content: f.responses[idx]}, nil
}

const duneContext = "Дюна: Часть вторая / Dune: Part Two [2024, фантастика, WEB-DL 2160p]\nDub + MVO"

func TestDeriveNameViaLLM(t *testing.T) {
	fp := &fakeProvider{responses: []string{
		`{"type":"movie","title":"Дюна: Часть вторая","year":2024,"director":"Дени Вильнёв","is_russian":false}`,
	}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), duneContext, "")
	want := "Дюна: Часть вторая (Дени Вильнёв, 2024)"
	if got != want {
		t.Errorf("DeriveName() = %q, want %q", got, want)
	}
	if fp.calls != 1 {
		t.Errorf("вызовов LLM = %d, want 1", fp.calls)
	}
}

func TestDeriveNameEmptyInputSkipsLLM(t *testing.T) {
	// Голый magnet без dn/контекста: выводить имя не из чего — LLM не зовём.
	fp := &fakeProvider{responses: []string{
		`{"type":"movie","title":"Unknown"}`, // как раз мусор, который был бы записан
	}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), "   ", "  ")
	if got != "" {
		t.Errorf("DeriveName() = %q, want empty", got)
	}
	if fp.calls != 0 {
		t.Errorf("вызовов LLM = %d, want 0 (пустой вход не зовёт модель)", fp.calls)
	}
}

func TestDeriveNameHintPresentSkipsGuard(t *testing.T) {
	// Контекст пуст, но есть подсказка (dn) — гард пустого входа НЕ срабатывает:
	// имя выводится фолбеком из подсказки. LLM здесь не зовётся (он работает
	// только по контексту, а его нет) — это прежнее поведение, гард его не меняет.
	fp := &fakeProvider{responses: []string{`{"type":"movie","title":"x"}`}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), "", "Dune.Part.Two.2024")
	if got == "" {
		t.Error("при наличии подсказки имя не должно быть пустым (гард не должен глотать hint)")
	}
	if fp.calls != 0 {
		t.Errorf("вызовов LLM = %d, want 0 (LLM выводит по контексту, которого нет)", fp.calls)
	}
}

func TestLabel(t *testing.T) {
	cases := []struct {
		name     string
		title    string
		director string
		year     int
		season   string
		want     string
	}{
		{"title only", "Брат", "", 0, "", "Брат"},
		{"title+year", "Fargo", "", 2015, "", "Fargo (2015)"},
		{"title+director+year", "Дюна", "Дени Вильнёв", 2024, "", "Дюна (Дени Вильнёв, 2024)"},
		{"director only", "Брат", "Алексей Балабанов", 0, "", "Брат (Алексей Балабанов)"},
		{"series one season", "Сёгун", "", 2024, "Сезон 2", "Сёгун (2024). Сезон 2"},
		{"series multi season", "Фарго", "", 0, "Сезоны 1–3", "Фарго. Сезоны 1–3"},
		{"series specials", "Рик и Морти", "", 0, "Спецвыпуски", "Рик и Морти. Спецвыпуски"},
		{"empty title", "   ", "Кто-то", 2024, "Сезон 1", ""},
		{"sanitized", "  Fargo\n", "", 2015, "", "Fargo (2015)"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := Label(c.title, c.director, c.year, c.season); got != c.want {
				t.Errorf("Label(%q, %q, %d, %q) = %q, want %q",
					c.title, c.director, c.year, c.season, got, c.want)
			}
		})
	}
}

func TestDeriveNameRussianTitle(t *testing.T) {
	fp := &fakeProvider{responses: []string{
		`{"type":"movie","title":"Брат","original_title":"Brat","year":1997,"is_russian":true}`,
	}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), "Брат / Brat [1997]", "")
	if got != "Брат (1997)" {
		t.Errorf("DeriveName() = %q, want %q", got, "Брат (1997)")
	}
}

func TestDeriveNameRetriesThenSucceeds(t *testing.T) {
	fp := &fakeProvider{responses: []string{
		"не json вовсе",
		`{"type":"movie","title":"Дюна: Часть вторая","year":2024}`,
	}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), duneContext, "")
	if got != "Дюна: Часть вторая (2024)" {
		t.Errorf("DeriveName() = %q", got)
	}
	if fp.calls != 2 {
		t.Errorf("вызовов LLM = %d, want 2", fp.calls)
	}
}

func TestDeriveNameExhaustsThenFallback(t *testing.T) {
	fp := &fakeProvider{responses: []string{"мусор"}}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), duneContext, "")
	// Бюджет исчерпан → алгоритмический фолбек: первая строка без тех. спецификаций.
	if got != "Дюна: Часть вторая / Dune: Part Two" {
		t.Errorf("DeriveName() = %q (ожидался фолбек)", got)
	}
	if fp.calls != 3 {
		t.Errorf("вызовов LLM = %d, want 3 (исчерпание бюджета)", fp.calls)
	}
}

func TestDeriveNameProviderErrorFallsBack(t *testing.T) {
	fp := &fakeProvider{err: errors.New("connection refused")}
	got := New(fp, 3, testLogger()).DeriveName(context.Background(), duneContext, "")
	if got != "Дюна: Часть вторая / Dune: Part Two" {
		t.Errorf("DeriveName() = %q (ожидался фолбек при ошибке)", got)
	}
	if fp.calls != 1 {
		t.Errorf("вызовов LLM = %d, want 1 (на транспортной ошибке не повторяем)", fp.calls)
	}
}

func TestDeriveNameNilProviderUsesFallback(t *testing.T) {
	got := New(nil, 3, testLogger()).DeriveName(context.Background(), duneContext, "")
	if got != "Дюна: Часть вторая / Dune: Part Two" {
		t.Errorf("DeriveName() = %q (ожидался фолбек без LLM)", got)
	}
}

// Значения ярлыка приходят снаружи — из credits метабазы и из сохранённого
// контекста. Категория Cf (zero-width, BOM, RLO) обязана сниматься здесь:
// требование metadata-match не чистит режиссёра в плане именно потому, что его
// чистит рендер. RLO в карточке Telegram переворачивает отображение имени.
func TestSanitize_StripsFormatChars(t *testing.T) {
	cases := []struct{ in, want string }{
		{"Denis\u202eVilleneuve", "DenisVilleneuve"},
		{"Denis\u200bVilleneuve", "DenisVilleneuve"},
		{"Denis\ufeffVilleneuve", "DenisVilleneuve"},
		{"Denis\nVilleneuve", "Denis Villeneuve"},
		{"Дени Вильнёв", "Дени Вильнёв"},
	}
	for _, c := range cases {
		if got := sanitize(c.in); got != c.want {
			t.Errorf("sanitize(%q) = %q, want %q", c.in, got, c.want)
		}
	}
	if got := Label("Dune", "Denis\u202eVilleneuve", 2021, ""); got != "Dune (DenisVilleneuve, 2021)" {
		t.Errorf("Label = %q", got)
	}
}
