package server

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/uptrace/bunrouter"
	"go.uber.org/zap"
)

// TestRecoverMiddlewarePanicBeforeResponse verifies a panic in a handler that
// hasn't written anything yet is converted into a 500 JSON error instead of
// crashing the request goroutine (which would otherwise surface to the client
// as a connection reset).
func TestRecoverMiddlewarePanicBeforeResponse(t *testing.T) {
	mw := recoverMiddleware(zap.NewNop())
	handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
		panic("boom")
	})

	req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()

	err := handler(w, bunrouter.Request{Request: req})
	if err != nil {
		t.Fatalf("expected recoverMiddleware to swallow the panic and return nil, got %v", err)
	}
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("expected status 500, got %d", w.Code)
	}

	var body apiErrorEnvelope
	if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
		t.Fatalf("expected a valid JSON error body, got decode error: %v (body=%q)", err, w.Body.String())
	}
	if body.Error.Type != "internal_error" {
		t.Fatalf("expected error type internal_error, got %q", body.Error.Type)
	}
}

// TestRecoverMiddlewarePanicAfterResponse verifies a panic that happens after
// the response has already been committed (e.g. mid-stream) doesn't attempt a
// second WriteHeader — it just surfaces as an error for the logger.
func TestRecoverMiddlewarePanicAfterResponse(t *testing.T) {
	mw := recoverMiddleware(zap.NewNop())
	handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("partial"))
		panic("boom mid-stream")
	})

	req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/chat/completions", nil)
	w := httptest.NewRecorder()

	err := handler(w, bunrouter.Request{Request: req})
	if err == nil {
		t.Fatal("expected an error to be returned once the response was already committed")
	}
	if w.Code != http.StatusOK {
		t.Fatalf("expected the original 200 to stand, got %d", w.Code)
	}
	if w.Body.String() != "partial" {
		t.Fatalf("expected body to be left as-is, got %q", w.Body.String())
	}
}

// TestRecoverMiddlewareNoPanic verifies the happy path is unaffected.
func TestRecoverMiddlewareNoPanic(t *testing.T) {
	mw := recoverMiddleware(zap.NewNop())
	handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
		return nil
	})

	req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()

	if err := handler(w, bunrouter.Request{Request: req}); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if w.Code != http.StatusOK || w.Body.String() != "ok" {
		t.Fatalf("unexpected response: status=%d body=%q", w.Code, w.Body.String())
	}
}
