package magnet

import (
	"net/url"
	"strings"
	"testing"
)

func TestParse(t *testing.T) {
	tests := []struct {
		name     string
		raw      string
		infohash string
		dn       string
		trackers int
	}{
		{
			name:     "btih hex (из BRIEF)",
			raw:      "magnet:?xt=urn:btih:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6&tr=http%3A%2F%2Fbt.t-ru.org%2Fann%3Fmagnet&dn=rutracker-topic-6514485",
			infohash: "541adcff3b6dd5dba7088ea83317d9d6fac331d6",
			dn:       "rutracker-topic-6514485",
			trackers: 1,
		},
		{
			name:     "btih base32 (20 нулевых байт)",
			raw:      "magnet:?xt=urn:btih:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
			infohash: "0000000000000000000000000000000000000000",
		},
		{
			name:     "btmh v2 sha256",
			raw:      "magnet:?xt=urn:btmh:12200123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
			infohash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
		},
		{
			// RFC 2141: namespace URN регистронезависим — URN:BTIH: валиден.
			name:     "btih с URN-префиксом в верхнем регистре",
			raw:      "magnet:?xt=URN:BTIH:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6&dn=Dune",
			infohash: "541adcff3b6dd5dba7088ea83317d9d6fac331d6",
			dn:       "Dune",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got, err := Parse(tc.raw)
			if err != nil {
				t.Fatalf("Parse: %v", err)
			}
			if got.Infohash != tc.infohash {
				t.Errorf("infohash = %q, want %q", got.Infohash, tc.infohash)
			}
			if got.DisplayName != tc.dn {
				t.Errorf("dn = %q, want %q", got.DisplayName, tc.dn)
			}
			if len(got.Trackers) != tc.trackers {
				t.Errorf("trackers = %d, want %d", len(got.Trackers), tc.trackers)
			}
		})
	}
}

// Гибридный magnet несёт оба хеша: Infohash — v1 (приоритет), Infohashes — оба.
func TestParseHybridKeepsBothHashes(t *testing.T) {
	const (
		v1 = "541adcff3b6dd5dba7088ea83317d9d6fac331d6"
		v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	)
	got, err := Parse("magnet:?xt=urn:btih:" + v1 + "&xt=urn:btmh:1220" + v2)
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if got.Infohash != v1 {
		t.Errorf("primary = %q, want v1", got.Infohash)
	}
	if len(got.Infohashes) != 2 || got.Infohashes[0] != v1 || got.Infohashes[1] != v2 {
		t.Errorf("infohashes = %v, want [v1 v2]", got.Infohashes)
	}
}

func TestParseErrors(t *testing.T) {
	cases := []string{
		"https://example.com/file.torrent", // не magnet
		"magnet:?dn=no-infohash",           // нет xt
		"magnet:?xt=urn:btih:zzzz",         // некорректный hash
		"",                                 // пусто
	}
	for _, raw := range cases {
		if _, err := Parse(raw); err == nil {
			t.Errorf("Parse(%q): ожидалась ошибка", raw)
		}
	}
}

func TestParseExtraFields(t *testing.T) {
	raw := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6" +
		"&xl=2200000000&xs=http%3A%2F%2Fmirror.example%2Ft.torrent&kt=dune+scifi"
	got, err := Parse(raw)
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if got.ExactLength != 2200000000 {
		t.Errorf("xl = %d, want 2200000000", got.ExactLength)
	}
	if len(got.Sources) != 1 || got.Sources[0] != "http://mirror.example/t.torrent" {
		t.Errorf("xs = %v", got.Sources)
	}
	if len(got.Keywords) != 1 || got.Keywords[0] != "dune scifi" {
		t.Errorf("kt = %v", got.Keywords)
	}
}

func TestParseInvalidXLIgnored(t *testing.T) {
	for _, raw := range []string{
		"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=notanumber",
		"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=-5",
		"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6",
	} {
		got, err := Parse(raw)
		if err != nil {
			t.Fatalf("Parse(%q): %v", raw, err)
		}
		if got.ExactLength != 0 {
			t.Errorf("Parse(%q): xl = %d, want 0", raw, got.ExactLength)
		}
	}
}

func TestInfoContext(t *testing.T) {
	tests := []struct {
		name    string
		info    Info
		want    []string // подстроки, которые ДОЛЖНЫ быть
		notWant []string // подстроки, которых быть НЕ должно
	}{
		{
			name:    "содержательный dn",
			info:    Info{DisplayName: "Dune.Part.Two.2024.2160p"},
			want:    []string{"Dune.Part.Two.2024.2160p"},
			notWant: []string{"Размер:", "Трекер:"},
		},
		{
			name:    "заглушка-dn не берётся как имя",
			info:    Info{DisplayName: "rutracker-topic-6514485", Trackers: []string{"http://bt.t-ru.org/ann?magnet"}},
			want:    []string{"Трекер: t-ru.org"},
			notWant: []string{"rutracker-topic"},
		},
		{
			name: "только размер",
			info: Info{ExactLength: 2200000000},
			want: []string{"Размер: ≈ 2.0 GiB"},
		},
		{
			name:    "домен из xs, служебный поддомен убран",
			info:    Info{Sources: []string{"udp://tracker.example.org:80/announce"}},
			want:    []string{"Трекер: example.org"},
			notWant: []string{"tracker.example.org"},
		},
		{
			name: "все поля вместе",
			info: Info{
				DisplayName: "Dune",
				ExactLength: 1500,
				Trackers:    []string{"http://bt.t-ru.org/ann"},
				Keywords:    []string{"scifi dune"}, // как отдаёт url.Query() из kt=scifi+dune
			},
			want: []string{"Dune", "Размер:", "Трекер: t-ru.org", "Ключевые слова: scifi, dune"},
		},
		{
			name: "пустой Info → пустой контекст",
			info: Info{},
			want: nil,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := tc.info.Context()
			if len(tc.want) == 0 && got != "" {
				t.Errorf("Context() = %q, want пусто", got)
			}
			for _, w := range tc.want {
				if !strings.Contains(got, w) {
					t.Errorf("Context() = %q, want содержит %q", got, w)
				}
			}
			for _, nw := range tc.notWant {
				if strings.Contains(got, nw) {
					t.Errorf("Context() = %q, НЕ должно содержать %q", got, nw)
				}
			}
		})
	}
}

// Реальная ссылка с рутрекера: dn несёт полный релиз-заголовок в
// процент-кодировке (кириллица + латиница, сезон/серии/год/жанры/качество),
// tr — announce-хост bt4.t-ru.org. Проверяем полный путь до Context().
func TestParseRealRutrackerLink(t *testing.T) {
	const raw = "magnet:?xt=urn:btih:BACA24E18C7382A9E9A44132C8D7DB86C4D319C2" +
		"&tr=http%3A%2F%2Fbt4.t-ru.org%2Fann%3Fmagnet" +
		"&dn=%D0%91%D1%83%D1%85%D1%82%D0%B0%20%D0%B2%D0%B4%D0%BE%D0%B2%20%2F%20Widow's%20Bay%20%2F%20%D0%A1%D0%B5%D0%B7%D0%BE%D0%BD%3A%201%20%2F%20%D0%A1%D0%B5%D1%80%D0%B8%D0%B8%3A%201-10%20%D0%B8%D0%B7%2010%20(%D0%A5%D0%B8%D1%80%D0%BE%20%D0%9C%D1%83%D1%80%D0%B0%D0%B9)%20%5B2026%2C%20%D0%A1%D0%A8%D0%90%2C%20%D0%A3%D0%B6%D0%B0%D1%81%D1%8B%2C%20%D0%B4%D1%80%D0%B0%D0%BC%D0%B0%2C%20%D0%BA%D0%BE%D0%BC%D0%B5%D0%B4%D0%B8%D1%8F%2C%20WEB-DL%201080p%5D%207%20x%20MVO"
	got, err := Parse(raw)
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if got.Infohash != "baca24e18c7382a9e9a44132c8d7db86c4d319c2" {
		t.Errorf("infohash = %q", got.Infohash)
	}
	// dn раскодирован url.Query() — без ручного urldecode.
	const wantName = "Бухта вдов / Widow's Bay / Сезон: 1 / Серии: 1-10 из 10 " +
		"(Хиро Мурай) [2026, США, Ужасы, драма, комедия, WEB-DL 1080p] 7 x MVO"
	if got.DisplayName != wantName {
		t.Errorf("dn = %q", got.DisplayName)
	}
	ctx := got.Context()
	if !strings.Contains(ctx, "Widow's Bay") || !strings.Contains(ctx, "2026") {
		t.Errorf("Context() без релиз-данных: %q", ctx)
	}
	if !strings.Contains(ctx, "Трекер: t-ru.org") {
		t.Errorf("Context() без домена трекера (bt4. должен срезаться): %q", ctx)
	}
	if strings.Contains(ctx, "bt4.") {
		t.Errorf("служебный поддомен не срезан: %q", ctx)
	}
}

// Ссылку могли скопировать целиком в процент-кодировке (например вытащили из
// другого URL) — Parse должен раскодировать её один раз и разобрать.
func TestParseDoubleEncoded(t *testing.T) {
	const inner = "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&dn=Dune"
	got, err := Parse(url.QueryEscape(inner))
	if err != nil {
		t.Fatalf("Parse(double-encoded): %v", err)
	}
	if got.Infohash != "541adcff3b6dd5dba7088ea83317d9d6fac331d6" {
		t.Errorf("infohash = %q", got.Infohash)
	}
	if got.DisplayName != "Dune" {
		t.Errorf("dn = %q", got.DisplayName)
	}
}

func TestParseNormalisesCase(t *testing.T) {
	lower := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6"
	upper := "magnet:?xt=urn:btih:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6"
	a, err := Parse(lower)
	if err != nil {
		t.Fatal(err)
	}
	b, err := Parse(upper)
	if err != nil {
		t.Fatal(err)
	}
	if a.Infohash != b.Infohash {
		t.Errorf("регистр влияет: %q != %q", a.Infohash, b.Infohash)
	}
}
