package handlers

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

	"github.com/go-chi/chi/v5"

	"git.threesix.ai/jordan/slack5-1770541397/pkg/auth"
	"git.threesix.ai/jordan/slack5-1770541397/pkg/httperror"
	"git.threesix.ai/jordan/slack5-1770541397/pkg/logging"
	"git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/domain"
	"git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/port"
	"git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/service"
)

// mockPreferencesRepository implements port.PreferencesRepository for handler tests.
type mockPreferencesRepository struct {
	mu    sync.RWMutex
	store map[domain.UserID]*domain.UserPreferences
}

var _ port.PreferencesRepository = (*mockPreferencesRepository)(nil)

func newMockPreferencesRepository() *mockPreferencesRepository {
	return &mockPreferencesRepository{
		store: make(map[domain.UserID]*domain.UserPreferences),
	}
}

func (m *mockPreferencesRepository) Get(ctx context.Context, userID domain.UserID) (*domain.UserPreferences, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	prefs, ok := m.store[userID]
	if !ok {
		return nil, domain.ErrPreferencesNotFound
	}
	copy := *prefs
	return &copy, nil
}

func (m *mockPreferencesRepository) Upsert(ctx context.Context, prefs *domain.UserPreferences) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	copy := *prefs
	m.store[prefs.UserID] = &copy
	return nil
}

func newTestPreferencesHandler() (*Preferences, *mockPreferencesRepository) {
	repo := newMockPreferencesRepository()
	svc := service.NewPreferencesService(repo, logging.Nop())
	handler := NewPreferences(svc, logging.Nop())
	return handler, repo
}

// withAuthUser creates an HTTP handler that injects an auth user into the context.
func withAuthUser(userID string, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := auth.SetUser(r.Context(), &auth.User{ID: userID})
		next.ServeHTTP(w, r.WithContext(ctx))
	}
}

func TestPreferences_Get(t *testing.T) {
	handler, repo := newTestPreferencesHandler()

	// Seed stored preferences for user-456
	repo.store["user-456"] = &domain.UserPreferences{
		UserID: "user-456",
		Preferences: domain.Preferences{
			Theme:    "dark",
			Language: "fr",
			Notifications: domain.NotificationPreferences{
				Email: false,
				Push:  true,
				SMS:   true,
			},
		},
	}

	tests := []struct {
		name       string
		pathUserID string
		authUserID string
		wantStatus int
		wantTheme  string
	}{
		{
			name:       "returns defaults for new user",
			pathUserID: "user-123",
			authUserID: "user-123",
			wantStatus: http.StatusOK,
			wantTheme:  "system",
		},
		{
			name:       "returns stored preferences",
			pathUserID: "user-456",
			authUserID: "user-456",
			wantStatus: http.StatusOK,
			wantTheme:  "dark",
		},
		{
			name:       "returns forbidden for different user",
			pathUserID: "user-456",
			authUserID: "user-other",
			wantStatus: http.StatusForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := chi.NewRouter()
			r.Get("/api/preferences-api/preferences/{user_id}", withAuthUser(tt.authUserID, func(w http.ResponseWriter, r *http.Request) {
				if err := handler.Get(w, r); err != nil {
					writeErrorStatus(w, err)
					return
				}
			}))

			req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+tt.pathUserID, nil)
			w := httptest.NewRecorder()
			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code)
			}

			if tt.wantStatus == http.StatusOK && tt.wantTheme != "" {
				var resp map[string]any
				if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
					t.Fatalf("failed to decode response: %v", err)
				}
				data, ok := resp["data"].(map[string]any)
				if !ok {
					t.Fatal("expected 'data' field in response")
				}
				prefs, ok := data["preferences"].(map[string]any)
				if !ok {
					t.Fatal("expected 'preferences' field in data")
				}
				if prefs["theme"] != tt.wantTheme {
					t.Errorf("expected theme '%s', got '%v'", tt.wantTheme, prefs["theme"])
				}
			}
		})
	}
}

func TestPreferences_Put(t *testing.T) {
	handler, _ := newTestPreferencesHandler()

	tests := []struct {
		name       string
		pathUserID string
		authUserID string
		body       any
		wantStatus int
	}{
		{
			name:       "sets preferences successfully",
			pathUserID: "user-123",
			authUserID: "user-123",
			body: PutPreferencesRequest{
				Preferences: PreferencesPayload{
					Theme:    "dark",
					Language: "en",
					Notifications: &NotificationPreferencesPayload{
						Email: true,
						Push:  false,
						SMS:   false,
					},
				},
			},
			wantStatus: http.StatusOK,
		},
		{
			name:       "returns forbidden for different user",
			pathUserID: "user-123",
			authUserID: "user-other",
			body: PutPreferencesRequest{
				Preferences: PreferencesPayload{
					Theme:    "dark",
					Language: "en",
				},
			},
			wantStatus: http.StatusForbidden,
		},
		{
			name:       "rejects invalid theme",
			pathUserID: "user-123",
			authUserID: "user-123",
			body: PutPreferencesRequest{
				Preferences: PreferencesPayload{
					Theme:    "neon",
					Language: "en",
				},
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "rejects language too long",
			pathUserID: "user-123",
			authUserID: "user-123",
			body: PutPreferencesRequest{
				Preferences: PreferencesPayload{
					Theme:    "dark",
					Language: "abcdefghijk",
				},
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "rejects empty body",
			pathUserID: "user-123",
			authUserID: "user-123",
			body:       nil,
			wantStatus: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := chi.NewRouter()
			r.Put("/api/preferences-api/preferences/{user_id}", withAuthUser(tt.authUserID, func(w http.ResponseWriter, r *http.Request) {
				if err := handler.Put(w, r); err != nil {
					writeErrorStatus(w, err)
					return
				}
			}))

			var body []byte
			if tt.body != nil {
				var err error
				body, err = json.Marshal(tt.body)
				if err != nil {
					t.Fatalf("failed to marshal body: %v", err)
				}
			}

			req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+tt.pathUserID, bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()
			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("expected status %d, got %d (body: %s)", tt.wantStatus, w.Code, w.Body.String())
			}
		})
	}
}

// writeErrorStatus writes an HTTP error status based on the error type.
func writeErrorStatus(w http.ResponseWriter, err error) {
	w.WriteHeader(httperror.StatusCode(err))
}
