package handlers

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

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

// mockCommandQueue implements port.CommandQueue for testing.
type mockCommandQueue struct {
	commands []*domain.QueuedCommand
	err      error
}

func (m *mockCommandQueue) Enqueue(ctx context.Context, cmd *domain.QueuedCommand) error {
	if m.err != nil {
		return m.err
	}
	cmd.ID = domain.QueuedCommandID("queued-cmd-123")
	m.commands = append(m.commands, cmd)
	return nil
}

func (m *mockCommandQueue) Dequeue(ctx context.Context, projectID string) (*domain.QueuedCommand, error) {
	if m.err != nil {
		return nil, m.err
	}
	for _, cmd := range m.commands {
		if cmd.ProjectID == projectID && cmd.Status == domain.QueueStatusPending {
			cmd.Status = domain.QueueStatusRunning
			return cmd, nil
		}
	}
	return nil, nil
}

func (m *mockCommandQueue) UpdateStatus(ctx context.Context, cmdID domain.QueuedCommandID, status domain.QueueStatus, result *domain.QueuedCommandResult) error {
	return m.err
}

func (m *mockCommandQueue) Cancel(ctx context.Context, cmdID domain.QueuedCommandID) error {
	if m.err != nil {
		return m.err
	}
	for _, cmd := range m.commands {
		if cmd.ID == cmdID {
			if cmd.Status != domain.QueueStatusPending {
				return domain.ErrCommandNotFound
			}
			cmd.Status = domain.QueueStatusCancelled
			return nil
		}
	}
	return domain.ErrCommandNotFound
}

func (m *mockCommandQueue) GetByID(ctx context.Context, cmdID domain.QueuedCommandID) (*domain.QueuedCommand, error) {
	if m.err != nil {
		return nil, m.err
	}
	for _, cmd := range m.commands {
		if cmd.ID == cmdID {
			return cmd, nil
		}
	}
	return nil, domain.ErrCommandNotFound
}

func (m *mockCommandQueue) List(ctx context.Context, projectID string, filters *domain.QueueFilters) ([]*domain.QueuedCommand, error) {
	if m.err != nil {
		return nil, m.err
	}
	var result []*domain.QueuedCommand
	for _, cmd := range m.commands {
		if cmd.ProjectID == projectID {
			result = append(result, cmd)
		}
	}
	return result, nil
}

func (m *mockCommandQueue) GetStats(ctx context.Context, projectID string) (*domain.QueueStats, error) {
	if m.err != nil {
		return nil, m.err
	}
	return &domain.QueueStats{
		TotalPending:   1,
		TotalRunning:   0,
		TotalCompleted: 0,
		TotalFailed:    0,
		TotalCancelled: 0,
	}, nil
}

func (m *mockCommandQueue) CleanupOld(ctx context.Context, olderThanDays int) (int64, error) {
	return 0, m.err
}

// mockProjectRepo implements port.ProjectRepository for queue handler testing.
type mockProjectRepo struct {
	projects map[domain.ProjectID]*domain.Project
}

func newMockProjectRepo() *mockProjectRepo {
	return &mockProjectRepo{
		projects: make(map[domain.ProjectID]*domain.Project),
	}
}

func (m *mockProjectRepo) List(ctx context.Context) ([]domain.Project, error) {
	var result []domain.Project
	for _, p := range m.projects {
		result = append(result, *p)
	}
	return result, nil
}

func (m *mockProjectRepo) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) {
	if p, ok := m.projects[id]; ok {
		return p, nil
	}
	return nil, domain.ErrProjectNotFound
}

func (m *mockProjectRepo) Exists(ctx context.Context, id domain.ProjectID) (bool, error) {
	_, ok := m.projects[id]
	return ok, nil
}

func (m *mockProjectRepo) RefreshStatus(ctx context.Context) error {
	return nil
}

func (m *mockProjectRepo) Register(ctx context.Context, p *domain.Project) error {
	m.projects[p.ID] = p
	return nil
}

func (m *mockProjectRepo) Unregister(ctx context.Context, id domain.ProjectID) error {
	delete(m.projects, id)
	return nil
}

func TestQueueHandler_Enqueue(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})

	tests := []struct {
		name       string
		projectID  string
		body       EnqueueRequest
		wantStatus int
	}{
		{
			name:      "valid claude command",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "explain this code",
				CommandType: "claude",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name:      "valid shell command",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "ls -la",
				CommandType: "shell",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name:      "valid git command",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     `["status"]`,
				CommandType: "git",
			},
			wantStatus: http.StatusCreated,
		},
		{
			name:      "invalid command type",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "test",
				CommandType: "invalid",
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:      "empty command",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "",
				CommandType: "claude",
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:      "project not found",
			projectID: "unknown",
			body: EnqueueRequest{
				Command:     "test",
				CommandType: "claude",
			},
			wantStatus: http.StatusNotFound,
		},
		{
			name:      "dangerous shell command",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "rm -rf /",
				CommandType: "shell",
			},
			wantStatus: http.StatusBadRequest,
		},
		{
			name:      "invalid git command format",
			projectID: "proj-1",
			body: EnqueueRequest{
				Command:     "not json array",
				CommandType: "git",
			},
			wantStatus: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			queue := &mockCommandQueue{}
			h := NewQueueHandler(queue, projectRepo)

			r := chi.NewRouter()
			r.Post("/projects/{id}/queue/", h.Enqueue)

			body, _ := json.Marshal(tt.body)
			req := httptest.NewRequest(http.MethodPost, "/projects/"+tt.projectID+"/queue/", bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()

			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("Enqueue() status = %d, want %d, body: %s", w.Code, tt.wantStatus, w.Body.String())
			}
		})
	}
}

func TestQueueHandler_List(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})

	queue := &mockCommandQueue{
		commands: []*domain.QueuedCommand{
			{
				ID:          "cmd-1",
				ProjectID:   "proj-1",
				Command:     "test",
				CommandType: domain.CommandTypeClaude,
				Status:      domain.QueueStatusPending,
				CreatedAt:   time.Now(),
			},
		},
	}

	tests := []struct {
		name       string
		projectID  string
		query      string
		wantStatus int
		wantCount  int
	}{
		{
			name:       "list all commands",
			projectID:  "proj-1",
			query:      "",
			wantStatus: http.StatusOK,
			wantCount:  1,
		},
		{
			name:       "project not found",
			projectID:  "unknown",
			query:      "",
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "with limit",
			projectID:  "proj-1",
			query:      "?limit=10",
			wantStatus: http.StatusOK,
			wantCount:  1,
		},
		{
			name:       "with offset",
			projectID:  "proj-1",
			query:      "?offset=0",
			wantStatus: http.StatusOK,
			wantCount:  1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := NewQueueHandler(queue, projectRepo)

			r := chi.NewRouter()
			r.Get("/projects/{id}/queue/", h.List)

			req := httptest.NewRequest(http.MethodGet, "/projects/"+tt.projectID+"/queue/"+tt.query, nil)
			w := httptest.NewRecorder()

			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("List() status = %d, want %d", w.Code, tt.wantStatus)
			}

			if tt.wantStatus == http.StatusOK {
				var resp struct {
					Data ListResponse `json:"data"`
				}
				if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
					t.Fatalf("failed to unmarshal response: %v", err)
				}
				if len(resp.Data.Commands) != tt.wantCount {
					t.Errorf("List() count = %d, want %d", len(resp.Data.Commands), tt.wantCount)
				}
			}
		})
	}
}

func TestQueueHandler_GetByID(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})

	queue := &mockCommandQueue{
		commands: []*domain.QueuedCommand{
			{
				ID:          "cmd-123",
				ProjectID:   "proj-1",
				Command:     "test",
				CommandType: domain.CommandTypeClaude,
				Status:      domain.QueueStatusPending,
				CreatedAt:   time.Now(),
			},
		},
	}

	tests := []struct {
		name       string
		projectID  string
		cmdID      string
		wantStatus int
	}{
		{
			name:       "existing command",
			projectID:  "proj-1",
			cmdID:      "cmd-123",
			wantStatus: http.StatusOK,
		},
		{
			name:       "non-existent command",
			projectID:  "proj-1",
			cmdID:      "cmd-unknown",
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "project not found",
			projectID:  "unknown",
			cmdID:      "cmd-123",
			wantStatus: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := NewQueueHandler(queue, projectRepo)

			r := chi.NewRouter()
			r.Get("/projects/{id}/queue/{cmdId}", h.GetByID)

			req := httptest.NewRequest(http.MethodGet, "/projects/"+tt.projectID+"/queue/"+tt.cmdID, nil)
			w := httptest.NewRecorder()

			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("GetByID() status = %d, want %d", w.Code, tt.wantStatus)
			}
		})
	}
}

func TestQueueHandler_Cancel(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})

	tests := []struct {
		name       string
		projectID  string
		cmdID      string
		commands   []*domain.QueuedCommand
		wantStatus int
	}{
		{
			name:      "cancel pending command",
			projectID: "proj-1",
			cmdID:     "cmd-123",
			commands: []*domain.QueuedCommand{
				{
					ID:        "cmd-123",
					ProjectID: "proj-1",
					Status:    domain.QueueStatusPending,
				},
			},
			wantStatus: http.StatusOK,
		},
		{
			name:       "command not found",
			projectID:  "proj-1",
			cmdID:      "cmd-unknown",
			commands:   nil,
			wantStatus: http.StatusNotFound,
		},
		{
			name:       "project not found",
			projectID:  "unknown",
			cmdID:      "cmd-123",
			commands:   nil,
			wantStatus: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			queue := &mockCommandQueue{commands: tt.commands}
			h := NewQueueHandler(queue, projectRepo)

			r := chi.NewRouter()
			r.Delete("/projects/{id}/queue/{cmdId}", h.Cancel)

			req := httptest.NewRequest(http.MethodDelete, "/projects/"+tt.projectID+"/queue/"+tt.cmdID, nil)
			w := httptest.NewRecorder()

			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("Cancel() status = %d, want %d, body: %s", w.Code, tt.wantStatus, w.Body.String())
			}
		})
	}
}

func TestQueueHandler_Stats(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})

	queue := &mockCommandQueue{}

	tests := []struct {
		name       string
		projectID  string
		wantStatus int
	}{
		{
			name:       "get stats",
			projectID:  "proj-1",
			wantStatus: http.StatusOK,
		},
		{
			name:       "project not found",
			projectID:  "unknown",
			wantStatus: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := NewQueueHandler(queue, projectRepo)

			r := chi.NewRouter()
			r.Get("/projects/{id}/queue/stats", h.Stats)

			req := httptest.NewRequest(http.MethodGet, "/projects/"+tt.projectID+"/queue/stats", nil)
			w := httptest.NewRecorder()

			r.ServeHTTP(w, req)

			if w.Code != tt.wantStatus {
				t.Errorf("Stats() status = %d, want %d", w.Code, tt.wantStatus)
			}
		})
	}
}

func TestQueueHandler_Enqueue_CommandSizeLimit(t *testing.T) {
	projectRepo := newMockProjectRepo()
	projectRepo.Register(context.Background(), &domain.Project{ID: "proj-1", Name: "Test Project"})
	queue := &mockCommandQueue{}
	h := NewQueueHandler(queue, projectRepo)

	r := chi.NewRouter()
	r.Post("/projects/{id}/queue/", h.Enqueue)

	// Create a command that exceeds MaxCommandSize (10KB)
	largeCommand := make([]byte, MaxCommandSize+1)
	for i := range largeCommand {
		largeCommand[i] = 'a'
	}

	body := EnqueueRequest{
		Command:     string(largeCommand),
		CommandType: "claude",
	}

	bodyBytes, _ := json.Marshal(body)
	req := httptest.NewRequest(http.MethodPost, "/projects/proj-1/queue/", bytes.NewReader(bodyBytes))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	r.ServeHTTP(w, req)

	if w.Code != http.StatusBadRequest {
		t.Errorf("Enqueue() with large command status = %d, want %d", w.Code, http.StatusBadRequest)
	}
}
