package coverart

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"
)

func TestHTTPClient_GetJSON_Success(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"name":"alice"}`))
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	var got struct{ Name string }
	if err := c.getJSON(context.Background(), srv.URL, &got); err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if got.Name != "alice" {
		t.Errorf("Name = %q, want alice", got.Name)
	}
}

func TestHTTPClient_GetJSON_404IsNotFound(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	var out any
	err := c.getJSON(context.Background(), srv.URL, &out)
	if !errors.Is(err, ErrNotFound) {
		t.Errorf("err = %v, want ErrNotFound", err)
	}
}

func TestHTTPClient_GetJSON_RetryOn429ThenSuccess(t *testing.T) {
	var calls atomic.Int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		n := calls.Add(1)
		if n < 3 {
			w.WriteHeader(http.StatusTooManyRequests)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"ok":true}`))
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{
		Name:        "test",
		MaxRetries:  3,
		BaseBackoff: 1 * time.Millisecond, // tiny so the test is fast
	})
	var got struct{ OK bool }
	if err := c.getJSON(context.Background(), srv.URL, &got); err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if calls.Load() != 3 {
		t.Errorf("calls = %d, want 3", calls.Load())
	}
	if !got.OK {
		t.Errorf("OK = false, want true")
	}
}

func TestHTTPClient_GetJSON_RetryOn5xxThenExhausted(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{
		Name:        "test",
		MaxRetries:  2,
		BaseBackoff: 1 * time.Millisecond,
	})
	var out any
	err := c.getJSON(context.Background(), srv.URL, &out)
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestHTTPClient_GetJSON_AuthAsTransient(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{
		Name:                 "test",
		TreatAuthAsTransient: true,
	})
	var out any
	err := c.getJSON(context.Background(), srv.URL, &out)
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestHTTPClient_GetJSON_AuthPassthroughByDefault(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	var out any
	err := c.getJSON(context.Background(), srv.URL, &out)
	if !errors.Is(err, ErrTransient) {
		// 401 is non-2xx so it falls into the generic transient bucket.
		t.Errorf("err = %v, want ErrTransient (generic non-2xx)", err)
	}
}

func TestHTTPClient_GetJSON_MalformedBody(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`not json`))
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	var got struct{ Name string }
	err := c.getJSON(context.Background(), srv.URL, &got)
	if !errors.Is(err, ErrTransient) {
		t.Errorf("err = %v, want ErrTransient", err)
	}
}

func TestHTTPClient_GetJSON_ContextCancel(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{
		Name:        "test",
		MaxRetries:  5,
		BaseBackoff: 200 * time.Millisecond,
	})
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	var out any
	err := c.getJSON(ctx, srv.URL, &out)
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Errorf("err = %v, want context.DeadlineExceeded", err)
	}
}

func TestHTTPClient_GetImage_Success(t *testing.T) {
	want := []byte("\x89PNG\x0d\x0a\x1a\x0a-fake-png-bytes")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "image/png")
		_, _ = w.Write(want)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	got, err := c.getImage(context.Background(), srv.URL)
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if string(got) != string(want) {
		t.Errorf("body mismatch")
	}
}

func TestHTTPClient_GetImage_404IsNotFound(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{Name: "test"})
	_, err := c.getImage(context.Background(), srv.URL)
	if !errors.Is(err, ErrNotFound) {
		t.Errorf("err = %v, want ErrNotFound", err)
	}
}

func TestHTTPClient_RateLimit(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(struct{ OK bool }{true})
	}))
	defer srv.Close()

	c := newHTTPClient(httpClientOptions{
		Name:        "test",
		MinInterval: 50 * time.Millisecond,
	})
	var out struct{ OK bool }
	t0 := time.Now()
	if err := c.getJSON(context.Background(), srv.URL, &out); err != nil {
		t.Fatalf("first call: %v", err)
	}
	if err := c.getJSON(context.Background(), srv.URL, &out); err != nil {
		t.Fatalf("second call: %v", err)
	}
	elapsed := time.Since(t0)
	if elapsed < 50*time.Millisecond {
		t.Errorf("elapsed %v, want >= 50ms (rate limit)", elapsed)
	}
}
