package notify

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

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
)

type prefsFakeResolver struct{ uid uuid.UUID }

func (f *prefsFakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) {
	if token == "" {
		return uuid.Nil, false, nil
	}
	return f.uid, true, nil
}

// reverseEncryptor 是测试用 Encryptor：取反作为 "加密"，与 reverseDecryptor 配对。
type reverseEncryptor struct{}

func (reverseEncryptor) Encrypt(pt []byte) ([]byte, error) { return reverseBytes(pt), nil }

func mountPrefs(t *testing.T, uid uuid.UUID, prefs PrefsRepository) http.Handler {
	t.Helper()
	r := chi.NewRouter()
	NewHandler(&noopRepo{}, &prefsFakeResolver{uid: uid}).
		WithPrefs(prefs, reverseEncryptor{}).Mount(r)
	return r
}

// noopRepo 满足 inbox Repository（prefs 测试不触达 inbox 端点）。
type noopRepo struct{}

func (noopRepo) Insert(context.Context, Message) error { return nil }
func (noopRepo) List(context.Context, uuid.UUID, bool, int) ([]Message, error) {
	return nil, nil
}
func (noopRepo) CountUnread(context.Context, uuid.UUID) (int, error)  { return 0, nil }
func (noopRepo) MarkRead(context.Context, uuid.UUID, uuid.UUID) error { return nil }
func (noopRepo) MarkAllRead(context.Context, uuid.UUID) error         { return nil }

func TestPrefsHandler_GetReturnsDefaultsForNewUser(t *testing.T) {
	uid := uuid.New()
	h := mountPrefs(t, uid, newFakePrefs())
	req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil)
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var dto prefsDTO
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &dto))
	require.False(t, dto.EmailEnabled)
	require.False(t, dto.IMEnabled)
	require.False(t, dto.IMWebhookURLSet)
	require.Equal(t, "warning", dto.MinSeverity)
}

func TestPrefsHandler_GetRequiresAuth(t *testing.T) {
	h := mountPrefs(t, uuid.New(), newFakePrefs())
	req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil) // no token
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	require.Equal(t, http.StatusUnauthorized, rec.Code)
}

func TestPrefsHandler_PutPersistsAndEncryptsWebhook(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	h := mountPrefs(t, uid, prefs)

	body, _ := json.Marshal(prefsUpdateReq{
		EmailEnabled: boolPtr(true),
		IMEnabled:    boolPtr(true),
		IMWebhookURL: strPtr("https://hooks.slack.test/xyz"),
		MinSeverity:  strPtr("error"),
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var dto prefsDTO
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &dto))
	require.True(t, dto.EmailEnabled)
	require.True(t, dto.IMEnabled)
	require.True(t, dto.IMWebhookURLSet)
	require.Equal(t, "error", dto.MinSeverity)

	// 落库的 webhook 应为密文（取反），解回应得回原 URL；明文绝不直接存。
	saved, err := prefs.Get(context.Background(), uid)
	require.NoError(t, err)
	require.NotEmpty(t, saved.ImWebhookURLEnc)
	require.NotEqual(t, []byte("https://hooks.slack.test/xyz"), saved.ImWebhookURLEnc)
	require.Equal(t, "https://hooks.slack.test/xyz", string(reverseBytes(saved.ImWebhookURLEnc)))
}

func TestPrefsHandler_PutPartialUpdatePreservesWebhook(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(Prefs{UserID: uid, IMEnabled: true, MinSeverity: SeverityWarning,
		ImWebhookURLEnc: reverseBytes([]byte("https://existing"))})
	h := mountPrefs(t, uid, prefs)

	// 仅改 email_enabled，省略 im_webhook_url -> 保留原密文。
	body, _ := json.Marshal(prefsUpdateReq{EmailEnabled: boolPtr(true)})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	saved, _ := prefs.Get(context.Background(), uid)
	require.Equal(t, "https://existing", string(reverseBytes(saved.ImWebhookURLEnc)))
	require.True(t, saved.EmailEnabled)
}

func TestPrefsHandler_PutEmptyWebhookClearsIt(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(Prefs{UserID: uid, IMEnabled: true, MinSeverity: SeverityWarning,
		ImWebhookURLEnc: reverseBytes([]byte("https://existing"))})
	h := mountPrefs(t, uid, prefs)

	body, _ := json.Marshal(prefsUpdateReq{IMWebhookURL: strPtr("")})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	saved, _ := prefs.Get(context.Background(), uid)
	require.Empty(t, saved.ImWebhookURLEnc)
}

func TestPrefsHandler_PutRejectsBadSeverity(t *testing.T) {
	uid := uuid.New()
	h := mountPrefs(t, uid, newFakePrefs())
	body, _ := json.Marshal(prefsUpdateReq{MinSeverity: strPtr("catastrophic")})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	require.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestPrefsHandler_NotMountedWithoutEncryptor(t *testing.T) {
	uid := uuid.New()
	r := chi.NewRouter()
	// WithPrefs 未调用 -> /prefs 不应被挂载。
	NewHandler(&noopRepo{}, &prefsFakeResolver{uid: uid}).Mount(r)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil)
	req.Header.Set("Authorization", "Bearer tok")
	rec := httptest.NewRecorder()
	r.ServeHTTP(rec, req)
	require.Equal(t, http.StatusNotFound, rec.Code)
}

func boolPtr(b bool) *bool    { return &b }
func strPtr(s string) *string { return &s }
