package download

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
)

// prowlarrStub is a fake Prowlarr instance.
type prowlarrStub struct {
	server *httptest.Server

	mu sync.Mutex

	results  []prowlarrResult
	indexers []map[string]any

	// lastQuery records the search query string for assertions.
	lastQuery string

	unauthorized bool
}

func newProwlarrStub(t *testing.T) *prowlarrStub {
	t.Helper()

	s := &prowlarrStub{
		indexers: []map[string]any{{"id": 1, "enable": true}},
	}

	mux := http.NewServeMux()

	mux.HandleFunc("/api/v1/system/status", func(w http.ResponseWriter, r *http.Request) {
		if s.reject(w, r) {
			return
		}

		writeJSON(t, w, map[string]any{"version": "1.0.0"})
	})

	mux.HandleFunc("/api/v1/indexer", func(w http.ResponseWriter, r *http.Request) {
		if s.reject(w, r) {
			return
		}

		s.mu.Lock()
		indexers := s.indexers
		s.mu.Unlock()

		writeJSON(t, w, indexers)
	})

	mux.HandleFunc("/api/v1/search", func(w http.ResponseWriter, r *http.Request) {
		if s.reject(w, r) {
			return
		}

		s.mu.Lock()
		s.lastQuery = r.URL.RawQuery
		results := s.results
		s.mu.Unlock()

		writeJSON(t, w, results)
	})

	s.server = httptest.NewServer(mux)
	t.Cleanup(s.server.Close)

	return s
}

func (s *prowlarrStub) reject(w http.ResponseWriter, r *http.Request) bool {
	s.mu.Lock()
	unauthorized := s.unauthorized
	s.mu.Unlock()

	if unauthorized || r.Header.Get("X-Api-Key") != "test-key" {
		w.WriteHeader(http.StatusUnauthorized)

		return true
	}

	return false
}

func newStubProwlarr(t *testing.T, stub *prowlarrStub, settings map[string]string) *prowlarr {
	t.Helper()

	if settings == nil {
		settings = map[string]string{}
	}

	settings["url"] = stub.server.URL

	p, err := newProwlarr(
		Config{ID: 1, Kind: KindProwlarr, Name: "prowlarr", Settings: settings},
		func(string) (string, error) { return "test-key", nil },
		slogDiscard(),
	)
	if err != nil {
		t.Fatalf("newProwlarr: %v", err)
	}

	pr, ok := p.(*prowlarr)
	if !ok {
		t.Fatalf("provider is %T, want *prowlarr", p)
	}

	return pr
}

func TestProwlarrCheck(t *testing.T) {
	t.Parallel()

	stub := newProwlarrStub(t)
	p := newStubProwlarr(t, stub, nil)

	if err := p.Check(context.Background()); err != nil {
		t.Errorf("Check: %v", err)
	}
}

// Prowlarr with every indexer disabled will silently return nothing
// forever, which is worth surfacing at configuration time.
func TestProwlarrCheckRequiresEnabledIndexer(t *testing.T) {
	t.Parallel()

	stub := newProwlarrStub(t)
	stub.indexers = []map[string]any{{"id": 1, "enable": false}}

	p := newStubProwlarr(t, stub, nil)

	if err := p.Check(context.Background()); !errors.Is(
		err, ErrProwlarrNoIndexers,
	) {
		t.Errorf("error = %v, want ErrProwlarrNoIndexers", err)
	}
}

// Prowlarr fills only the Searcher role, so its candidates must carry a
// protocol the pipeline can pair with a transport.
func TestProwlarrSearchMarksProtocols(t *testing.T) {
	t.Parallel()

	stub := newProwlarrStub(t)
	stub.results = []prowlarrResult{
		{
			GUID:      "a",
			Title:     "Radiohead - OK Computer [FLAC]",
			Indexer:   "SomeTracker",
			Protocol:  "torrent",
			Seeders:   50,
			Size:      400_000_000,
			MagnetURL: "magnet:?xt=urn:btih:abc123",
		},
		{
			GUID:        "b",
			Title:       "Radiohead - OK Computer [MP3]",
			Indexer:     "SomeUsenet",
			Protocol:    "usenet",
			Size:        90_000_000,
			DownloadURL: "https://example.com/x.nzb",
		},
	}

	p := newStubProwlarr(t, stub, nil)

	got, err := p.Search(context.Background(), Download{
		Artist: "Radiohead",
		Album:  "OK Computer",
	})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}

	if len(got) != 2 {
		t.Fatalf("got %d candidates, want 2", len(got))
	}

	byProtocol := map[Protocol]Candidate{}
	for _, c := range got {
		byProtocol[c.Protocol] = c
	}

	torrent, ok := byProtocol[ProtocolTorrent]
	if !ok {
		t.Fatal("no torrent candidate")
	}

	if torrent.Payload["link"] != "magnet:?xt=urn:btih:abc123" {
		t.Errorf("torrent link = %q, want the magnet", torrent.Payload["link"])
	}

	usenet, ok := byProtocol[ProtocolUsenet]
	if !ok {
		t.Fatal("no usenet candidate")
	}

	if usenet.Payload["link"] != "https://example.com/x.nzb" {
		t.Errorf("usenet link = %q, want the NZB URL", usenet.Payload["link"])
	}

	// Indexer results are opaque before fetching; claiming a file list
	// would be inventing information.
	if len(torrent.Files) != 0 {
		t.Errorf("torrent candidate has %d files, want none", len(torrent.Files))
	}
}

// A torrent nobody is seeding will never finish, so offering it wastes
// the user's choice.
func TestProwlarrFiltersDeadTorrents(t *testing.T) {
	t.Parallel()

	stub := newProwlarrStub(t)
	stub.results = []prowlarrResult{
		{
			GUID: "dead", Title: "Dead", Protocol: "torrent",
			Seeders: 0, MagnetURL: "magnet:?xt=urn:btih:dead",
		},
		{
			GUID: "alive", Title: "Alive", Protocol: "torrent",
			Seeders: 10, MagnetURL: "magnet:?xt=urn:btih:alive",
		},
	}

	p := newStubProwlarr(t, stub, map[string]string{"minSeeders": "1"})

	got, err := p.Search(context.Background(), Download{Query: "x"})
	if err != nil {
		t.Fatalf("Search: %v", err)
	}

	if len(got) != 1 {
		t.Fatalf("got %d candidates, want 1", len(got))
	}

	if got[0].Title != "Alive" {
		t.Errorf("kept %q, want the seeded torrent", got[0].Title)
	}
}

// Searching without a category constraint returns films and software
// alongside music.
func TestProwlarrSearchesMusicCategory(t *testing.T) {
	t.Parallel()

	stub := newProwlarrStub(t)
	p := newStubProwlarr(t, stub, nil)

	if _, err := p.Search(
		context.Background(), Download{Query: "radiohead"},
	); err != nil {
		t.Fatalf("Search: %v", err)
	}

	stub.mu.Lock()
	query := stub.lastQuery
	stub.mu.Unlock()

	if !strings.Contains(query, "categories="+prowlarrMusicCategory) {
		t.Errorf("query %q does not constrain to the music category", query)
	}
}

func TestSwarmHealth(t *testing.T) {
	t.Parallel()

	// More seeders is never worse.
	prev := -1.0

	for _, seeders := range []int{0, 1, 3, 10, 50, 500} {
		got := swarmHealth(ProtocolTorrent, seeders)

		if got < prev {
			t.Errorf("health fell at %d seeders: %f after %f", seeders, got, prev)
		}

		if got < 0 || got > 1 {
			t.Errorf("health %f out of range at %d seeders", got, seeders)
		}

		prev = got
	}

	// Usenet has no swarm, so seeder count is meaningless there.
	if a, b := swarmHealth(ProtocolUsenet, 0), swarmHealth(ProtocolUsenet, 99); a != b {
		t.Errorf("usenet health varied with seeders: %f vs %f", a, b)
	}
}

func TestProtocolFor(t *testing.T) {
	t.Parallel()

	tests := map[string]Protocol{
		"torrent": ProtocolTorrent,
		"Torrent": ProtocolTorrent,
		"usenet":  ProtocolUsenet,
		"USENET":  ProtocolUsenet,
		"weird":   ProtocolDirect,
		"":        ProtocolDirect,
	}

	for in, want := range tests {
		if got := protocolFor(in); got != want {
			t.Errorf("protocolFor(%q) = %q, want %q", in, got, want)
		}
	}
}
