package ingest

import (
	"bytes"
	"context"
	"crypto/sha1"
	"encoding/hex"
	"strings"
	"testing"

	"github.com/anacrolix/torrent/bencode"
	"github.com/anacrolix/torrent/metainfo"

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

// buildTorrent собирает валидные байты .torrent и ожидаемый v1-инфохэш.
func buildTorrent(t *testing.T, name, announce string) (data []byte, infohash string) {
	t.Helper()
	info := metainfo.Info{Name: name, Length: 1024, PieceLength: 512, Pieces: make([]byte, 40)}
	infoBytes, err := bencode.Marshal(info)
	if err != nil {
		t.Fatalf("marshal info: %v", err)
	}
	sum := sha1.Sum(infoBytes)
	mi := metainfo.MetaInfo{InfoBytes: infoBytes, Announce: announce}
	var buf bytes.Buffer
	if err := mi.Write(&buf); err != nil {
		t.Fatalf("write metainfo: %v", err)
	}
	return buf.Bytes(), hex.EncodeToString(sum[:])
}

func TestIngestTorrentFile(t *testing.T) {
	data, wantHash := buildTorrent(t, "Dune.Part.Two.2024.mkv", "http://bt.rutracker.org/ann")
	fs := &fakeStore{}
	res, err := newService(fs).Ingest(context.Background(), Request{TorrentData: data, Context: "мой текст"})
	if err != nil {
		t.Fatalf("Ingest: %v", err)
	}
	if res.State != store.StateCatched || res.Deduplicated {
		t.Errorf("res = %+v", res)
	}
	if len(res.Infohashes) != 1 || res.Infohashes[0] != wantHash {
		t.Errorf("infohashes = %v, want [%s]", res.Infohashes, wantHash)
	}
	if len(fs.created) != 1 {
		t.Fatalf("создано задач: %d", len(fs.created))
	}
	d := fs.created[0]
	if d.SourceType != store.SourceTorrent {
		t.Errorf("source_type = %q, want torrent", d.SourceType)
	}
	if d.SourceRef != "Dune.Part.Two.2024.mkv" {
		t.Errorf("source_ref = %q (имя раздачи, не URL)", d.SourceRef)
	}
	// Контекст: текст пользователя первым, затем синтез из полей файла.
	if !strings.HasPrefix(d.Context, "мой текст") {
		t.Errorf("context не с текста пользователя: %q", d.Context)
	}
	if !strings.Contains(d.Context, "Dune.Part.Two") || !strings.Contains(d.Context, "rutracker.org") {
		t.Errorf("context без синтеза из файла: %q", d.Context)
	}
	// Байты сохранены в транзакции создания.
	if len(fs.blobs) != 1 || !bytes.Equal(fs.blobs[0], data) {
		t.Errorf("байты .torrent не переданы в CreateDownloadIfNoActive")
	}
}

func TestIngestTorrentDedupNoBlob(t *testing.T) {
	data, _ := buildTorrent(t, "X", "http://t/ann")
	// Активная задача уже есть → дедуп; байты писаться не должны.
	fs := &fakeStore{active: &store.Download{ID: "existing", State: store.StateDownloading}}
	res, err := newService(fs).Ingest(context.Background(), Request{TorrentData: data})
	if err != nil {
		t.Fatalf("Ingest: %v", err)
	}
	if !res.Deduplicated || res.DownloadID != "existing" {
		t.Errorf("ожидался дедуп, res = %+v", res)
	}
	if len(fs.created) != 0 || len(fs.blobs) != 0 {
		t.Errorf("при дедупе не должно быть создания/записи байтов")
	}
}

// F6: дедуп .torrent на пойманную (catched) magnet-задачу вызывает апгрейд —
// сохранение байтов и смену источника (magnet закрытого трекера иначе застрянет
// в metaDL).
func TestIngestTorrentUpgradesCatchedMagnet(t *testing.T) {
	data, hash := buildTorrent(t, "Dune", "http://t/ann")
	existing := &store.Download{
		ID:         "cm",
		State:      store.StateCatched,
		SourceType: store.SourceMagnet,
		Infohashes: []store.Infohash{{Infohash: hash, Kind: store.HashV1}},
	}
	fs := &fakeStore{active: existing, upgradeUp: true}
	res, err := newService(fs).Ingest(context.Background(), Request{TorrentData: data})
	if err != nil {
		t.Fatalf("Ingest: %v", err)
	}
	if !res.Deduplicated || res.DownloadID != "cm" {
		t.Errorf("ожидался дедуп на cm, res = %+v", res)
	}
	if fs.upgradeID != "cm" {
		t.Errorf("апгрейд не вызван для существующей задачи (upgradeID=%q)", fs.upgradeID)
	}
	if !bytes.Equal(fs.upgradeBlob, data) {
		t.Errorf("в апгрейд переданы не те байты")
	}
}

// Дедуп magnet-ссылки (не .torrent) апгрейд не вызывает — нечего сохранять.
func TestIngestMagnetDedupNoUpgrade(t *testing.T) {
	existing := &store.Download{ID: "cm", State: store.StateCatched, SourceType: store.SourceMagnet}
	fs := &fakeStore{active: existing}
	if _, err := newService(fs).Ingest(context.Background(), Request{Source: sampleMagnet}); err != nil {
		t.Fatalf("Ingest: %v", err)
	}
	if fs.upgradeID != "" {
		t.Errorf("апгрейд не должен вызываться для magnet-дедупа")
	}
}

func TestIngestTorrentTooLarge(t *testing.T) {
	fs := &fakeStore{}
	big := make([]byte, MaxTorrentSize+1)
	_, err := newService(fs).Ingest(context.Background(), Request{TorrentData: big})
	if err == nil {
		t.Fatal("ожидалась ошибка превышения размера")
	}
	if len(fs.created) != 0 {
		t.Errorf("при превышении размера задача не создаётся")
	}
}

// У раздачи без содержательного имени source_ref берётся из имени файла.
// Случая два, и они разные: раздача БЕЗ поля name (BestName() == "") и
// раздача, объявившая вырожденное `-` (metainfo.NoName) — второй нормализует
// разборщик, приём про него уже не знает.
func TestIngestTorrentNameFallback(t *testing.T) {
	for _, tc := range []struct {
		name     string
		infoName string
	}{
		{"без поля name", ""},
		{"вырожденное имя", metainfo.NoName},
	} {
		t.Run(tc.name, func(t *testing.T) {
			info := metainfo.Info{Name: tc.infoName, Length: 1024, PieceLength: 512, Pieces: make([]byte, 40)}
			infoBytes, err := bencode.Marshal(info)
			if err != nil {
				t.Fatalf("marshal: %v", err)
			}
			mi := metainfo.MetaInfo{InfoBytes: infoBytes, Announce: "http://t/ann"}
			var buf bytes.Buffer
			if err := mi.Write(&buf); err != nil {
				t.Fatalf("write: %v", err)
			}

			fs := &fakeStore{}
			_, err = newService(fs).Ingest(context.Background(),
				Request{TorrentData: buf.Bytes(), TorrentName: "Fallback.Name.torrent"})
			if err != nil {
				t.Fatalf("Ingest: %v", err)
			}
			if len(fs.created) != 1 || fs.created[0].SourceRef != "Fallback.Name.torrent" {
				t.Errorf("source_ref = %q, want фолбек на имя файла", fs.created[0].SourceRef)
			}
		})
	}
}

func TestIngestTorrentInvalid(t *testing.T) {
	fs := &fakeStore{}
	_, err := newService(fs).Ingest(context.Background(), Request{TorrentData: []byte("not a torrent")})
	if err == nil {
		t.Fatal("ожидалась ошибка разбора .torrent")
	}
}
