package service

import (
	"context"
	"sync"
	"testing"

	"git.threesix.ai/jordan/slack5-1770529463/pkg/logging"
	"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/domain"
	"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/port"
)

// mockPreferenceRepository implements port.PreferenceRepository for testing.
type mockPreferenceRepository struct {
	mu    sync.RWMutex
	prefs map[string]map[string]string // userID -> key -> value
}

var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil)

func newMockPreferenceRepository() *mockPreferenceRepository {
	return &mockPreferenceRepository{
		prefs: make(map[string]map[string]string),
	}
}

func (m *mockPreferenceRepository) GetByUserID(ctx context.Context, userID string) ([]port.PreferenceRow, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	userPrefs, ok := m.prefs[userID]
	if !ok {
		return nil, nil
	}

	rows := make([]port.PreferenceRow, 0, len(userPrefs))
	for k, v := range userPrefs {
		rows = append(rows, port.PreferenceRow{
			UserID: userID,
			Key:    k,
			Value:  v,
		})
	}
	return rows, nil
}

func (m *mockPreferenceRepository) Upsert(ctx context.Context, userID string, key string, value string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.prefs[userID] == nil {
		m.prefs[userID] = make(map[string]string)
	}
	m.prefs[userID][key] = value
	return nil
}

func TestPreferenceService_GetPreferences(t *testing.T) {
	repo := newMockPreferenceRepository()
	svc := NewPreferenceService(repo, logging.Nop())
	ctx := context.Background()
	validUserID := "550e8400-e29b-41d4-a716-446655440000"

	t.Run("returns defaults when no preferences stored", func(t *testing.T) {
		result, err := svc.GetPreferences(ctx, validUserID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.UserID != validUserID {
			t.Errorf("expected user_id %s, got %s", validUserID, result.UserID)
		}
		if result.Preferences["theme"] != "system" {
			t.Errorf("expected default theme 'system', got %v", result.Preferences["theme"])
		}
		if result.Preferences["language"] != "en" {
			t.Errorf("expected default language 'en', got %v", result.Preferences["language"])
		}
		if result.Preferences["notifications_enabled"] != true {
			t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
		}
	})

	t.Run("merges stored values with defaults", func(t *testing.T) {
		// Store only theme
		_ = repo.Upsert(ctx, validUserID, "theme", "dark")

		result, err := svc.GetPreferences(ctx, validUserID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.Preferences["theme"] != "dark" {
			t.Errorf("expected theme 'dark', got %v", result.Preferences["theme"])
		}
		if result.Preferences["language"] != "en" {
			t.Errorf("expected default language 'en', got %v", result.Preferences["language"])
		}
		if result.Preferences["notifications_enabled"] != true {
			t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
		}
	})

	t.Run("returns all stored values when all keys present", func(t *testing.T) {
		userID := "660e8400-e29b-41d4-a716-446655440000"
		_ = repo.Upsert(ctx, userID, "theme", "light")
		_ = repo.Upsert(ctx, userID, "language", "fr")
		_ = repo.Upsert(ctx, userID, "notifications_enabled", "false")

		result, err := svc.GetPreferences(ctx, userID)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.Preferences["theme"] != "light" {
			t.Errorf("expected theme 'light', got %v", result.Preferences["theme"])
		}
		if result.Preferences["language"] != "fr" {
			t.Errorf("expected language 'fr', got %v", result.Preferences["language"])
		}
		if result.Preferences["notifications_enabled"] != false {
			t.Errorf("expected notifications_enabled false, got %v", result.Preferences["notifications_enabled"])
		}
	})

	t.Run("returns error for invalid user_id", func(t *testing.T) {
		_, err := svc.GetPreferences(ctx, "not-a-uuid")
		if err != domain.ErrInvalidUserID {
			t.Errorf("expected ErrInvalidUserID, got %v", err)
		}
	})
}

func TestPreferenceService_UpdatePreferences(t *testing.T) {
	repo := newMockPreferenceRepository()
	svc := NewPreferenceService(repo, logging.Nop())
	ctx := context.Background()
	validUserID := "550e8400-e29b-41d4-a716-446655440000"

	t.Run("updates preferences and returns full result", func(t *testing.T) {
		result, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
			"theme":    "dark",
			"language": "fr",
		})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.Preferences["theme"] != "dark" {
			t.Errorf("expected theme 'dark', got %v", result.Preferences["theme"])
		}
		if result.Preferences["language"] != "fr" {
			t.Errorf("expected language 'fr', got %v", result.Preferences["language"])
		}
		// notifications_enabled should be default
		if result.Preferences["notifications_enabled"] != true {
			t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
		}
	})

	t.Run("handles boolean input for notifications_enabled", func(t *testing.T) {
		result, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
			"notifications_enabled": false,
		})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.Preferences["notifications_enabled"] != false {
			t.Errorf("expected notifications_enabled false, got %v", result.Preferences["notifications_enabled"])
		}
	})

	t.Run("rejects unknown key", func(t *testing.T) {
		_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
			"font_size": "large",
		})
		if err == nil {
			t.Fatal("expected error for unknown key")
		}
		// Should wrap ErrUnknownPreferenceKey
		if !containsError(err, domain.ErrUnknownPreferenceKey) {
			t.Errorf("expected ErrUnknownPreferenceKey, got %v", err)
		}
	})

	t.Run("rejects invalid theme value", func(t *testing.T) {
		_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
			"theme": "neon",
		})
		if err == nil {
			t.Fatal("expected error for invalid theme")
		}
		if !containsError(err, domain.ErrInvalidPreferenceValue) {
			t.Errorf("expected ErrInvalidPreferenceValue, got %v", err)
		}
	})

	t.Run("rejects invalid language value", func(t *testing.T) {
		_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
			"language": "123",
		})
		if err == nil {
			t.Fatal("expected error for invalid language")
		}
		if !containsError(err, domain.ErrInvalidPreferenceValue) {
			t.Errorf("expected ErrInvalidPreferenceValue, got %v", err)
		}
	})

	t.Run("rejects invalid user_id", func(t *testing.T) {
		_, err := svc.UpdatePreferences(ctx, "bad-id", map[string]any{
			"theme": "dark",
		})
		if err != domain.ErrInvalidUserID {
			t.Errorf("expected ErrInvalidUserID, got %v", err)
		}
	})

	t.Run("is idempotent", func(t *testing.T) {
		userID := "770e8400-e29b-41d4-a716-446655440000"
		input := map[string]any{"theme": "dark"}

		result1, err := svc.UpdatePreferences(ctx, userID, input)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		result2, err := svc.UpdatePreferences(ctx, userID, input)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		if result1.Preferences["theme"] != result2.Preferences["theme"] {
			t.Errorf("expected idempotent result, got %v and %v", result1.Preferences["theme"], result2.Preferences["theme"])
		}
	})
}

// containsError checks if err wraps target using errors.Is-like behavior with fmt.Errorf wrapping.
func containsError(err, target error) bool {
	if err == nil {
		return false
	}
	for e := err; e != nil; {
		if e == target {
			return true
		}
		if e.Error() == target.Error() {
			return true
		}
		u, ok := e.(interface{ Unwrap() error })
		if !ok {
			break
		}
		e = u.Unwrap()
	}
	return false
}
