package tags

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

func newMBProvider(enabled bool) *musicbrainzProvider {
	p := &musicbrainzProvider{client: newHTTPClient(httpClientOptions{Name: "musicbrainz", MaxRetries: 1})}
	_ = p.Configure(ProviderSettings{Enabled: enabled})
	return p
}

func tagsByName(ts []Tag) map[string]float64 {
	m := make(map[string]float64, len(ts))
	for _, t := range ts {
		m[t.Name] = t.Weight
	}
	return m
}

func TestNormalizeMBTags(t *testing.T) {
	got := tagsByName(normalizeMBTags([]mbTag{
		{Count: 3, Name: "rock"},
		{Count: 6, Name: "shoegaze"},
		{Count: 0, Name: "zero-drop"},
		{Count: -2, Name: "downvoted-drop"},
		{Count: 5, Name: ""},
	}))
	if len(got) != 2 {
		t.Fatalf("want 2 tags, got %v", got)
	}
	if got["shoegaze"] != 1.0 {
		t.Errorf("shoegaze weight = %v, want 1.0", got["shoegaze"])
	}
	if got["rock"] != 0.5 {
		t.Errorf("rock weight = %v, want 0.5", got["rock"])
	}
	if normalizeMBTags(nil) != nil {
		t.Error("nil input should yield nil")
	}
}

func TestMusicBrainzFetch_GatedOff(t *testing.T) {
	// Disabled → ErrNotFound without any network call.
	if _, err := newMBProvider(false).FetchTrackTags(context.Background(),
		TrackRef{MBID: "abc"}); !errors.Is(err, ErrNotFound) {
		t.Errorf("disabled: err = %v, want ErrNotFound", err)
	}
	// No recording MBID and no artist MBID → nothing to look up → ErrNotFound.
	if _, err := newMBProvider(true).FetchTrackTags(context.Background(),
		TrackRef{ArtistName: "A", Title: "B"}); !errors.Is(err, ErrNotFound) {
		t.Errorf("no MBID: err = %v, want ErrNotFound", err)
	}
}

// mbEntityServer returns tags keyed by entity path segment ("recording" or
// "artist"), so a test can make the recording untagged while the artist has
// tags (or vice-versa). An empty body string means "{tags:[]}".
func mbEntityServer(recordingJSON, artistJSON string) *httptest.Server {
	empty := `{"tags":[]}`
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body := empty
		switch {
		case strings.Contains(r.URL.Path, "/recording/"):
			if recordingJSON != "" {
				body = recordingJSON
			}
		case strings.Contains(r.URL.Path, "/artist/"):
			if artistJSON != "" {
				body = artistJSON
			}
		}
		_, _ = w.Write([]byte(body))
	}))
}

func TestMusicBrainzFetch_ArtistFallbackWhenRecordingUntagged(t *testing.T) {
	// Recording exists but has no tags; artist has tags → use the artist's,
	// down-weighted by artistTagWeightFactor.
	srv := mbEntityServer(``, `{"tags":[{"count":4,"name":"art rock"},{"count":2,"name":"experimental"}]}`)
	defer srv.Close()
	old := mbBaseURL
	mbBaseURL = srv.URL
	defer func() { mbBaseURL = old }()

	tags, err := newMBProvider(true).FetchTrackTags(context.Background(),
		TrackRef{MBID: "rec-1", ArtistMBID: "art-1"})
	if err != nil {
		t.Fatalf("fetch: %v", err)
	}
	m := tagsByName(tags)
	// "art rock" is the top artist tag (weight 1.0) × 0.6 fallback factor.
	if got := m["art rock"]; got != 0.6 {
		t.Errorf("art rock = %v, want 0.6 (down-weighted)", got)
	}
	if got := m["experimental"]; got != 0.3 { // 0.5 × 0.6
		t.Errorf("experimental = %v, want 0.3", got)
	}
}

func TestMusicBrainzFetch_ArtistOnlyWhenNoRecordingMBID(t *testing.T) {
	srv := mbEntityServer(``, `{"tags":[{"count":1,"name":"folk"}]}`)
	defer srv.Close()
	old := mbBaseURL
	mbBaseURL = srv.URL
	defer func() { mbBaseURL = old }()

	tags, err := newMBProvider(true).FetchTrackTags(context.Background(),
		TrackRef{ArtistName: "A", Title: "B", ArtistMBID: "art-2"})
	if err != nil {
		t.Fatalf("fetch: %v", err)
	}
	if got := tagsByName(tags)["folk"]; got != 0.6 {
		t.Errorf("folk = %v, want 0.6", got)
	}
}

func TestMusicBrainzFetch_RecordingPreferredOverArtist(t *testing.T) {
	// Recording has tags → they win at full weight; artist is never consulted.
	srv := mbEntityServer(`{"tags":[{"count":3,"name":"shoegaze"}]}`,
		`{"tags":[{"count":9,"name":"rock"}]}`)
	defer srv.Close()
	old := mbBaseURL
	mbBaseURL = srv.URL
	defer func() { mbBaseURL = old }()

	tags, err := newMBProvider(true).FetchTrackTags(context.Background(),
		TrackRef{MBID: "rec-2", ArtistMBID: "art-3"})
	if err != nil {
		t.Fatalf("fetch: %v", err)
	}
	m := tagsByName(tags)
	if got := m["shoegaze"]; got != 1.0 {
		t.Errorf("shoegaze = %v, want 1.0 (recording, full weight)", got)
	}
	if _, ok := m["rock"]; ok {
		t.Error("artist tag 'rock' leaked in — recording tags should win outright")
	}
}

func TestMusicBrainzFetch_ParsesTags(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"tags":[{"count":2,"name":"post-punk"},{"count":4,"name":"melancholic"}]}`))
	}))
	defer srv.Close()
	old := mbBaseURL
	mbBaseURL = srv.URL
	defer func() { mbBaseURL = old }()

	tags, err := newMBProvider(true).FetchTrackTags(context.Background(), TrackRef{MBID: "mbid-1"})
	if err != nil {
		t.Fatalf("fetch: %v", err)
	}
	m := tagsByName(tags)
	if m["melancholic"] != 1.0 || m["post-punk"] != 0.5 {
		t.Errorf("weights = %v, want melancholic=1.0 post-punk=0.5", m)
	}
}

func TestMusicBrainzFetch_EmptyTags(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"tags":[]}`))
	}))
	defer srv.Close()
	old := mbBaseURL
	mbBaseURL = srv.URL
	defer func() { mbBaseURL = old }()

	if _, err := newMBProvider(true).FetchTrackTags(context.Background(),
		TrackRef{MBID: "mbid-1"}); !errors.Is(err, ErrNotFound) {
		t.Errorf("empty tags: err = %v, want ErrNotFound", err)
	}
}
