package listenbrainz

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

func newTestClient(handler http.HandlerFunc) (*Client, *httptest.Server) {
	srv := httptest.NewServer(handler)
	return &Client{BaseURL: srv.URL, LabsBaseURL: srv.URL, HTTP: srv.Client()}, srv
}

func TestClient_SubmitListens_Success(t *testing.T) {
	var seenPath, seenAuth, seenBody string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		seenPath = r.URL.Path
		seenAuth = r.Header.Get("Authorization")
		buf := make([]byte, 4096)
		n, _ := r.Body.Read(buf)
		seenBody = string(buf[:n])
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	})
	defer srv.Close()

	listens := []Listen{{
		ListenedAt: 1700000000,
		Track: Track{
			ArtistName:  "Miles Davis",
			TrackName:   "So What",
			ReleaseName: "Kind of Blue",
			DurationMs:  545000,
		},
	}}
	if err := c.SubmitListens(context.Background(), "tk", listens); err != nil {
		t.Fatalf("err: %v", err)
	}
	if seenPath != "/1/submit-listens" {
		t.Errorf("path = %q, want /1/submit-listens", seenPath)
	}
	if seenAuth != "Token tk" {
		t.Errorf("auth = %q, want %q", seenAuth, "Token tk")
	}
	if !strings.Contains(seenBody, `"track_name":"So What"`) {
		t.Errorf("body missing track_name: %s", seenBody)
	}
	if !strings.Contains(seenBody, `"artist_name":"Miles Davis"`) {
		t.Errorf("body missing artist_name: %s", seenBody)
	}
}

func TestClient_SubmitListens_401_ReturnsErrAuth(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	})
	defer srv.Close()
	err := c.SubmitListens(context.Background(), "bad", []Listen{{}})
	if !errors.Is(err, ErrAuth) {
		t.Errorf("err = %v, want ErrAuth", err)
	}
}

func TestClient_SubmitListens_400_ReturnsErrPermanent(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
	})
	defer srv.Close()
	err := c.SubmitListens(context.Background(), "tk", []Listen{{}})
	if !errors.Is(err, ErrPermanent) {
		t.Errorf("err = %v, want ErrPermanent", err)
	}
}

func TestClient_SubmitListens_503_ReturnsErrTransient(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	})
	defer srv.Close()
	err := c.SubmitListens(context.Background(), "tk", []Listen{{}})
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestClient_SubmitListens_429_ReturnsRetryAfter(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Retry-After", "60")
		w.WriteHeader(http.StatusTooManyRequests)
	})
	defer srv.Close()
	err := c.SubmitListens(context.Background(), "tk", []Listen{{}})
	var ra *RetryAfterError
	if !errors.As(err, &ra) {
		t.Fatalf("err = %v, want *RetryAfterError", err)
	}
	if ra.Wait != 60*time.Second {
		t.Errorf("Wait = %v, want 60s", ra.Wait)
	}
}

func TestClient_SubmitListens_NetworkFailure_ReturnsErrTransient(t *testing.T) {
	c := &Client{BaseURL: "http://127.0.0.1:1", HTTP: &http.Client{Timeout: 100 * time.Millisecond}}
	err := c.SubmitListens(context.Background(), "tk", []Listen{{}})
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestClient_SubmitListens_PayloadTypeImportForBatch(t *testing.T) {
	var seenBody string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		buf := make([]byte, 4096)
		n, _ := r.Body.Read(buf)
		seenBody = string(buf[:n])
		w.WriteHeader(http.StatusOK)
	})
	defer srv.Close()
	_ = c.SubmitListens(context.Background(), "tk", []Listen{
		{ListenedAt: 1, Track: Track{ArtistName: "A", TrackName: "X"}},
		{ListenedAt: 2, Track: Track{ArtistName: "B", TrackName: "Y"}},
	})
	if !strings.Contains(seenBody, `"listen_type":"import"`) {
		t.Errorf("batch body missing listen_type=import: %s", seenBody)
	}
}

func TestClient_SubmitListens_PayloadTypeSingleForOne(t *testing.T) {
	var seenBody string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		buf := make([]byte, 4096)
		n, _ := r.Body.Read(buf)
		seenBody = string(buf[:n])
		w.WriteHeader(http.StatusOK)
	})
	defer srv.Close()
	_ = c.SubmitListens(context.Background(), "tk", []Listen{
		{ListenedAt: 1, Track: Track{ArtistName: "A", TrackName: "X"}},
	})
	if !strings.Contains(seenBody, `"listen_type":"single"`) {
		t.Errorf("single body missing listen_type=single: %s", seenBody)
	}
}

func TestClient_SubmitListens_OmitsEmptyMBIDs(t *testing.T) {
	var seenBody string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		buf := make([]byte, 4096)
		n, _ := r.Body.Read(buf)
		seenBody = string(buf[:n])
		w.WriteHeader(http.StatusOK)
	})
	defer srv.Close()
	_ = c.SubmitListens(context.Background(), "tk", []Listen{
		{ListenedAt: 1, Track: Track{ArtistName: "A", TrackName: "X"}},
	})
	if strings.Contains(seenBody, "recording_mbid") {
		t.Errorf("body should omit empty recording_mbid: %s", seenBody)
	}
}

func TestClient_SimilarRecordings_Success(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`[
			{"recording_mbid": "11111111-1111-1111-1111-111111111111", "score": 0.95},
			{"recording_mbid": "22222222-2222-2222-2222-222222222222", "score": 0.80}
		]`))
	})
	defer srv.Close()
	out, err := c.SimilarRecordings(context.Background(), "aaaa", 100)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if len(out) != 2 {
		t.Fatalf("got %d results, want 2", len(out))
	}
	if out[0].MBID != "11111111-1111-1111-1111-111111111111" || out[0].Score != 0.95 {
		t.Errorf("first result wrong: %+v", out[0])
	}
}

func TestClient_SimilarRecordings_AlgorithmParamSet(t *testing.T) {
	var seenURL string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		seenURL = r.URL.String()
		_, _ = w.Write([]byte(`[]`))
	})
	defer srv.Close()
	_, _ = c.SimilarRecordings(context.Background(), "abc", 50)
	if !strings.Contains(seenURL, "algorithm=") {
		t.Errorf("URL missing algorithm param: %q", seenURL)
	}
}

func TestClient_SimilarRecordings_MbidParamSet(t *testing.T) {
	var seenURL string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		seenURL = r.URL.String()
		_, _ = w.Write([]byte(`[]`))
	})
	defer srv.Close()
	_, _ = c.SimilarRecordings(context.Background(), "abc", 50)
	// Labs API takes the MBID as a query param, not a path segment; the
	// result count is fixed by the algorithm (no count/limit param).
	if !strings.Contains(seenURL, "recording_mbids=abc") {
		t.Errorf("URL missing recording_mbids param: %q", seenURL)
	}
	if !strings.Contains(seenURL, "/similar-recordings/json") {
		t.Errorf("URL not the Labs similar-recordings endpoint: %q", seenURL)
	}
}

func TestClient_SimilarRecordings_401_ReturnsErrAuth(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	})
	defer srv.Close()
	_, err := c.SimilarRecordings(context.Background(), "abc", 100)
	if !errors.Is(err, ErrAuth) {
		t.Errorf("err = %v, want ErrAuth", err)
	}
}

func TestClient_SimilarRecordings_400_ReturnsErrPermanent(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
	})
	defer srv.Close()
	_, err := c.SimilarRecordings(context.Background(), "abc", 100)
	if !errors.Is(err, ErrPermanent) {
		t.Errorf("err = %v, want ErrPermanent", err)
	}
}

func TestClient_SimilarRecordings_503_ReturnsErrTransient(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	})
	defer srv.Close()
	_, err := c.SimilarRecordings(context.Background(), "abc", 100)
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestClient_SimilarRecordings_429_ReturnsRetryAfter(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Retry-After", "120")
		w.WriteHeader(http.StatusTooManyRequests)
	})
	defer srv.Close()
	_, err := c.SimilarRecordings(context.Background(), "abc", 100)
	var ra *RetryAfterError
	if !errors.As(err, &ra) {
		t.Fatalf("err = %v, want *RetryAfterError", err)
	}
	if ra.Wait != 120*time.Second {
		t.Errorf("Wait = %v, want 120s", ra.Wait)
	}
}

func TestClient_SimilarArtists_Success(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`[
			{"artist_mbid": "33333333-3333-3333-3333-333333333333", "score": 0.91},
			{"artist_mbid": "44444444-4444-4444-4444-444444444444", "score": 0.72}
		]`))
	})
	defer srv.Close()
	out, err := c.SimilarArtists(context.Background(), "aaaa", 100)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if len(out) != 2 {
		t.Fatalf("got %d, want 2", len(out))
	}
	if out[0].MBID != "33333333-3333-3333-3333-333333333333" || out[0].Score != 0.91 {
		t.Errorf("first result wrong: %+v", out[0])
	}
}

func TestClient_SimilarArtists_AlgorithmParamSet(t *testing.T) {
	var seenURL string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		seenURL = r.URL.String()
		_, _ = w.Write([]byte(`[]`))
	})
	defer srv.Close()
	_, _ = c.SimilarArtists(context.Background(), "abc", 50)
	if !strings.Contains(seenURL, "algorithm=") {
		t.Errorf("URL missing algorithm param: %q", seenURL)
	}
}

func TestClient_SimilarArtists_MbidParamSet(t *testing.T) {
	var seenURL string
	c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) {
		seenURL = r.URL.String()
		_, _ = w.Write([]byte(`[]`))
	})
	defer srv.Close()
	_, _ = c.SimilarArtists(context.Background(), "abc", 50)
	if !strings.Contains(seenURL, "artist_mbids=abc") {
		t.Errorf("URL missing artist_mbids param: %q", seenURL)
	}
	if !strings.Contains(seenURL, "/similar-artists/json") {
		t.Errorf("URL not the Labs similar-artists endpoint: %q", seenURL)
	}
}

func TestClient_SimilarArtists_401_ReturnsErrAuth(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	})
	defer srv.Close()
	_, err := c.SimilarArtists(context.Background(), "abc", 100)
	if !errors.Is(err, ErrAuth) {
		t.Errorf("err = %v, want ErrAuth", err)
	}
}

func TestClient_SimilarArtists_400_ReturnsErrPermanent(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
	})
	defer srv.Close()
	_, err := c.SimilarArtists(context.Background(), "abc", 100)
	if !errors.Is(err, ErrPermanent) {
		t.Errorf("err = %v, want ErrPermanent", err)
	}
}

func TestClient_SimilarArtists_503_ReturnsErrTransient(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	})
	defer srv.Close()
	_, err := c.SimilarArtists(context.Background(), "abc", 100)
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestClient_SimilarArtists_429_ReturnsRetryAfter(t *testing.T) {
	c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Retry-After", "60")
		w.WriteHeader(http.StatusTooManyRequests)
	})
	defer srv.Close()
	_, err := c.SimilarArtists(context.Background(), "abc", 100)
	var ra *RetryAfterError
	if !errors.As(err, &ra) {
		t.Fatalf("err = %v, want *RetryAfterError", err)
	}
	if ra.Wait != 60*time.Second {
		t.Errorf("Wait = %v, want 60s", ra.Wait)
	}
}
