package handlers

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

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

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

const testUserID = "550e8400-e29b-41d4-a716-446655440000"
const otherUserID = "550e8400-e29b-41d4-a716-446655440001"

// mockPrefsRepository implements port.PreferencesRepository for handler testing.
type mockPrefsRepository struct {
	prefs map[string]*domain.UserPreferences
}

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

func newMockPrefsRepository() *mockPrefsRepository {
	return &mockPrefsRepository{
		prefs: make(map[string]*domain.UserPreferences),
	}
}

func (m *mockPrefsRepository) Get(ctx context.Context, userID string) (*domain.UserPreferences, error) {
	p, ok := m.prefs[userID]
	if !ok {
		return nil, nil
	}
	cp := *p
	return &cp, nil
}

func (m *mockPrefsRepository) Upsert(ctx context.Context, userID string, prefs map[string]any) (*domain.UserPreferences, error) {
	existing, ok := m.prefs[userID]
	if !ok {
		existing = &domain.UserPreferences{
			UserID:      userID,
			Preferences: map[string]any{},
		}
	}

	merged := make(map[string]any)
	for k, v := range existing.Preferences {
		merged[k] = v
	}
	for k, v := range prefs {
		merged[k] = v
	}

	result := &domain.UserPreferences{
		UserID:      userID,
		Preferences: merged,
	}
	m.prefs[userID] = result
	return result, nil
}

func newPrefsTestHandler() (*Preferences, *mockPrefsRepository) {
	repo := newMockPrefsRepository()
	svc := service.NewPreferencesService(repo, logging.Nop())
	handler := NewPreferences(svc, logging.Nop())
	return handler, repo
}

// withAuthUser adds an authenticated user to the request context.
func withAuthUser(r *http.Request, userID string) *http.Request {
	ctx := auth.SetUser(r.Context(), &auth.User{ID: userID})
	return r.WithContext(ctx)
}

func TestPreferences_Get(t *testing.T) {
	tests := []struct {
		name       string
		userID     string
		authUserID string
		seedPrefs  map[string]any
		wantStatus int
		wantData   bool
	}{
		{
			name:       "returns 200 with preferences for existing user",
			userID:     testUserID,
			authUserID: testUserID,
			seedPrefs:  map[string]any{"theme": "dark", "language": "en"},
			wantStatus: http.StatusOK,
			wantData:   true,
		},
		{
			name:       "returns 200 with empty preferences for new user",
			userID:     testUserID,
			authUserID: testUserID,
			seedPrefs:  nil,
			wantStatus: http.StatusOK,
			wantData:   true,
		},
		{
			name:       "returns 400 for invalid UUID",
			userID:     "not-a-uuid",
			authUserID: testUserID,
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "returns 403 for ownership mismatch",
			userID:     testUserID,
			authUserID: otherUserID,
			wantStatus: http.StatusForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler, repo := newPrefsTestHandler()

			if tt.seedPrefs != nil {
				repo.prefs[tt.userID] = &domain.UserPreferences{
					UserID:      tt.userID,
					Preferences: tt.seedPrefs,
				}
			}

			r := chi.NewRouter()
			r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get))

			req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+tt.userID, nil)
			req = withAuthUser(req, tt.authUserID)
			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())
			}

			if tt.wantData {
				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' object in response")
				}
				if _, ok := resp["meta"]; !ok {
					t.Fatal("expected 'meta' field in response")
				}
				if data["user_id"] != tt.userID {
					t.Errorf("expected user_id %s, got %v", tt.userID, data["user_id"])
				}
				prefs, ok := data["preferences"].(map[string]any)
				if !ok {
					t.Fatal("expected 'preferences' map in data")
				}
				if tt.seedPrefs == nil && len(prefs) != 0 {
					t.Errorf("expected empty preferences, got %v", prefs)
				}
				if tt.seedPrefs != nil {
					for k, v := range tt.seedPrefs {
						if prefs[k] != v {
							t.Errorf("expected preferences[%s] = %v, got %v", k, v, prefs[k])
						}
					}
				}
			}
		})
	}
}

func TestPreferences_Update(t *testing.T) {
	tests := []struct {
		name       string
		userID     string
		authUserID string
		body       any
		wantStatus int
		wantData   bool
	}{
		{
			name:       "returns 200 with merged preferences on success",
			userID:     testUserID,
			authUserID: testUserID,
			body:       map[string]any{"preferences": map[string]any{"theme": "dark"}},
			wantStatus: http.StatusOK,
			wantData:   true,
		},
		{
			name:       "returns 400 for unknown preference keys",
			userID:     testUserID,
			authUserID: testUserID,
			body:       map[string]any{"preferences": map[string]any{"unknown": "value"}},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "returns 400 for invalid preference values",
			userID:     testUserID,
			authUserID: testUserID,
			body:       map[string]any{"preferences": map[string]any{"theme": "blue"}},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "returns 400 for missing preferences field",
			userID:     testUserID,
			authUserID: testUserID,
			body:       map[string]any{},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "returns 400 for invalid UUID",
			userID:     "not-a-uuid",
			authUserID: testUserID,
			body:       map[string]any{"preferences": map[string]any{"theme": "dark"}},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:       "returns 403 for ownership mismatch",
			userID:     testUserID,
			authUserID: otherUserID,
			body:       map[string]any{"preferences": map[string]any{"theme": "dark"}},
			wantStatus: http.StatusForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler, _ := newPrefsTestHandler()

			r := chi.NewRouter()
			r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update))

			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.userID, bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			req = withAuthUser(req, tt.authUserID)
			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())
			}

			if tt.wantData {
				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' object in response")
				}
				if _, ok := resp["meta"]; !ok {
					t.Fatal("expected 'meta' field in response")
				}
				if data["user_id"] != tt.userID {
					t.Errorf("expected user_id %s, got %v", tt.userID, data["user_id"])
				}
				prefs, ok := data["preferences"].(map[string]any)
				if !ok {
					t.Fatal("expected 'preferences' map in data")
				}
				if prefs["theme"] != "dark" {
					t.Errorf("expected theme 'dark', got %v", prefs["theme"])
				}
			}
		})
	}
}
