package apierr_test

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

	"github.com/wangjia/pangolin/server/internal/apierr"
)

// TestErrorInterface verifies that *Error implements the error interface.
func TestErrorInterface(t *testing.T) {
	var err error = apierr.ErrBadRequest
	if err == nil {
		t.Fatal("ErrBadRequest should not be nil")
	}
	if err.Error() == "" {
		t.Error("Error() returned empty string")
	}
}

// TestNew verifies that New constructs an *Error with the given fields.
func TestNew(t *testing.T) {
	e := apierr.New("TEST_CODE", "测试消息", "test message")
	if e.Code != "TEST_CODE" {
		t.Errorf("Code = %q, want %q", e.Code, "TEST_CODE")
	}
	if e.MessageZH != "测试消息" {
		t.Errorf("MessageZH = %q, want %q", e.MessageZH, "测试消息")
	}
	if e.MessageEn != "test message" {
		t.Errorf("MessageEn = %q, want %q", e.MessageEn, "test message")
	}
}

// TestStatusFor verifies HTTP status code mapping.
func TestStatusFor(t *testing.T) {
	cases := []struct {
		code       string
		wantStatus int
	}{
		{"UNAUTHORIZED", http.StatusUnauthorized},
		{"FORBIDDEN", http.StatusForbidden},
		{"NOT_FOUND", http.StatusNotFound},
		{"CONFLICT", http.StatusConflict},
		{"RATE_LIMITED", http.StatusTooManyRequests},
		{"ACCOUNT_LOCKED", http.StatusTooManyRequests},
		{"INTERNAL_ERROR", http.StatusInternalServerError},
		{"BAD_REQUEST", http.StatusBadRequest},
		{"INVALID_CODE", http.StatusBadRequest},
		{"CODE_NOT_FOUND", http.StatusBadRequest},
		{"UNKNOWN_CODE", http.StatusBadRequest},
	}
	for _, tc := range cases {
		e := &apierr.Error{Code: tc.code}
		got := apierr.StatusFor(e)
		if got != tc.wantStatus {
			t.Errorf("StatusFor({Code:%q}) = %d, want %d", tc.code, got, tc.wantStatus)
		}
	}
}

// TestWriteJSON verifies that WriteJSON sets the correct Content-Type,
// status code, and JSON body.
func TestWriteJSON(t *testing.T) {
	e := apierr.New("TEST", "中文", "english")

	w := httptest.NewRecorder()
	apierr.WriteJSON(w, http.StatusBadRequest, e)

	resp := w.Result()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusBadRequest)
	}
	ct := resp.Header.Get("Content-Type")
	if ct != "application/json; charset=utf-8" {
		t.Errorf("Content-Type = %q, want %q", ct, "application/json; charset=utf-8")
	}

	var got apierr.Error
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if got.Code != e.Code {
		t.Errorf("body.code = %q, want %q", got.Code, e.Code)
	}
	if got.MessageZH != e.MessageZH {
		t.Errorf("body.message_zh = %q, want %q", got.MessageZH, e.MessageZH)
	}
	if got.MessageEn != e.MessageEn {
		t.Errorf("body.message_en = %q, want %q", got.MessageEn, e.MessageEn)
	}
}

// TestMiddlewareCatchesAPIError verifies that the middleware intercepts
// a panic(*Error) and writes the correct JSON response.
func TestMiddlewareCatchesAPIError(t *testing.T) {
	panicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		panic(apierr.ErrUnauthorized)
	})

	handler := apierr.Middleware(panicHandler)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	resp := w.Result()
	if resp.StatusCode != http.StatusUnauthorized {
		t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
	}

	var got apierr.Error
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if got.Code != apierr.ErrUnauthorized.Code {
		t.Errorf("body.code = %q, want %q", got.Code, apierr.ErrUnauthorized.Code)
	}
}

// TestMiddlewareReRaisesNonAPIError verifies that the middleware re-raises
// panics that are not of type *Error.
func TestMiddlewareReRaisesNonAPIError(t *testing.T) {
	panicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		panic("unexpected string panic")
	})

	handler := apierr.Middleware(panicHandler)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	w := httptest.NewRecorder()

	defer func() {
		if rv := recover(); rv == nil {
			t.Error("expected panic to be re-raised, but it was not")
		}
	}()
	handler.ServeHTTP(w, req)
}

// TestMiddlewarePassesthrough verifies that the middleware is a no-op when
// the handler does not panic.
func TestMiddlewarePassesthrough(t *testing.T) {
	okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"ok":true}`))
	})

	handler := apierr.Middleware(okHandler)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
	}
}

// TestPredefinedErrors checks that all predefined errors have non-empty fields
// and comply with the desensitisation rule (no forbidden words in messages).
func TestPredefinedErrors(t *testing.T) {
	forbidden := []string{"VPN", "翻墙", "科学上网"}
	errors := []*apierr.Error{
		apierr.ErrBadRequest,
		apierr.ErrUnauthorized,
		apierr.ErrForbidden,
		apierr.ErrNotFound,
		apierr.ErrConflict,
		apierr.ErrRateLimited,
		apierr.ErrInternal,
		apierr.ErrInvalidCode,
		apierr.ErrCodeNotFound,
		apierr.ErrCodeRedeemed,
		apierr.ErrCodeVoid,
		apierr.ErrLocked,
		apierr.ErrWebhookSignature,
		apierr.ErrWebhookTimestamp,
		apierr.ErrWebhookReplay,
	}

	for _, e := range errors {
		if e.Code == "" {
			t.Errorf("error %+v has empty Code", e)
		}
		if e.MessageZH == "" {
			t.Errorf("error %q has empty MessageZH", e.Code)
		}
		if e.MessageEn == "" {
			t.Errorf("error %q has empty MessageEn", e.Code)
		}
		for _, f := range forbidden {
			if contains(e.MessageZH, f) || contains(e.MessageEn, f) {
				t.Errorf("error %q contains forbidden word %q", e.Code, f)
			}
		}
	}
}

func contains(s, sub string) bool {
	return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
}

func containsStr(s, sub string) bool {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}
