package http

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

	"github.com/go-chi/chi/v5"
	"github.com/nomadcode/nomadcode-core/internal/db"
	"github.com/nomadcode/nomadcode-core/internal/projectsync"
	"github.com/nomadcode/nomadcode-core/internal/storage"
	"github.com/nomadcode/nomadcode-core/internal/workflow"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
	"github.com/nomadcode/nomadcode-core/internal/workitempipeline"
)

type fakeWorkItemTaskCreator struct {
	task       storage.Task
	err        error
	calledWith *workitempipeline.CreateTaskInput
}

func (f *fakeWorkItemTaskCreator) CreateTaskFromWorkItem(_ context.Context, input workitempipeline.CreateTaskInput) (storage.Task, error) {
	f.calledWith = &input
	return f.task, f.err
}

type handlerOpt func(*Handler)

func withProviders(providers map[workitem.ProviderID]WorkItemTaskCreator) handlerOpt {
	return func(h *Handler) {
		h.workItemProviders = providers
	}
}

func withPlaneCreator(creator WorkItemTaskCreator) handlerOpt {
	return func(h *Handler) {
		h.workItemProviders["plane"] = creator
	}
}

type fakeWebhookEnqueuer struct {
	task     storage.Task
	err      error
	enqueued []string
}

func (f *fakeWebhookEnqueuer) EnqueueTask(_ context.Context, id string) (storage.Task, error) {
	f.enqueued = append(f.enqueued, id)
	return f.task, f.err
}

func withWebhookEnqueuer(enqueuer webhookTaskEnqueuer) handlerOpt {
	return func(h *Handler) {
		h.webhookEnqueuer = enqueuer
	}
}

func newHandlerForTest(opts ...handlerOpt) *Handler {
	h := &Handler{
		workItemProviders: make(map[workitem.ProviderID]WorkItemTaskCreator),
		gitoDelivery:      newGitoDeliveryStore(),
	}
	for _, opt := range opts {
		opt(h)
	}
	return h
}

func TestNewHandlerRegistersConfiguredWorkItemProviders(t *testing.T) {
	workflowSvc := workflow.NewService(nil, nil, nil)
	planeReader := &fakeReaderAdapter{}
	jiraReader := &fakeReaderAdapter{}

	h := NewHandler(
		nil,
		workflowSvc,
		nil,
		nil,
		WorkItemProvider{ID: "plane", Reader: planeReader},
		WorkItemProvider{ID: "jira", Reader: jiraReader},
	)

	if len(h.workItemProviders) != 2 {
		t.Fatalf("expected 2 providers in registry, got %d", len(h.workItemProviders))
	}
	if _, ok := h.workItemProviders["plane"]; !ok {
		t.Error("plane provider missing from registry")
	}
	if _, ok := h.workItemProviders["jira"]; !ok {
		t.Error("jira provider missing from registry")
	}
}

func TestNewHandlerSkipsNilProviderReaders(t *testing.T) {
	workflowSvc := workflow.NewService(nil, nil, nil)
	jiraReader := &fakeReaderAdapter{}

	h := NewHandler(
		nil,
		workflowSvc,
		nil,
		nil,
		WorkItemProvider{ID: "plane", Reader: nil},
		WorkItemProvider{ID: "jira", Reader: jiraReader},
	)

	if _, ok := h.workItemProviders["plane"]; ok {
		t.Error("plane should not be registered when nil")
	}
	if _, ok := h.workItemProviders["jira"]; !ok {
		t.Error("jira should still be registered")
	}
}

func TestNewHandlerNilWorkflowServiceSkipsAllRegistration(t *testing.T) {
	planeReader := &fakeReaderAdapter{}
	jiraReader := &fakeReaderAdapter{}

	h := NewHandler(
		nil,
		nil,
		nil,
		nil,
		WorkItemProvider{ID: "plane", Reader: planeReader},
		WorkItemProvider{ID: "jira", Reader: jiraReader},
	)

	if len(h.workItemProviders) != 0 {
		t.Fatalf("expected 0 providers with nil workflow service, got %d", len(h.workItemProviders))
	}
}

func TestRegisterWorkItemProviderSkipsNilOrEmpty(t *testing.T) {
	h := newHandlerForTest()

	h.registerWorkItemProvider("", &fakeWorkItemTaskCreator{task: storage.Task{ID: "x"}})
	h.registerWorkItemProvider("empty", nil)
	h.registerWorkItemProvider("  ", &fakeWorkItemTaskCreator{task: storage.Task{ID: "y"}})
	h.registerWorkItemProvider("jira", &fakeWorkItemTaskCreator{task: storage.Task{ID: "j"}})

	if len(h.workItemProviders) != 1 {
		t.Fatalf("expected 1 provider after skip, got %d: keys=%v", len(h.workItemProviders), getProviderKeys(h.workItemProviders))
	}
	if _, ok := h.workItemProviders["jira"]; !ok {
		t.Error("valid provider jira should be registered")
	}
	if _, ok := h.workItemProviders[""]; ok {
		t.Error("empty string provider should not be registered")
	}
	if _, ok := h.workItemProviders["empty"]; ok {
		t.Error("nil provider should not be registered")
	}
	if _, ok := h.workItemProviders["  "]; ok {
		t.Error("whitespace-only provider should not be registered")
	}
}

func getProviderKeys(m map[workitem.ProviderID]WorkItemTaskCreator) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, string(k))
	}
	return keys
}

type fakeReaderAdapter struct{}

func (f *fakeReaderAdapter) FetchWorkItem(_ context.Context, ref workitem.Ref) (workitem.WorkItem, error) {
	return workitem.WorkItem{
		Ref:   ref,
		Title: "fetched",
	}, nil
}

func TestLegacyWorkItemTaskRequestBuildsProviderNeutralRef(t *testing.T) {
	req := legacyWorkItemTaskRequest{
		WorkspaceSlug: "  acme  ",
		ProjectID:     "  proj-1  ",
		WorkItemID:    "  work-1  ",
		ExternalURL:   "  https://plane.example/work-1  ",
	}
	ref, err := req.workItemRef(workitem.ProviderID("plane"))
	if err != nil {
		t.Fatalf("workItemRef returned error: %v", err)
	}
	if ref.Provider != workitem.ProviderID("plane") {
		t.Errorf("provider: got %q", ref.Provider)
	}
	if ref.Tenant != "acme" {
		t.Errorf("tenant: got %q", ref.Tenant)
	}
	if ref.Project != "proj-1" {
		t.Errorf("project: got %q", ref.Project)
	}
	if ref.ID != "work-1" {
		t.Errorf("id: got %q", ref.ID)
	}
	if ref.URL != "https://plane.example/work-1" {
		t.Errorf("url: got %q", ref.URL)
	}
}

func TestCreatePlaneTaskCompatibilityRouteUsesRegistry(t *testing.T) {
	planeCreator := &fakeWorkItemTaskCreator{
		task: storage.Task{ID: "planed-task-123", Status: "pending"},
	}

	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"plane": planeCreator,
		}),
		withPlaneCreator(planeCreator),
	)

	body := `{"workspace_slug":"acme","project_id":"proj-1","work_item_id":"work-1","state_id":"state-1","comment":"my note","trigger_state_id":" state-backlog ","agent_assignee_id":" agent-1 ","actor":" actor-user ","self_actor":" self-bot "}`
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/plane/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.CreatePlaneTask(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected %d, got %d: %s", http.StatusCreated, rec.Code, rec.Body.String())
	}
	if planeCreator.calledWith == nil {
		t.Fatal("registry CreateTaskFromWorkItem was not called")
	}
	if planeCreator.calledWith.Ref.Provider != "plane" {
		t.Errorf("ref.provider: got %q", planeCreator.calledWith.Ref.Provider)
	}
	if planeCreator.calledWith.Ref.Tenant != "acme" {
		t.Errorf("ref.tenant: got %q", planeCreator.calledWith.Ref.Tenant)
	}
	if planeCreator.calledWith.Ref.Project != "proj-1" {
		t.Errorf("ref.project: got %q", planeCreator.calledWith.Ref.Project)
	}
	if planeCreator.calledWith.Ref.ID != "work-1" {
		t.Errorf("ref.id: got %q", planeCreator.calledWith.Ref.ID)
	}
	if planeCreator.calledWith.Trigger.RequiredStateID != "state-backlog" {
		t.Errorf("trigger.RequiredStateID: got %q, want 'state-backlog'", planeCreator.calledWith.Trigger.RequiredStateID)
	}
	if planeCreator.calledWith.Trigger.RequiredAssigneeID != "agent-1" {
		t.Errorf("trigger.RequiredAssigneeID: got %q, want 'agent-1'", planeCreator.calledWith.Trigger.RequiredAssigneeID)
	}
	if planeCreator.calledWith.Trigger.Actor != "actor-user" {
		t.Errorf("trigger.Actor: got %q, want 'actor-user'", planeCreator.calledWith.Trigger.Actor)
	}
	if planeCreator.calledWith.Trigger.SelfActor != "self-bot" {
		t.Errorf("trigger.SelfActor: got %q, want 'self-bot'", planeCreator.calledWith.Trigger.SelfActor)
	}

	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp["id"] != "planed-task-123" || resp["status"] != "pending" {
		t.Errorf("response: %v", resp)
	}
}

func TestCreatePlaneTaskReturns503WhenPipelineNotConfigured(t *testing.T) {
	h := &Handler{}
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/plane/tasks", strings.NewReader(`{}`))
	rec := httptest.NewRecorder()

	h.CreatePlaneTask(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("expected 503, got %d", rec.Code)
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp["error"] != "plane client is not configured" {
		t.Errorf("error message: got %q", resp["error"])
	}
}

func TestCreateWorkItemTaskDelegatesToRegisteredProvider(t *testing.T) {
	creator := &fakeWorkItemTaskCreator{
		task: storage.Task{ID: "jira-task-456", Status: "pending"},
	}

	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": creator,
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"PROJ-1","state_id":"state-2","comment":"jira note"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected %d, got %d: %s", http.StatusCreated, rec.Code, rec.Body.String())
	}
	if creator.calledWith == nil {
		t.Fatal("CreateTaskFromWorkItem was not called")
	}
	if creator.calledWith.Ref.Provider != "jira" {
		t.Errorf("ref.provider: got %q", creator.calledWith.Ref.Provider)
	}
	if creator.calledWith.Ref.Tenant != "acme" {
		t.Errorf("ref.tenant: got %q", creator.calledWith.Ref.Tenant)
	}
	if creator.calledWith.Ref.ID != "PROJ-1" {
		t.Errorf("ref.id: got %q", creator.calledWith.Ref.ID)
	}
}

func TestCreateWorkItemTaskRejectsUnknownProvider(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{task: storage.Task{ID: "t1"}},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"TICKET-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/asana/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "unknown or unconfigured provider") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskRejectsProviderMismatch(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira":  &fakeWorkItemTaskCreator{task: storage.Task{ID: "t1"}},
			"asana": &fakeWorkItemTaskCreator{task: storage.Task{ID: "t2"}},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"TICKET-1","provider":"asana"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "provider mismatch") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskRejectsInvalidJSONBody(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{task: storage.Task{ID: "t1"}},
		}),
	)

	body := `{invalid json}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "invalid JSON body") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskRejectsEmptyBody(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{task: storage.Task{ID: "t1"}},
		}),
	)

	body := `{"provider":"jira"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "tenant, project, and id are required") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskReturns422WhenProjectSyncMissing(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{err: storage.ErrProjectSyncNotFound},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"PROJ-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "no active project sync setting") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskReturns503WhenResolverUnconfigured(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{err: workitempipeline.ErrProjectSyncNotConfigured},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"PROJ-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("expected 503, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "project sync resolver is not configured") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskReturns409WhenNoSlotAvailable(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{err: storage.ErrNoAvailableWorkspaceSlot},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"PROJ-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusConflict {
		t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "no available workspace slot") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

type fakeProjectSyncStore struct {
	setting      db.ProjectSyncSetting
	settingErr   error
	slot         db.WorkspaceSlot
	slotErr      error
	gotProvider  string
	gotTenant    string
	gotProject   string
	reservedFor  int64
	upsertedSlot *db.UpsertWorkspaceSlotParams
	upsertSlot   db.WorkspaceSlot
	upsertErr    error
}

func (f *fakeProjectSyncStore) GetActiveProjectSyncSettingByTarget(_ context.Context, provider, tenant, project string) (db.ProjectSyncSetting, error) {
	f.gotProvider, f.gotTenant, f.gotProject = provider, tenant, project
	return f.setting, f.settingErr
}

func (f *fakeProjectSyncStore) UpsertWorkspaceSlot(_ context.Context, args db.UpsertWorkspaceSlotParams) (db.WorkspaceSlot, error) {
	f.upsertedSlot = &args
	return f.upsertSlot, f.upsertErr
}

func (f *fakeProjectSyncStore) ReserveWorkspaceSlot(_ context.Context, projectSyncSettingID int64) (db.WorkspaceSlot, error) {
	f.reservedFor = projectSyncSettingID
	return f.slot, f.slotErr
}

func TestNewProjectBinderResolvesBindingByTarget(t *testing.T) {
	store := &fakeProjectSyncStore{setting: db.ProjectSyncSetting{
		ID:                7,
		Provider:          "plane",
		Tenant:            "acme",
		Project:           "proj-1",
		GitRemoteUrl:      "git@example.com:acme/nomadcode.git",
		SourceBranch:      "develop",
		WorkspaceID:       "ws-main",
		WorkspaceBasePath: "/home/user/workspace",
		RepoDirName:       "nomadcode",
		Active:            true,
	}}
	binder := NewProjectBinder(store)

	binding, err := binder.ResolveProjectBinding(context.Background(), workitem.Ref{
		Provider: "plane", Tenant: "acme", Project: "proj-1", ID: "work-1",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if store.gotProvider != "plane" || store.gotTenant != "acme" || store.gotProject != "proj-1" {
		t.Errorf("store received wrong target: %s/%s/%s", store.gotProvider, store.gotTenant, store.gotProject)
	}
	if binding.SettingID != 7 {
		t.Errorf("setting id: got %d, want 7", binding.SettingID)
	}
	if binding.Config.Target.Project != "proj-1" || binding.Config.GitRemoteURL != "git@example.com:acme/nomadcode.git" {
		t.Errorf("config not mapped from setting: %+v", binding.Config)
	}
}

func TestNewProjectBinderReservesSlotBySettingID(t *testing.T) {
	store := &fakeProjectSyncStore{slot: db.WorkspaceSlot{
		ID:                   11,
		ProjectSyncSettingID: 7,
		SlotIndex:            1,
		State:                string(projectsync.SlotStateInUse),
		Path:                 "/home/user/workspace/nomadcode/slots/001",
	}}
	binder := NewProjectBinder(store)

	slot, err := binder.ReserveWorkspaceSlot(context.Background(), 7)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if store.reservedFor != 7 {
		t.Errorf("reserved for: got %d, want 7", store.reservedFor)
	}
	if slot.ID != 11 || slot.Index != projectsync.SlotIndex(1) || slot.State != projectsync.SlotStateInUse {
		t.Errorf("slot not converted from db record: %+v", slot)
	}
}

func TestNewProjectBinderPropagatesErrors(t *testing.T) {
	resolveStore := &fakeProjectSyncStore{settingErr: storage.ErrProjectSyncNotFound}
	if _, err := NewProjectBinder(resolveStore).ResolveProjectBinding(context.Background(), workitem.Ref{
		Provider: "plane", Tenant: "acme", Project: "proj-x", ID: "work-1",
	}); !errors.Is(err, storage.ErrProjectSyncNotFound) {
		t.Errorf("resolve: expected ErrProjectSyncNotFound, got %v", err)
	}

	reserveStore := &fakeProjectSyncStore{slotErr: storage.ErrNoAvailableWorkspaceSlot}
	if _, err := NewProjectBinder(reserveStore).ReserveWorkspaceSlot(context.Background(), 7); !errors.Is(err, storage.ErrNoAvailableWorkspaceSlot) {
		t.Errorf("reserve: expected ErrNoAvailableWorkspaceSlot, got %v", err)
	}
}

func TestNewProjectBinderNilStoreYieldsNil(t *testing.T) {
	// A nil store must produce a nil binder so the pipeline fails explicitly as
	// unconfigured rather than panicking at request time.
	if binder := NewProjectBinder(nil); binder != nil {
		t.Fatalf("expected nil binder for nil store, got %T", binder)
	}
}

type fakeWorkspaceProvisioner struct {
	err        error
	calledWith *projectsync.ProvisionPlan
}

func (f *fakeWorkspaceProvisioner) EnsureProvisioned(_ context.Context, plan projectsync.ProvisionPlan) error {
	f.calledWith = &plan
	return f.err
}

func TestNewProjectBinderEnsuresWorkspaceAndRegistersDefaultSlot(t *testing.T) {
	store := &fakeProjectSyncStore{
		setting: db.ProjectSyncSetting{
			ID:                5,
			Provider:          "plane",
			Tenant:            "acme",
			Project:           "proj-1",
			GitRemoteUrl:      "git@example.com:acme/nomadcode.git",
			SourceBranch:      "develop",
			WorkspaceID:       "ws-main",
			WorkspaceBasePath: "/home/user/workspace",
			RepoDirName:       "nomadcode",
			Active:            true,
		},
		upsertSlot: db.WorkspaceSlot{
			ID: 11,
		},
	}
	provisioner := &fakeWorkspaceProvisioner{}
	binder := NewProjectBinderWithProvisioner(store, provisioner)

	binding := workitempipeline.ProjectBinding{
		SettingID: 5,
		Config: projectsync.Config{
			Target: projectsync.ProviderProjectTarget{
				Provider: "plane", Tenant: "acme", Project: "proj-1",
			},
			GitRemoteURL:      "git@example.com:acme/nomadcode.git",
			SourceBranch:      "develop",
			WorkspaceID:       "ws-main",
			WorkspaceBasePath: "/home/user/workspace",
			RepoDirName:       "nomadcode",
		},
	}

	err := binder.EnsureProjectWorkspace(context.Background(), binding)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if provisioner.calledWith == nil {
		t.Fatal("provisioner.EnsureProvisioned was not called")
	}
	wantRoot := "/home/user/workspace/nomadcode"
	if provisioner.calledWith.ProjectWorkspaceRoot != wantRoot {
		t.Errorf("ProjectWorkspaceRoot: got %q, want %q", provisioner.calledWith.ProjectWorkspaceRoot, wantRoot)
	}

	if store.upsertedSlot == nil {
		t.Fatal("store.UpsertWorkspaceSlot was not called")
	}
	if store.upsertedSlot.ProjectSyncSettingID != 5 {
		t.Errorf("upserted ProjectSyncSettingID: got %d, want 5", store.upsertedSlot.ProjectSyncSettingID)
	}
	if store.upsertedSlot.SlotIndex != int32(projectsync.DefaultSlotIndex) {
		t.Errorf("upserted SlotIndex: got %d, want %d", store.upsertedSlot.SlotIndex, projectsync.DefaultSlotIndex)
	}
	wantSlotPath := "/home/user/workspace/nomadcode/slots/000"
	if store.upsertedSlot.Path != wantSlotPath {
		t.Errorf("upserted Path: got %q, want %q", store.upsertedSlot.Path, wantSlotPath)
	}
}

func TestNewProjectBinderPropagatesProvisionErrorWithoutUpsert(t *testing.T) {
	store := &fakeProjectSyncStore{}
	provisionErr := errors.New("provision error")
	provisioner := &fakeWorkspaceProvisioner{err: provisionErr}
	binder := NewProjectBinderWithProvisioner(store, provisioner)

	binding := workitempipeline.ProjectBinding{
		SettingID: 5,
		Config: projectsync.Config{
			Target: projectsync.ProviderProjectTarget{
				Provider: "plane", Tenant: "acme", Project: "proj-1",
			},
			GitRemoteURL:      "git@example.com:acme/nomadcode.git",
			SourceBranch:      "develop",
			WorkspaceID:       "ws-main",
			WorkspaceBasePath: "/home/user/workspace",
			RepoDirName:       "nomadcode",
		},
	}

	err := binder.EnsureProjectWorkspace(context.Background(), binding)
	if !errors.Is(err, provisionErr) {
		t.Fatalf("expected provision error, got %v", err)
	}

	if store.upsertedSlot != nil {
		t.Error("store.UpsertWorkspaceSlot should not be called on provision failure")
	}
}

func TestCreateWorkItemTaskReturns409WhenWorkspaceProvisionNotReady(t *testing.T) {
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"jira": &fakeWorkItemTaskCreator{err: projectsync.ErrWorkspaceProvisionNotReady},
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"PROJ-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/jira/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusConflict {
		t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
	}
	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if !strings.Contains(resp["error"], "workspace provision is not ready") {
		t.Errorf("unexpected error: %q", resp["error"])
	}
}

func TestCreateWorkItemTaskPassesCreationTrigger(t *testing.T) {
	creator := &fakeWorkItemTaskCreator{
		task: storage.Task{ID: "task-123", Status: "pending"},
	}
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"plane": creator,
		}),
	)

	body := `{
		"tenant": "acme",
		"project": "proj-1",
		"id": "work-1",
		"trigger_state_id": "state-backlog",
		"agent_assignee_id": "agent-1",
		"actor": "user-a",
		"self_actor": "nomadcode-bot"
	}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/plane/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("expected 201 Created, got %d: %s", rec.Code, rec.Body.String())
	}
	if creator.calledWith == nil {
		t.Fatal("expected CreateTaskFromWorkItem call")
	}
	trig := creator.calledWith.Trigger
	if trig.RequiredStateID != "state-backlog" || trig.RequiredAssigneeID != "agent-1" || trig.Actor != "user-a" || trig.SelfActor != "nomadcode-bot" {
		t.Errorf("unexpected trigger passed: %+v", trig)
	}
}

func TestCreateWorkItemTaskReturns202WhenTriggerIgnored(t *testing.T) {
	creator := &fakeWorkItemTaskCreator{
		err: workitempipeline.ErrCreationTriggerIgnored,
	}
	h := newHandlerForTest(
		withProviders(map[workitem.ProviderID]WorkItemTaskCreator{
			"plane": creator,
		}),
	)

	body := `{"tenant":"acme","project":"proj-1","id":"work-1"}`
	req := httptest.NewRequest(http.MethodPost, "/integrations/plane/tasks", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	router := chi.NewRouter()
	router.Post("/integrations/{provider}/tasks", h.CreateWorkItemTask)
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusAccepted {
		t.Fatalf("expected 202 Accepted, got %d: %s", rec.Code, rec.Body.String())
	}

	var resp map[string]string
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp["status"] != "ignored" || resp["reason"] != workitempipeline.ErrCreationTriggerIgnored.Error() {
		t.Errorf("unexpected ignored response: %v", resp)
	}
}
