package worker

import (
	"context"
	"testing"

	"git.vakhrushev.me/av/jellybit/internal/qbt"
	"git.vakhrushev.me/av/jellybit/internal/store"
)

const ihDisc = "7931aa3ed6666746012f5739d099b5bc64d72a16"

func emptyStore() *fakeStore {
	return &fakeStore{downloads: map[string]*store.Download{}}
}

// findByInfohash возвращает усыновлённую задачу по infohash.
func findByInfohash(st *fakeStore, infohash string) *store.Download {
	for _, d := range st.downloads {
		if hasAnyHash(d, []string{infohash}) {
			return d
		}
	}
	return nil
}

func TestDiscover_AdoptsByCategory(t *testing.T) {
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: ihDisc, Name: "Avatar", Category: "jellybit", State: "stalledUP", AddedOn: 1_700_000_000},
	})

	d := findByInfohash(st, ihDisc)
	if d == nil {
		t.Fatal("раздача с категорией jellybit не усыновлена")
	}
	if d.State != store.StateDownloading || d.SourceType != store.SourceMagnet {
		t.Errorf("adopted = %+v", d)
	}
	if len(d.Infohashes) != 1 || d.Infohashes[0].Kind != store.HashV1 {
		t.Errorf("infohashes = %+v", d.Infohashes)
	}
	// Усыновление берёт заголовок из имени торрента qBittorrent и фиксирует
	// время добавления (added_on) как базис сортировки.
	if d.DisplayName != "Avatar" {
		t.Errorf("display_name = %q, want Avatar", d.DisplayName)
	}
	if !d.SourceAddedAt.Valid {
		t.Errorf("source_added_at не зафиксирован при усыновлении")
	}
}

func TestDiscover_AdoptsByTag(t *testing.T) {
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.cfg.Tag = "jellybit"
	// Категория чужая, но тег наш — усыновляем (не трогая категорию).
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: ihDisc, Name: "Fargo", Category: "movies", Tags: "hd, jellybit, rus", State: "uploading"},
	})
	if findByInfohash(st, ihDisc) == nil {
		t.Fatal("раздача с тегом jellybit не усыновлена")
	}
}

func TestDiscover_SkipsUntracked(t *testing.T) {
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.cfg.Tag = "jellybit"
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: ihDisc, Category: "movies", Tags: "hd, rus"},
	})
	if len(st.downloads) != 0 {
		t.Errorf("чужая раздача не должна усыновляться: %+v", st.downloads)
	}
}

func TestDiscover_SkipsExisting(t *testing.T) {
	st := emptyStore()
	// Уже есть задача (напр. терминальная done) — не переусыновляем.
	st.downloads["1"] = &store.Download{
		ID: "1", State: store.StateDone, Infohashes: hashesOf("1", ihDisc),
	}
	w := newTestWorker(st, &fakeQbt{})
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: ihDisc, Category: "jellybit"},
	})
	if len(st.downloads) != 1 {
		t.Errorf("существующий infohash не должен порождать новую задачу: %d", len(st.downloads))
	}
}

func TestDiscover_SkipsNoInfohash(t *testing.T) {
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.discover(context.Background(), []qbt.Torrent{{Category: "jellybit"}})
	if len(st.downloads) != 0 {
		t.Error("без infohash усыновлять нечего")
	}
}

// TestPoll_AdoptsAndCompletes — сценарий пользователя целиком: помеченная и
// уже скачанная раздача за один тик усыновляется и доходит до completed.
func TestPoll_CapturesSourceAddedAt(t *testing.T) {
	ih := "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
	st := &fakeStore{downloads: map[string]*store.Download{
		"1": {ID: "1", State: store.StateDownloading, SourceType: store.SourceMagnet,
			Infohashes: hashesOf("1", ih)},
	}}
	qb := &fakeQbt{torrents: []qbt.Torrent{
		{Hash: ih, Name: "X", Category: "jellybit", State: "downloading", AddedOn: 1_700_000_000},
	}}
	w := newTestWorker(st, qb)

	if err := w.Poll(context.Background()); err != nil {
		t.Fatalf("Poll: %v", err)
	}
	if d := st.downloads["1"]; !d.SourceAddedAt.Valid {
		t.Fatalf("source_added_at не захвачен при поллинге активной задачи")
	}
}

func TestPoll_AdoptsAndCompletes(t *testing.T) {
	st := emptyStore()
	qb := &fakeQbt{torrents: []qbt.Torrent{
		{Hash: ihDisc, Name: "Avatar", Category: "other", Tags: "jellybit", State: "stalledUP"},
	}}
	w := newTestWorker(st, qb)
	w.cfg.Tag = "jellybit"

	if err := w.Poll(context.Background()); err != nil {
		t.Fatalf("Poll: %v", err)
	}
	d := findByInfohash(st, ihDisc)
	if d == nil {
		t.Fatal("не усыновлено")
	}
	if d.State != store.StateCompleted {
		t.Errorf("state = %q, want completed (готовая раздача)", d.State)
	}
}

func TestHasTag(t *testing.T) {
	cases := []struct {
		tags, tag string
		want      bool
	}{
		{"jellybit", "jellybit", true},
		{"hd, jellybit, rus", "jellybit", true},
		{"hd,rus", "jellybit", false},
		{"jellybit-extra", "jellybit", false},
		{"", "jellybit", false},
		{"jellybit", "", false},
	}
	for _, c := range cases {
		if got := hasTag(c.tags, c.tag); got != c.want {
			t.Errorf("hasTag(%q,%q) = %v, want %v", c.tags, c.tag, got, c.want)
		}
	}
}

func TestTorrentHashes(t *testing.T) {
	got := torrentHashes(qbt.Torrent{Hash: "ABC", InfohashV1: "abc", InfohashV2: "DEF"})
	if len(got) != 2 || got[0] != "abc" || got[1] != "def" {
		t.Errorf("got %v, want [abc def] (lowercase, без дублей, v1 первым)", got)
	}
	if got := torrentHashes(qbt.Torrent{}); len(got) != 0 {
		t.Errorf("got %v, want empty", got)
	}
	// Старый qBittorrent без infohash_v1/v2 — берём hash.
	if got := torrentHashes(qbt.Torrent{Hash: "ABC"}); len(got) != 1 || got[0] != "abc" {
		t.Errorf("legacy hash: got %v, want [abc]", got)
	}
	// v2-only: t.Hash — УСЕЧЁННЫЙ v2 (40 hex), хранить его нельзя.
	const v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	got = torrentHashes(qbt.Torrent{Hash: v2[:40], InfohashV2: v2})
	if len(got) != 1 || got[0] != v2 {
		t.Errorf("v2-only: got %v, want только полный v2", got)
	}
}

// TestTorrentIndexHashes — NIT-11: lookup-мапы индексируют раздачу по её хешам,
// но усечённый 40-hex t.Hash v2-only торрента исключаем (по длине неотличим от v1).
func TestTorrentIndexHashes(t *testing.T) {
	// Гибрид: индексируем оба infohash, t.Hash игнорируем.
	got := torrentIndexHashes(qbt.Torrent{Hash: "trunc", InfohashV1: "v1", InfohashV2: "v2"})
	if len(got) != 2 || got[0] != "v1" || got[1] != "v2" {
		t.Errorf("гибрид: got %v, want [v1 v2] (t.Hash исключён)", got)
	}
	// Старый qBittorrent без infohash_v1/v2 — берём hash (это полный v1).
	if got := torrentIndexHashes(qbt.Torrent{Hash: "legacy"}); len(got) != 1 || got[0] != "legacy" {
		t.Errorf("legacy: got %v, want [legacy]", got)
	}
	// v2-only: усечённый t.Hash не индексируем, только полный v2.
	const v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	if got := torrentIndexHashes(qbt.Torrent{Hash: v2[:40], InfohashV2: v2}); len(got) != 1 || got[0] != v2 {
		t.Errorf("v2-only: got %v, want [%s] (усечённый t.Hash исключён)", got, v2)
	}
	if got := torrentIndexHashes(qbt.Torrent{}); len(got) != 0 {
		t.Errorf("пустой: got %v, want empty", got)
	}
}

// Усыновление v2-only раздачи: SourceRef — валидный btmh-magnet из полного
// v2-хеша (не битый btih из усечённого), kind в БД — v2.
func TestDiscover_AdoptsV2Only(t *testing.T) {
	const v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: v2[:40], InfohashV2: v2, Name: "V2Only", Category: "jellybit", State: "downloading"},
	})
	d := findByInfohash(st, v2)
	if d == nil {
		t.Fatal("v2-only раздача не усыновлена")
	}
	if d.SourceRef != "magnet:?xt=urn:btmh:1220"+v2 {
		t.Errorf("SourceRef = %q, want btmh с полным v2", d.SourceRef)
	}
	if len(d.Infohashes) != 1 || d.Infohashes[0].Kind != store.HashV2 {
		t.Errorf("infohashes = %+v, want один v2 (усечённый не хранится)", d.Infohashes)
	}
}

// Усыновление гибридного торрента записывает оба хеша.
func TestDiscover_AdoptsBothHashes(t *testing.T) {
	const v2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	st := emptyStore()
	w := newTestWorker(st, &fakeQbt{})
	w.discover(context.Background(), []qbt.Torrent{
		{Hash: ihDisc, InfohashV1: ihDisc, InfohashV2: v2, Name: "Hybrid", Category: "jellybit", State: "downloading"},
	})
	d := findByInfohash(st, v2)
	if d == nil {
		t.Fatal("гибридная раздача не находится по v2-хешу")
	}
	if len(d.Infohashes) != 2 {
		t.Errorf("infohashes = %+v, want v1+v2", d.Infohashes)
	}
}
