package handlers

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

	"github.com/go-chi/chi/v5"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
	"github.com/orchard9/rdev/internal/service"
)

// mockSessionRepository implements port.SessionRepository for testing.
type mockSessionRepository struct {
	sessions map[string]*domain.Session
	err      error
}

func newMockSessionRepository() *mockSessionRepository {
	return &mockSessionRepository{sessions: make(map[string]*domain.Session)}
}

func (m *mockSessionRepository) Create(_ context.Context, session *domain.Session) error {
	if m.err != nil {
		return m.err
	}
	// Check unique constraint: only one active per project.
	for _, s := range m.sessions {
		if s.ProjectID == session.ProjectID && s.Status == domain.SessionStatusActive {
			return domain.ErrSessionExists
		}
	}
	session.ID = domain.SessionID("test-session-id")
	m.sessions[string(session.ID)] = session
	return nil
}

func (m *mockSessionRepository) Get(_ context.Context, id domain.SessionID) (*domain.Session, error) {
	if m.err != nil {
		return nil, m.err
	}
	s, ok := m.sessions[string(id)]
	if !ok {
		return nil, domain.ErrSessionNotFound
	}
	return s, nil
}

func (m *mockSessionRepository) GetActiveByProject(_ context.Context, projectID domain.ProjectID) (*domain.Session, error) {
	if m.err != nil {
		return nil, m.err
	}
	for _, s := range m.sessions {
		if s.ProjectID == projectID && s.Status == domain.SessionStatusActive {
			return s, nil
		}
	}
	return nil, domain.ErrSessionNotFound
}

func (m *mockSessionRepository) ListByProject(_ context.Context, projectID domain.ProjectID) ([]*domain.Session, error) {
	if m.err != nil {
		return nil, m.err
	}
	var result []*domain.Session
	for _, s := range m.sessions {
		if s.ProjectID == projectID {
			result = append(result, s)
		}
	}
	return result, nil
}

func (m *mockSessionRepository) SetEnded(_ context.Context, id domain.SessionID) error {
	if m.err != nil {
		return m.err
	}
	s, ok := m.sessions[string(id)]
	if !ok || s.Status != domain.SessionStatusActive {
		return domain.ErrSessionNotActive
	}
	s.Status = domain.SessionStatusEnded
	now := time.Now()
	s.EndedAt = &now
	return nil
}

func (m *mockSessionRepository) CleanupExpired(_ context.Context) ([]*domain.Session, error) {
	if m.err != nil {
		return nil, m.err
	}
	var expired []*domain.Session
	for _, s := range m.sessions {
		if s.Status == domain.SessionStatusActive && time.Now().After(s.ExpiresAt) {
			s.Status = domain.SessionStatusExpired
			now := time.Now()
			s.EndedAt = &now
			expired = append(expired, s)
		}
	}
	return expired, nil
}

// mockCheckoutRepository implements port.CheckoutRepository for testing sessions.
type mockCheckoutRepository struct {
	checkouts map[string]*domain.Checkout
	err       error
}

func newMockCheckoutRepository() *mockCheckoutRepository {
	return &mockCheckoutRepository{checkouts: make(map[string]*domain.Checkout)}
}

func (m *mockCheckoutRepository) Create(_ context.Context, c *domain.Checkout) error {
	if m.err != nil {
		return m.err
	}
	c.ID = domain.CheckoutID("test-checkout-id")
	m.checkouts[string(c.ID)] = c
	return nil
}

func (m *mockCheckoutRepository) Get(_ context.Context, id domain.CheckoutID) (*domain.Checkout, error) {
	if m.err != nil {
		return nil, m.err
	}
	c, ok := m.checkouts[string(id)]
	if !ok {
		return nil, domain.ErrCheckoutNotFound
	}
	return c, nil
}

func (m *mockCheckoutRepository) GetByProjectBranch(_ context.Context, _ domain.ProjectID, _ string) (*domain.Checkout, error) {
	return nil, domain.ErrCheckoutNotFound
}

func (m *mockCheckoutRepository) List(_ context.Context, _ domain.CheckoutListOptions) ([]*domain.Checkout, error) {
	return nil, nil
}

func (m *mockCheckoutRepository) ListByProject(_ context.Context, _ domain.ProjectID) ([]*domain.Checkout, error) {
	return nil, nil
}

func (m *mockCheckoutRepository) UpdateStatus(_ context.Context, id domain.CheckoutID, status domain.CheckoutStatus) error {
	if c, ok := m.checkouts[string(id)]; ok {
		c.Status = status
	}
	return nil
}

func (m *mockCheckoutRepository) SetCheckedIn(_ context.Context, id domain.CheckoutID, _ string) error {
	if c, ok := m.checkouts[string(id)]; ok {
		c.Status = domain.CheckoutStatusCheckedIn
		now := time.Now()
		c.CheckedInAt = &now
	}
	return nil
}

func (m *mockCheckoutRepository) SetReviewTask(_ context.Context, _ domain.CheckoutID, _ string) error {
	return nil
}

func (m *mockCheckoutRepository) CleanupExpired(_ context.Context) ([]int64, error) {
	return nil, nil
}

// setupSessionTest creates a sessions handler with mock dependencies.
// It reuses mockProjectRepo from queue_test.go.
func setupSessionTest() (*SessionsHandler, *mockSessionRepository, *mockProjectRepo) {
	sessionRepo := newMockSessionRepository()
	checkoutRepo := newMockCheckoutRepository()
	projectRepo := newMockProjectRepo()
	gitRepo := newMockGitRepository()
	previewMgr := newMockPreviewManager()

	// Add a test project.
	projectRepo.projects["test-project"] = &domain.Project{
		ID:      "test-project",
		Name:    "test-project",
		PodName: "test-project-0",
		Status:  domain.ProjectStatusRunning,
	}

	checkoutService := service.NewCheckoutService(
		checkoutRepo,
		gitRepo,
		projectRepo,
		service.CheckoutServiceConfig{
			GiteaURL:      "https://git.threesix.ai",
			DefaultOwner:  "threesix",
			DefaultExpiry: 24 * time.Hour,
		},
	)

	sessionService := service.NewSessionService(
		sessionRepo,
		checkoutService,
		projectRepo,
		previewMgr,
		service.SessionServiceConfig{
			PreviewDomain: "preview.threesix.ai",
			DefaultExpiry: 24 * time.Hour,
		},
	)

	handler := NewSessionsHandler(sessionService)
	return handler, sessionRepo, projectRepo
}

func TestSessionsHandler_Create(t *testing.T) {
	handler, _, _ := setupSessionTest()

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	t.Run("create_session", func(t *testing.T) {
		body := `{"branch": "develop"}`
		req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sessions", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusCreated {
			t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
		}

		var resp map[string]any
		if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
			t.Fatalf("unmarshal: %v", err)
		}

		data, ok := resp["data"].(map[string]any)
		if !ok {
			t.Fatalf("expected data map, got %T", resp["data"])
		}

		if data["id"] == "" {
			t.Error("expected non-empty session id")
		}
		if data["preview_url"] == "" {
			t.Error("expected non-empty preview_url")
		}
		if data["auth_clone_url"] == "" {
			t.Error("expected auth_clone_url on creation")
		}
		if data["instructions"] == "" {
			t.Error("expected instructions on creation")
		}
		if data["pod_name"] != "test-project-0" {
			t.Errorf("expected pod_name=test-project-0, got %v", data["pod_name"])
		}
	})

	t.Run("create_session_no_branch", func(t *testing.T) {
		body := `{}`
		req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sessions", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusBadRequest {
			t.Errorf("got status %d, want %d", rec.Code, http.StatusBadRequest)
		}
	})

	t.Run("create_session_project_not_found", func(t *testing.T) {
		body := `{"branch": "develop"}`
		req := httptest.NewRequest(http.MethodPost, "/projects/nonexistent/sessions", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusNotFound {
			t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
		}
	})
}

func TestSessionsHandler_Get(t *testing.T) {
	handler, sessionRepo, _ := setupSessionTest()

	// Seed a session.
	session := &domain.Session{
		ID:          "session-abc",
		ProjectID:   "test-project",
		CheckoutID:  "checkout-abc",
		PodName:     "test-project-0",
		PreviewURL:  "https://abc.preview.threesix.ai",
		PreviewHost: "abc.preview.threesix.ai",
		CreatedBy:   "test",
		CreatedAt:   time.Now(),
		ExpiresAt:   time.Now().Add(24 * time.Hour),
		Status:      domain.SessionStatusActive,
	}
	sessionRepo.sessions["session-abc"] = session

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	t.Run("get_existing", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sessions/session-abc", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
		}
	})

	t.Run("get_wrong_project", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/other-project/sessions/session-abc", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusNotFound {
			t.Errorf("got status %d, want %d", rec.Code, http.StatusNotFound)
		}
	})

	t.Run("get_not_found", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sessions/nonexistent", nil)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusNotFound {
			t.Errorf("got status %d, want %d", rec.Code, http.StatusNotFound)
		}
	})
}

func TestSessionsHandler_List(t *testing.T) {
	handler, sessionRepo, _ := setupSessionTest()

	// Seed sessions.
	sessionRepo.sessions["session-1"] = &domain.Session{
		ID:          "session-1",
		ProjectID:   "test-project",
		CheckoutID:  "checkout-1",
		PodName:     "test-project-0",
		PreviewURL:  "https://s1.preview.threesix.ai",
		PreviewHost: "s1.preview.threesix.ai",
		CreatedBy:   "test",
		CreatedAt:   time.Now(),
		ExpiresAt:   time.Now().Add(24 * time.Hour),
		Status:      domain.SessionStatusActive,
	}
	sessionRepo.sessions["session-2"] = &domain.Session{
		ID:          "session-2",
		ProjectID:   "test-project",
		CheckoutID:  "checkout-2",
		PodName:     "test-project-0",
		PreviewURL:  "https://s2.preview.threesix.ai",
		PreviewHost: "s2.preview.threesix.ai",
		CreatedBy:   "test",
		CreatedAt:   time.Now().Add(-1 * time.Hour),
		ExpiresAt:   time.Now().Add(23 * time.Hour),
		Status:      domain.SessionStatusEnded,
	}

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sessions", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
	}

	var resp map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	data, ok := resp["data"].(map[string]any)
	if !ok {
		t.Fatalf("expected data map, got %T", resp["data"])
	}
	sessions, ok := data["sessions"].([]any)
	if !ok {
		t.Fatalf("expected sessions array, got %T", data["sessions"])
	}
	if len(sessions) != 2 {
		t.Errorf("got %d sessions, want 2", len(sessions))
	}
}

func TestSessionsHandler_Checkin(t *testing.T) {
	handler, sessionRepo, _ := setupSessionTest()

	// Seed an active session with a matching checkout.
	sessionRepo.sessions["session-end"] = &domain.Session{
		ID:          "session-end",
		ProjectID:   "test-project",
		CheckoutID:  "checkout-end",
		PodName:     "test-project-0",
		PreviewURL:  "https://end.preview.threesix.ai",
		PreviewHost: "end.preview.threesix.ai",
		CreatedBy:   "test",
		CreatedAt:   time.Now(),
		ExpiresAt:   time.Now().Add(24 * time.Hour),
		Status:      domain.SessionStatusActive,
	}

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	t.Run("checkin_session", func(t *testing.T) {
		// Checkout doesn't exist in the mock, but session service continues on checkout errors.
		body := `{"skip_review": true}`
		req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sessions/session-end/checkin", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
		}
	})

	t.Run("checkin_already_ended", func(t *testing.T) {
		body := `{}`
		req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sessions/session-end/checkin", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusBadRequest {
			t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
		}
	})
}

func TestWorkersHandler_PoolStatus(t *testing.T) {
	registry := newMockWorkerRegistry()
	queue := newMockWorkQueue()
	workerService := service.NewWorkerService(registry, queue)
	handler := NewWorkersHandler(workerService).WithWorkQueue(queue)

	registry.workers["w1"] = &domain.Worker{
		ID: "w1", Hostname: "h1", Status: domain.WorkerStatusIdle,
		RegisteredAt: time.Now(), LastHeartbeat: time.Now(),
	}
	registry.workers["w2"] = &domain.Worker{
		ID: "w2", Hostname: "h2", Status: domain.WorkerStatusBusy,
		RegisteredAt: time.Now(), LastHeartbeat: time.Now(),
	}

	// Add a pending task to the queue.
	queue.tasks["t1"] = &domain.WorkTask{ID: "t1", Status: domain.WorkTaskStatusPending}

	router := chi.NewRouter()
	router.Use(testAdminAuth)
	handler.Mount(router)

	req := httptest.NewRequest(http.MethodGet, "/workers/pool", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
	}

	var resp map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	data, ok := resp["data"].(map[string]any)
	if !ok {
		t.Fatalf("expected data map, got %T", resp["data"])
	}

	if int(data["total"].(float64)) != 2 {
		t.Errorf("total: got %v, want 2", data["total"])
	}
	if int(data["idle"].(float64)) != 1 {
		t.Errorf("idle: got %v, want 1", data["idle"])
	}
	if int(data["busy"].(float64)) != 1 {
		t.Errorf("busy: got %v, want 1", data["busy"])
	}
	if int(data["available"].(float64)) != 1 {
		t.Errorf("available: got %v, want 1", data["available"])
	}
	if int(data["queue_depth"].(float64)) != 1 {
		t.Errorf("queue_depth: got %v, want 1", data["queue_depth"])
	}
}

// Verify port.PreviewManager is implemented by the mock.
var _ port.PreviewManager = (*mockPreviewManager)(nil)
