package handlers

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

	"github.com/breakpilot/edu-search-service/internal/orchestrator"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

func init() {
	gin.SetMode(gin.TestMode)
}

// MockRepository implements orchestrator.Repository for testing
type MockRepository struct {
	items        []orchestrator.CrawlQueueItem
	failOnAdd    bool
	failOnUpdate bool
}

func NewMockRepository() *MockRepository {
	return &MockRepository{
		items: make([]orchestrator.CrawlQueueItem, 0),
	}
}

func (m *MockRepository) GetQueueItems(ctx context.Context) ([]orchestrator.CrawlQueueItem, error) {
	return m.items, nil
}

func (m *MockRepository) GetNextInQueue(ctx context.Context) (*orchestrator.CrawlQueueItem, error) {
	for i := range m.items {
		if m.items[i].CurrentPhase != orchestrator.PhaseCompleted &&
			m.items[i].CurrentPhase != orchestrator.PhaseFailed &&
			m.items[i].CurrentPhase != orchestrator.PhasePaused {
			return &m.items[i], nil
		}
	}
	return nil, nil
}

func (m *MockRepository) AddToQueue(ctx context.Context, universityID uuid.UUID, priority int, initiatedBy string) (*orchestrator.CrawlQueueItem, error) {
	if m.failOnAdd {
		return nil, context.DeadlineExceeded
	}

	position := len(m.items) + 1
	item := orchestrator.CrawlQueueItem{
		ID:            uuid.New(),
		UniversityID:  universityID,
		QueuePosition: &position,
		Priority:      priority,
		CurrentPhase:  orchestrator.PhasePending,
		CreatedAt:     time.Now(),
		UpdatedAt:     time.Now(),
	}
	m.items = append(m.items, item)
	return &item, nil
}

func (m *MockRepository) RemoveFromQueue(ctx context.Context, universityID uuid.UUID) error {
	for i, item := range m.items {
		if item.UniversityID == universityID {
			m.items = append(m.items[:i], m.items[i+1:]...)
			return nil
		}
	}
	return nil
}

func (m *MockRepository) UpdateQueueItem(ctx context.Context, item *orchestrator.CrawlQueueItem) error {
	if m.failOnUpdate {
		return context.DeadlineExceeded
	}
	for i, existing := range m.items {
		if existing.UniversityID == item.UniversityID {
			m.items[i] = *item
			return nil
		}
	}
	return nil
}

func (m *MockRepository) PauseQueueItem(ctx context.Context, universityID uuid.UUID) error {
	for i, item := range m.items {
		if item.UniversityID == universityID {
			m.items[i].CurrentPhase = orchestrator.PhasePaused
			return nil
		}
	}
	return nil
}

func (m *MockRepository) ResumeQueueItem(ctx context.Context, universityID uuid.UUID) error {
	for i, item := range m.items {
		if item.UniversityID == universityID && m.items[i].CurrentPhase == orchestrator.PhasePaused {
			m.items[i].CurrentPhase = orchestrator.PhasePending
			return nil
		}
	}
	return nil
}

func (m *MockRepository) CompletePhase(ctx context.Context, universityID uuid.UUID, phase orchestrator.CrawlPhase, count int) error {
	return nil
}

func (m *MockRepository) FailPhase(ctx context.Context, universityID uuid.UUID, phase orchestrator.CrawlPhase, errMsg string) error {
	return nil
}

func (m *MockRepository) GetCompletedTodayCount(ctx context.Context) (int, error) {
	count := 0
	today := time.Now().Truncate(24 * time.Hour)
	for _, item := range m.items {
		if item.CurrentPhase == orchestrator.PhaseCompleted &&
			item.CompletedAt != nil &&
			item.CompletedAt.After(today) {
			count++
		}
	}
	return count, nil
}

func (m *MockRepository) GetTotalProcessedCount(ctx context.Context) (int, error) {
	count := 0
	for _, item := range m.items {
		if item.CurrentPhase == orchestrator.PhaseCompleted {
			count++
		}
	}
	return count, nil
}

// MockStaffCrawler implements orchestrator.StaffCrawlerInterface
type MockStaffCrawler struct{}

func (m *MockStaffCrawler) DiscoverSampleProfessor(ctx context.Context, universityID uuid.UUID) (*orchestrator.CrawlProgress, error) {
	return &orchestrator.CrawlProgress{
		Phase:      orchestrator.PhaseDiscovery,
		ItemsFound: 1,
	}, nil
}

func (m *MockStaffCrawler) CrawlProfessors(ctx context.Context, universityID uuid.UUID) (*orchestrator.CrawlProgress, error) {
	return &orchestrator.CrawlProgress{
		Phase:      orchestrator.PhaseProfessors,
		ItemsFound: 10,
	}, nil
}

func (m *MockStaffCrawler) CrawlAllStaff(ctx context.Context, universityID uuid.UUID) (*orchestrator.CrawlProgress, error) {
	return &orchestrator.CrawlProgress{
		Phase:      orchestrator.PhaseAllStaff,
		ItemsFound: 50,
	}, nil
}

// MockPubCrawler implements orchestrator.PublicationCrawlerInterface
type MockPubCrawler struct{}

func (m *MockPubCrawler) CrawlPublicationsForUniversity(ctx context.Context, universityID uuid.UUID) (*orchestrator.CrawlProgress, error) {
	return &orchestrator.CrawlProgress{
		Phase:      orchestrator.PhasePublications,
		ItemsFound: 100,
	}, nil
}

// setupOrchestratorTestRouter creates a test router with orchestrator handler
func setupOrchestratorTestRouter(orch *orchestrator.Orchestrator, repo orchestrator.Repository, apiKey string) *gin.Engine {
	router := gin.New()

	handler := NewOrchestratorHandler(orch, repo)

	v1 := router.Group("/v1")
	v1.Use(AuthMiddleware(apiKey))
	SetupOrchestratorRoutes(v1, handler)

	return router
}

func TestOrchestratorGetStatus(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("GET", "/v1/crawl/status", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	var status orchestrator.OrchestratorStatus
	if err := json.Unmarshal(w.Body.Bytes(), &status); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if status.IsRunning != false {
		t.Error("Expected orchestrator to not be running initially")
	}
}

func TestOrchestratorGetQueue(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("GET", "/v1/crawl/queue", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	var response struct {
		Queue []orchestrator.CrawlQueueItem `json:"queue"`
		Count int                           `json:"count"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if response.Count != 0 {
		t.Errorf("Expected empty queue, got %d items", response.Count)
	}
}

func TestOrchestratorAddToQueue(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	universityID := uuid.New()
	reqBody := AddToQueueRequest{
		UniversityID: universityID.String(),
		Priority:     7,
		InitiatedBy:  "test-user",
	}

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/v1/crawl/queue", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusCreated {
		t.Errorf("Expected status 201, got %d: %s", w.Code, w.Body.String())
	}

	var item orchestrator.CrawlQueueItem
	if err := json.Unmarshal(w.Body.Bytes(), &item); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if item.UniversityID != universityID {
		t.Errorf("Expected universityID %s, got %s", universityID, item.UniversityID)
	}
	if item.Priority != 7 {
		t.Errorf("Expected priority 7, got %d", item.Priority)
	}
}

func TestOrchestratorAddToQueue_InvalidUUID(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	reqBody := map[string]interface{}{
		"university_id": "not-a-valid-uuid",
		"priority":      5,
	}

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/v1/crawl/queue", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestOrchestratorAddToQueue_MissingUniversityID(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	reqBody := map[string]interface{}{
		"priority": 5,
	}

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/v1/crawl/queue", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestOrchestratorRemoveFromQueue(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	// Add an item first
	universityID := uuid.New()
	repo.AddToQueue(context.Background(), universityID, 5, "test")

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("DELETE", "/v1/crawl/queue/"+universityID.String(), nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify it was removed
	items, _ := repo.GetQueueItems(context.Background())
	if len(items) != 0 {
		t.Errorf("Expected queue to be empty, got %d items", len(items))
	}
}

func TestOrchestratorRemoveFromQueue_InvalidUUID(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("DELETE", "/v1/crawl/queue/invalid-uuid", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestOrchestratorStartStop(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	// Start orchestrator
	req, _ := http.NewRequest("POST", "/v1/crawl/start", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 on start, got %d: %s", w.Code, w.Body.String())
	}

	// Try to start again (should fail)
	req, _ = http.NewRequest("POST", "/v1/crawl/start", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusConflict {
		t.Errorf("Expected status 409 on duplicate start, got %d", w.Code)
	}

	// Stop orchestrator
	req, _ = http.NewRequest("POST", "/v1/crawl/stop", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 on stop, got %d: %s", w.Code, w.Body.String())
	}

	// Try to stop again (should fail)
	req, _ = http.NewRequest("POST", "/v1/crawl/stop", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusConflict {
		t.Errorf("Expected status 409 on duplicate stop, got %d", w.Code)
	}
}

func TestOrchestratorPauseResume(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	// Add an item first
	universityID := uuid.New()
	repo.AddToQueue(context.Background(), universityID, 5, "test")

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	// Pause university
	req, _ := http.NewRequest("POST", "/v1/crawl/queue/"+universityID.String()+"/pause", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 on pause, got %d: %s", w.Code, w.Body.String())
	}

	// Verify it's paused
	items, _ := repo.GetQueueItems(context.Background())
	if len(items) != 1 || items[0].CurrentPhase != orchestrator.PhasePaused {
		t.Errorf("Expected item to be paused, got phase %s", items[0].CurrentPhase)
	}

	// Resume university
	req, _ = http.NewRequest("POST", "/v1/crawl/queue/"+universityID.String()+"/resume", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200 on resume, got %d: %s", w.Code, w.Body.String())
	}

	// Verify it's resumed
	items, _ = repo.GetQueueItems(context.Background())
	if len(items) != 1 || items[0].CurrentPhase == orchestrator.PhasePaused {
		t.Errorf("Expected item to not be paused, got phase %s", items[0].CurrentPhase)
	}
}

func TestOrchestratorPause_InvalidUUID(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("POST", "/v1/crawl/queue/invalid-uuid/pause", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestOrchestratorNoAuth(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	// Request without auth
	req, _ := http.NewRequest("GET", "/v1/crawl/status", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status 401, got %d", w.Code)
	}
}

func TestOrchestratorDefaultPriority(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	// Add without priority (should default to 5)
	universityID := uuid.New()
	reqBody := AddToQueueRequest{
		UniversityID: universityID.String(),
		// Priority and InitiatedBy omitted
	}

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/v1/crawl/queue", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusCreated {
		t.Errorf("Expected status 201, got %d: %s", w.Code, w.Body.String())
	}

	var item orchestrator.CrawlQueueItem
	if err := json.Unmarshal(w.Body.Bytes(), &item); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if item.Priority != 5 {
		t.Errorf("Expected default priority 5, got %d", item.Priority)
	}
}

// TestOrchestratorQueueWithNullableFields tests that queue items with NULL values
// for optional fields (UniversityShort, LastError) are handled correctly.
// This tests the COALESCE fix in repository.go that prevents NULL scan errors.
func TestOrchestratorQueueWithNullableFields(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	// Add item with empty optional fields (simulates NULL from DB)
	universityID := uuid.New()
	item := orchestrator.CrawlQueueItem{
		ID:              uuid.New(),
		UniversityID:    universityID,
		UniversityName:  "Test Universität",
		UniversityShort: "", // Empty string (COALESCE converts NULL to '')
		CurrentPhase:    orchestrator.PhasePending,
		LastError:       "", // Empty string (COALESCE converts NULL to '')
		CreatedAt:       time.Now(),
		UpdatedAt:       time.Now(),
	}
	position := 1
	item.QueuePosition = &position
	repo.items = append(repo.items, item)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("GET", "/v1/crawl/queue", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	var response struct {
		Queue []orchestrator.CrawlQueueItem `json:"queue"`
		Count int                           `json:"count"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if response.Count != 1 {
		t.Errorf("Expected 1 item in queue, got %d", response.Count)
	}

	// Verify empty strings are preserved (not NULL)
	if response.Queue[0].UniversityShort != "" {
		t.Errorf("Expected empty UniversityShort, got %q", response.Queue[0].UniversityShort)
	}
	if response.Queue[0].LastError != "" {
		t.Errorf("Expected empty LastError, got %q", response.Queue[0].LastError)
	}
}

// TestOrchestratorQueueWithLastError tests that queue items with an error message
// are correctly serialized and returned.
func TestOrchestratorQueueWithLastError(t *testing.T) {
	repo := NewMockRepository()
	staffCrawler := &MockStaffCrawler{}
	pubCrawler := &MockPubCrawler{}
	orch := orchestrator.NewOrchestrator(repo, staffCrawler, pubCrawler)

	// Add item with an error
	universityID := uuid.New()
	item := orchestrator.CrawlQueueItem{
		ID:              uuid.New(),
		UniversityID:    universityID,
		UniversityName:  "Test Universität mit Fehler",
		UniversityShort: "TUmF",
		CurrentPhase:    orchestrator.PhaseFailed,
		LastError:       "connection timeout after 30s",
		RetryCount:      3,
		MaxRetries:      3,
		CreatedAt:       time.Now(),
		UpdatedAt:       time.Now(),
	}
	position := 1
	item.QueuePosition = &position
	repo.items = append(repo.items, item)

	router := setupOrchestratorTestRouter(orch, repo, "test-key")

	req, _ := http.NewRequest("GET", "/v1/crawl/queue", nil)
	req.Header.Set("Authorization", "Bearer test-key")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	var response struct {
		Queue []orchestrator.CrawlQueueItem `json:"queue"`
		Count int                           `json:"count"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if response.Count != 1 {
		t.Errorf("Expected 1 item in queue, got %d", response.Count)
	}

	// Verify error message is preserved
	if response.Queue[0].LastError != "connection timeout after 30s" {
		t.Errorf("Expected LastError to be 'connection timeout after 30s', got %q", response.Queue[0].LastError)
	}
	if response.Queue[0].UniversityShort != "TUmF" {
		t.Errorf("Expected UniversityShort 'TUmF', got %q", response.Queue[0].UniversityShort)
	}
}
