package handlers

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

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

	"git.threesix.ai/jordan/persona-community-5/pkg/app"
	"git.threesix.ai/jordan/persona-community-5/pkg/logging"
	"git.threesix.ai/jordan/persona-community-5/pkg/queue"
	"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/adapter/memory"
	"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
	"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/service"
)

// mockProducer implements queue.Producer for testing.
type mockProducer struct {
	jobs []mockJob
}

type mockJob struct {
	jobType string
	payload map[string]any
}

var _ queue.Producer = (*mockProducer)(nil)

func (m *mockProducer) Enqueue(_ context.Context, jobType string, payload map[string]any) (string, error) {
	m.jobs = append(m.jobs, mockJob{jobType: jobType, payload: payload})
	return "test-job-id", nil
}

func (m *mockProducer) EnqueueWithOptions(_ context.Context, job queue.Job) (string, error) {
	m.jobs = append(m.jobs, mockJob{jobType: job.Type, payload: job.Payload})
	return "test-job-id", nil
}

func newTestPersonaHandler() (*Persona, *memory.PersonaRepository, *mockProducer) {
	repo := memory.NewPersonaRepository()
	q := &mockProducer{}
	svc := service.NewPersonaService(repo, q, nil, logging.Nop())
	handler := NewPersona(svc, logging.Nop())
	return handler, repo, q
}

func TestPersona_Create(t *testing.T) {
	handler, _, q := newTestPersonaHandler()

	tests := []struct {
		name       string
		body       any
		wantStatus int
	}{
		{
			name: "valid request",
			body: CreatePersonaRequest{
				Description: "mysterious woman with dark hair",
				Gender:      "woman",
			},
			wantStatus: http.StatusAccepted,
		},
		{
			name: "valid request with custom name",
			body: CreatePersonaRequest{
				Description: "energetic man who loves music",
				Gender:      "man",
				CustomName:  "DJ Beats",
			},
			wantStatus: http.StatusAccepted,
		},
		{
			name:       "empty body",
			body:       nil,
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "missing description",
			body: map[string]string{
				"gender": "woman",
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "invalid gender",
			body: map[string]string{
				"description": "a persona",
				"gender":      "invalid",
			},
			wantStatus: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := chi.NewRouter()
			r.Post("/personas", app.Wrap(handler.Create))

			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.MethodPost, "/personas", 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())
			}
		})
	}

	// Verify a generate_spec job was enqueued
	found := false
	for _, j := range q.jobs {
		if j.jobType == "generate_spec" {
			found = true
			if j.payload["stage"] != "spec" {
				t.Errorf("expected stage 'spec', got %v", j.payload["stage"])
			}
		}
	}
	if !found {
		t.Error("expected generate_spec job to be enqueued")
	}
}

func TestPersona_GetByID(t *testing.T) {
	handler, repo, _ := newTestPersonaHandler()

	// Seed data
	persona := &domain.Persona{
		Name:        "Test Persona",
		Handle:      "test_persona",
		Gender:      "woman",
		Description: "A test persona",
		Tags:        []string{},
		ImageURLs:   []string{},
		VideoURLs:   []string{},
		Status:      domain.PersonaStatusPending,
	}
	_ = repo.Create(context.Background(), persona)
	seededID := persona.ID.String()

	tests := []struct {
		name       string
		id         string
		wantStatus int
	}{
		{
			name:       "existing persona",
			id:         seededID,
			wantStatus: http.StatusOK,
		},
		{
			name:       "not found",
			id:         "550e8400-e29b-41d4-a716-446655440099",
			wantStatus: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := chi.NewRouter()
			r.Get("/personas/{id}", app.Wrap(handler.GetByID))

			req := httptest.NewRequest(http.MethodGet, "/personas/"+tt.id, nil)
			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.wantStatus == http.StatusOK {
				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")
				}
				if data["id"] != seededID {
					t.Errorf("expected id %s, got %v", seededID, data["id"])
				}
				if data["name"] != "Test Persona" {
					t.Errorf("expected name 'Test Persona', got %v", data["name"])
				}
			}
		})
	}
}

func TestPersona_List(t *testing.T) {
	handler, repo, _ := newTestPersonaHandler()

	t.Run("empty list", func(t *testing.T) {
		r := chi.NewRouter()
		r.Get("/personas", app.Wrap(handler.List))

		req := httptest.NewRequest(http.MethodGet, "/personas", nil)
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", w.Code)
		}

		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"].([]any)
		if !ok {
			t.Fatal("expected 'data' to be an array")
		}
		if len(data) != 0 {
			t.Errorf("expected 0 items, got %d", len(data))
		}
	})

	// Seed data
	for i, name := range []string{"Persona A", "Persona B", "Persona C"} {
		p := &domain.Persona{
			Name:        name,
			Handle:      name + "_handle",
			Gender:      "woman",
			Description: "description " + string(rune('A'+i)),
			Tags:        []string{},
			ImageURLs:   []string{},
			VideoURLs:   []string{},
			Status:      domain.PersonaStatusPending,
		}
		_ = repo.Create(context.Background(), p)
	}

	t.Run("returns all personas", func(t *testing.T) {
		r := chi.NewRouter()
		r.Get("/personas", app.Wrap(handler.List))

		req := httptest.NewRequest(http.MethodGet, "/personas", nil)
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", w.Code)
		}

		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"].([]any)
		if !ok {
			t.Fatal("expected 'data' to be an array")
		}
		if len(data) != 3 {
			t.Errorf("expected 3 items, got %d", len(data))
		}
	})

	t.Run("respects limit parameter", func(t *testing.T) {
		r := chi.NewRouter()
		r.Get("/personas", app.Wrap(handler.List))

		req := httptest.NewRequest(http.MethodGet, "/personas?limit=2", nil)
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", w.Code)
		}

		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"].([]any)
		if !ok {
			t.Fatal("expected 'data' to be an array")
		}
		if len(data) != 2 {
			t.Errorf("expected 2 items, got %d", len(data))
		}
	})

	t.Run("respects offset parameter", func(t *testing.T) {
		r := chi.NewRouter()
		r.Get("/personas", app.Wrap(handler.List))

		req := httptest.NewRequest(http.MethodGet, "/personas?offset=2", nil)
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)

		if w.Code != http.StatusOK {
			t.Errorf("expected status 200, got %d", w.Code)
		}

		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"].([]any)
		if !ok {
			t.Fatal("expected 'data' to be an array")
		}
		if len(data) != 1 {
			t.Errorf("expected 1 item, got %d", len(data))
		}
	})
}
