package httputil_test

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/libnovel/backend/internal/httputil"
)

// ── RetryGet ──────────────────────────────────────────────────────────────────

func TestRetryGet_ImmediateSuccess(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello"))
	}))
	defer srv.Close()

	body, err := httputil.RetryGet(context.Background(), srv.Client(), srv.URL, 3, time.Millisecond)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if body != "hello" {
		t.Errorf("want hello, got %q", body)
	}
}

func TestRetryGet_RetriesOn5xx(t *testing.T) {
	calls := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls++
		if calls < 3 {
			w.WriteHeader(http.StatusServiceUnavailable)
			return
		}
		w.Write([]byte("ok"))
	}))
	defer srv.Close()

	body, err := httputil.RetryGet(context.Background(), srv.Client(), srv.URL, 5, time.Millisecond)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if body != "ok" {
		t.Errorf("want ok, got %q", body)
	}
	if calls != 3 {
		t.Errorf("want 3 calls, got %d", calls)
	}
}

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

	_, err := httputil.RetryGet(context.Background(), srv.Client(), srv.URL, 3, time.Millisecond)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

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

	ctx, cancel := context.WithCancel(context.Background())

	// Cancel after first failed attempt hits the backoff wait.
	go func() { time.Sleep(5 * time.Millisecond); cancel() }()

	_, err := httputil.RetryGet(ctx, srv.Client(), srv.URL, 10, 500*time.Millisecond)
	if err == nil {
		t.Fatal("expected context cancellation error")
	}
}

func TestRetryGet_NoRetryOn4xx(t *testing.T) {
	calls := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		calls++
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	_, err := httputil.RetryGet(context.Background(), srv.Client(), srv.URL, 5, time.Millisecond)
	if err == nil {
		t.Fatal("expected error for 404")
	}
	// 4xx is NOT retried — should be exactly 1 call.
	if calls != 1 {
		t.Errorf("want 1 call for 4xx, got %d", calls)
	}
}

// ── WriteJSON ─────────────────────────────────────────────────────────────────

func TestWriteJSON_SetsHeadersAndStatus(t *testing.T) {
	rr := httptest.NewRecorder()
	httputil.WriteJSON(rr, http.StatusCreated, map[string]string{"key": "val"})

	if rr.Code != http.StatusCreated {
		t.Errorf("status: want 201, got %d", rr.Code)
	}
	if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
		t.Errorf("Content-Type: want application/json, got %q", ct)
	}
	var got map[string]string
	if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if got["key"] != "val" {
		t.Errorf("body key: want val, got %q", got["key"])
	}
}

// ── WriteError ────────────────────────────────────────────────────────────────

func TestWriteError_Format(t *testing.T) {
	rr := httptest.NewRecorder()
	httputil.WriteError(rr, http.StatusBadRequest, "bad input")

	if rr.Code != http.StatusBadRequest {
		t.Errorf("status: want 400, got %d", rr.Code)
	}
	var got map[string]string
	json.NewDecoder(rr.Body).Decode(&got)
	if got["error"] != "bad input" {
		t.Errorf("error field: want bad input, got %q", got["error"])
	}
}

// ── DecodeJSON ────────────────────────────────────────────────────────────────

func TestDecodeJSON_HappyPath(t *testing.T) {
	body := `{"name":"test","value":42}`
	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	var payload struct {
		Name  string `json:"name"`
		Value int    `json:"value"`
	}
	if err := httputil.DecodeJSON(req, &payload); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if payload.Name != "test" || payload.Value != 42 {
		t.Errorf("unexpected payload: %+v", payload)
	}
}

func TestDecodeJSON_UnknownFieldReturnsError(t *testing.T) {
	body := `{"name":"test","unknown_field":"boom"}`
	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))

	var payload struct {
		Name string `json:"name"`
	}
	if err := httputil.DecodeJSON(req, &payload); err == nil {
		t.Fatal("expected error for unknown field, got nil")
	}
}

func TestDecodeJSON_BodyTooLarge(t *testing.T) {
	// Build a body > 1 MiB.
	big := bytes.Repeat([]byte("a"), 2<<20)
	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(big))

	var payload map[string]any
	if err := httputil.DecodeJSON(req, &payload); err == nil {
		t.Fatal("expected error for oversized body, got nil")
	}
}
