package handlers

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"chat-switchboard/database"
)

// ═══════════════════════════════════════════════
// Task CRUD Tests
// ═══════════════════════════════════════════════

func TestTask_CreateAndGet(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("taskowner", "taskowner@test.com")

	// Create a prompt task
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":        "Test Task",
		"description": "Unit test task",
		"task_type":   "prompt",
		"schedule":    "@daily",
		"user_prompt": "Summarize news",
		"model_id":    "test-model",
		"timezone":    "UTC",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create task: want 201, got %d: %s", w.Code, w.Body.String())
	}

	var task map[string]interface{}
	decode(w, &task)

	taskID := task["id"].(string)
	if taskID == "" {
		t.Fatal("task should have an id")
	}
	if task["name"].(string) != "Test Task" {
		t.Fatalf("name mismatch: got %q", task["name"])
	}
	if task["is_active"] != true {
		t.Fatal("task should be active by default")
	}
	if task["schedule"].(string) != "@daily" {
		t.Fatalf("schedule mismatch: got %q", task["schedule"])
	}
	// next_run_at should be computed
	if task["next_run_at"] == nil {
		t.Fatal("next_run_at should be set for cron task")
	}
	// created_at should not be zero time
	createdAt, _ := task["created_at"].(string)
	if createdAt == "" || createdAt == "0001-01-01T00:00:00Z" {
		t.Fatalf("created_at should be a real timestamp, got %q", createdAt)
	}

	// Get it back
	w = h.request("GET", "/api/v1/tasks/"+taskID, token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("get task: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var got map[string]interface{}
	decode(w, &got)
	if got["id"].(string) != taskID {
		t.Fatal("get returned wrong task")
	}
}

func TestTask_ListMine(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("lister", "lister@test.com")

	// Create 2 tasks
	for i := 0; i < 2; i++ {
		w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
			"name":        fmt.Sprintf("Task %d", i),
			"task_type":   "prompt",
			"schedule":    "@hourly",
			"user_prompt": "do stuff",
			"model_id":    "m",
		})
		if w.Code != http.StatusCreated {
			t.Fatalf("create task %d: want 201, got %d", i, w.Code)
		}
	}

	// List
	w := h.request("GET", "/api/v1/tasks", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list: want 200, got %d", w.Code)
	}
	var resp map[string]interface{}
	decode(w, &resp)
	data := resp["data"].([]interface{})
	if len(data) != 2 {
		t.Fatalf("expected 2 tasks, got %d", len(data))
	}
}

func TestTask_Update(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("updater", "updater@test.com")

	// Create
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Before", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "old", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Patch name and user_prompt
	w = h.request("PUT", "/api/v1/tasks/"+taskID, token, map[string]interface{}{
		"name":        "After",
		"user_prompt": "new prompt",
	})
	if w.Code != http.StatusOK {
		t.Fatalf("update: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var updated map[string]interface{}
	decode(w, &updated)
	if updated["name"].(string) != "After" {
		t.Fatalf("name not updated: got %q", updated["name"])
	}
	if updated["user_prompt"].(string) != "new prompt" {
		t.Fatalf("user_prompt not updated: got %q", updated["user_prompt"])
	}
}

func TestTask_UpdateToolGrants(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("grantuser", "grant@test.com")

	// Create with tool_grants
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Grants", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
		"tool_grants": []string{"web_search"},
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Patch tool_grants
	newGrants := json.RawMessage(`["url_fetch","web_search"]`)
	w = h.request("PUT", "/api/v1/tasks/"+taskID, token, map[string]interface{}{
		"tool_grants": newGrants,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("update tool_grants: want 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify via GET
	w = h.request("GET", "/api/v1/tasks/"+taskID, token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("get: %d", w.Code)
	}
	var got map[string]interface{}
	decode(w, &got)
	grants, ok := got["tool_grants"].([]interface{})
	if !ok || len(grants) != 2 {
		t.Fatalf("tool_grants not updated: got %v", got["tool_grants"])
	}
}

func TestTask_Delete(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("deleter", "deleter@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Doomed", "task_type": "prompt", "schedule": "once",
		"user_prompt": "bye", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Delete
	w = h.request("DELETE", "/api/v1/tasks/"+taskID, token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("delete: want 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify gone
	w = h.request("GET", "/api/v1/tasks/"+taskID, token, nil)
	if w.Code != http.StatusNotFound {
		t.Fatalf("get deleted: want 404, got %d", w.Code)
	}
}

// ═══════════════════════════════════════════════
// Ownership / Access Control Tests
// ═══════════════════════════════════════════════

func TestTask_OwnershipIsolation(t *testing.T) {
	h := setupHarness(t)
	_, tokenA := h.createAdminUser("ownerA", "ownerA@test.com")

	// User B — non-admin regular user (seeded directly, bypassing register endpoint)
	userB := database.SeedTestUser(h.t, "ownerB", "ownerB@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userB)
	tokenB := makeToken(userB, "ownerB@test.com", "user")

	// A creates a task
	w := h.request("POST", "/api/v1/tasks", tokenA, map[string]interface{}{
		"name": "A's Task", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "private", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// B cannot GET it (no RequirePermission on GET — reaches handler → 404)
	w = h.request("GET", "/api/v1/tasks/"+taskID, tokenB, nil)
	if w.Code != http.StatusNotFound {
		t.Fatalf("non-owner GET: want 404, got %d", w.Code)
	}

	// B cannot UPDATE it (RequirePermission blocks → 403, or handler blocks → 404)
	w = h.request("PUT", "/api/v1/tasks/"+taskID, tokenB, map[string]interface{}{
		"name": "Hijacked",
	})
	if w.Code == http.StatusOK {
		t.Fatal("non-owner PUT should not succeed")
	}

	// B cannot DELETE it
	w = h.request("DELETE", "/api/v1/tasks/"+taskID, tokenB, nil)
	if w.Code == http.StatusOK {
		t.Fatal("non-owner DELETE should not succeed")
	}

	// B's list should be empty (no tasks of their own)
	w = h.request("GET", "/api/v1/tasks", tokenB, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list: want 200, got %d", w.Code)
	}
	var resp map[string]interface{}
	decode(w, &resp)
	data := resp["data"].([]interface{})
	if len(data) != 0 {
		t.Fatalf("expected 0 tasks for B, got %d", len(data))
	}
}

// ═══════════════════════════════════════════════
// Validation Tests
// ═══════════════════════════════════════════════

func TestTask_CreateValidation(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("validator", "validator@test.com")

	tests := []struct {
		name string
		body map[string]interface{}
		want int
	}{
		{
			name: "missing name",
			body: map[string]interface{}{"task_type": "prompt", "schedule": "@daily", "user_prompt": "x", "model_id": "m"},
			want: http.StatusBadRequest,
		},
		{
			name: "missing schedule",
			body: map[string]interface{}{"name": "X", "task_type": "prompt", "user_prompt": "x", "model_id": "m"},
			want: http.StatusBadRequest,
		},
		{
			name: "prompt without user_prompt",
			body: map[string]interface{}{"name": "X", "task_type": "prompt", "schedule": "@daily", "model_id": "m"},
			want: http.StatusBadRequest,
		},
		{
			name: "invalid cron",
			body: map[string]interface{}{"name": "X", "task_type": "prompt", "schedule": "not-a-cron", "user_prompt": "x", "model_id": "m"},
			want: http.StatusBadRequest,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			w := h.request("POST", "/api/v1/tasks", token, tc.body)
			if w.Code != tc.want {
				t.Fatalf("want %d, got %d: %s", tc.want, w.Code, w.Body.String())
			}
		})
	}
}

func TestTask_UpdateInvalidSchedule(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("badsched", "badsched@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "T", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)

	// Try to update with invalid schedule
	w = h.request("PUT", "/api/v1/tasks/"+task["id"].(string), token, map[string]interface{}{
		"schedule": "garbage-cron",
	})
	if w.Code != http.StatusBadRequest {
		t.Fatalf("invalid schedule update: want 400, got %d: %s", w.Code, w.Body.String())
	}
}

// ═══════════════════════════════════════════════
// Run Now / Kill Tests
// ═══════════════════════════════════════════════

func TestTask_RunNowAndListRuns(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("runner", "runner@test.com")

	// Create
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Runnable", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "go", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Run Now
	w = h.request("POST", "/api/v1/tasks/"+taskID+"/run", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("run now: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var runResp map[string]interface{}
	decode(w, &runResp)
	if runResp["scheduled"] != true {
		t.Fatal("expected scheduled: true")
	}

	// List runs — might be empty if scheduler hasn't ticked, but endpoint should work
	w = h.request("GET", "/api/v1/tasks/"+taskID+"/runs", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list runs: want 200, got %d: %s", w.Code, w.Body.String())
	}
}

func TestTask_KillNoActiveRun(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("killer", "killer@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "NoRun", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)

	// Kill with no active run — should 404
	w = h.request("POST", "/api/v1/tasks/"+task["id"].(string)+"/kill", token, nil)
	if w.Code != http.StatusNotFound {
		t.Fatalf("kill no-run: want 404, got %d: %s", w.Code, w.Body.String())
	}
}

// ═══════════════════════════════════════════════
// Webhook Trigger Tests (v0.28.0)
// ═══════════════════════════════════════════════

func TestTask_WebhookScheduleCreatesTriggerToken(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("webhooker", "webhooker@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":        "Webhook Task",
		"task_type":   "prompt",
		"schedule":    "webhook",
		"user_prompt": "process this",
		"model_id":    "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create webhook task: want 201, got %d: %s", w.Code, w.Body.String())
	}

	var task map[string]interface{}
	decode(w, &task)

	// Should have trigger_token
	triggerToken, ok := task["trigger_token"].(string)
	if !ok || triggerToken == "" {
		t.Fatal("webhook task should have a trigger_token")
	}

	// next_run_at should be nil (webhook tasks don't have cron)
	if task["next_run_at"] != nil {
		t.Fatalf("webhook task should have nil next_run_at, got %v", task["next_run_at"])
	}
}

func TestTask_TriggerEndpoint(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("triguser", "triguser@test.com")

	// Create webhook task
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":        "CI Watcher",
		"task_type":   "prompt",
		"schedule":    "webhook",
		"user_prompt": "analyze failure",
		"model_id":    "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	triggerToken := task["trigger_token"].(string)

	// Fire the trigger (unauthenticated)
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{
		"build_id": 12345,
		"status":   "failed",
		"repo":     "chat-switchboard",
	})
	if w.Code != http.StatusAccepted {
		t.Fatalf("trigger: want 202, got %d: %s", w.Code, w.Body.String())
	}
	var trigResp map[string]interface{}
	decode(w, &trigResp)
	if trigResp["triggered"] != true {
		t.Fatal("expected triggered: true")
	}
	if trigResp["run_id"] == nil || trigResp["run_id"].(string) == "" {
		t.Fatal("expected run_id")
	}

	// Second trigger should 409 (queued run exists)
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{
		"build_id": 12346,
	})
	if w.Code != http.StatusConflict {
		t.Fatalf("duplicate trigger: want 409, got %d: %s", w.Code, w.Body.String())
	}
}

func TestTask_TriggerInvalidToken(t *testing.T) {
	h := setupHarness(t)

	w := h.request("POST", "/api/v1/hooks/t/nonexistent-token", "", map[string]interface{}{
		"data": "nope",
	})
	if w.Code != http.StatusNotFound {
		t.Fatalf("bad token: want 404, got %d", w.Code)
	}
}

func TestTask_TriggerInactiveTask(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("inactive", "inactive@test.com")

	// Create webhook task
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Deactivated", "task_type": "prompt", "schedule": "webhook",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)
	triggerToken := task["trigger_token"].(string)

	// Deactivate
	w = h.request("PUT", "/api/v1/tasks/"+taskID, token, map[string]interface{}{
		"is_active": false,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("deactivate: %d", w.Code)
	}

	// Trigger should 410
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{})
	if w.Code != http.StatusGone {
		t.Fatalf("inactive trigger: want 410, got %d: %s", w.Code, w.Body.String())
	}
}

// ═══════════════════════════════════════════════
// Action Task Tests (v0.28.0)
// ═══════════════════════════════════════════════

func TestTask_ActionTaskAdminAllowed(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("actionadmin", "action@test.com")

	// Admin can always create action tasks
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":      "Action Relay",
		"task_type": "action",
		"schedule":  "webhook",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create action task: want 201, got %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	if task["task_type"].(string) != "action" {
		t.Fatalf("task_type should be action, got %q", task["task_type"])
	}
}

// ═══════════════════════════════════════════════
// Admin List All Tasks
// ═══════════════════════════════════════════════

func TestTask_AdminListAll(t *testing.T) {
	h := setupHarness(t)
	_, adminToken := h.createAdminUser("tadmin", "tadmin@test.com")

	// Create 2 tasks
	for i := 0; i < 2; i++ {
		h.request("POST", "/api/v1/tasks", adminToken, map[string]interface{}{
			"name": fmt.Sprintf("Admin Task %d", i), "task_type": "prompt",
			"schedule": "@daily", "user_prompt": "x", "model_id": "m",
		})
	}

	// Admin list
	w := h.request("GET", "/api/v1/admin/tasks", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("admin list: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	decode(w, &resp)
	data := resp["data"].([]interface{})
	if len(data) < 2 {
		t.Fatalf("admin list: expected at least 2 tasks, got %d", len(data))
	}
}

// ═══════════════════════════════════════════════
// Webhook Secret / Output Channel (Audit C2, C3)
// ═══════════════════════════════════════════════

func TestTask_WebhookSecretGenerated(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("secretuser", "secret@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Webhook Out", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
		"webhook_url": "https://example.com/hook",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Verify webhook_secret was auto-generated
	secret, ok := task["webhook_secret"].(string)
	if !ok || secret == "" {
		t.Fatal("webhook_secret should be auto-generated when webhook_url is set")
	}

	// Verify it persists on GET (C2 fix)
	w = h.request("GET", "/api/v1/tasks/"+taskID, token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("get: %d", w.Code)
	}
	var got map[string]interface{}
	decode(w, &got)
	gotSecret, ok := got["webhook_secret"].(string)
	if !ok || gotSecret != secret {
		t.Fatalf("webhook_secret not persisted on read: got %q, want %q", gotSecret, secret)
	}
}

// ═══════════════════════════════════════════════
// Once-shot schedule
// ═══════════════════════════════════════════════

func TestTask_OnceSchedule(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("onceuser", "once@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "One Shot", "task_type": "prompt", "schedule": "once",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)

	// Should have next_run_at set (approximately now)
	if task["next_run_at"] == nil {
		t.Fatal("once task should have next_run_at set to ~now")
	}
}

// ═══════════════════════════════════════════════
// Budget defaults applied
// ═══════════════════════════════════════════════

func TestTask_DefaultBudgets(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("budget", "budget@test.com")

	// Create with zero budgets — should get defaults
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Defaults", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d: %s", w.Code, w.Body.String())
	}
	var task map[string]interface{}
	decode(w, &task)

	maxTokens := int(task["max_tokens"].(float64))
	maxToolCalls := int(task["max_tool_calls"].(float64))
	maxWall := int(task["max_wall_clock"].(float64))

	if maxTokens != 4096 {
		t.Fatalf("max_tokens: want 4096, got %d", maxTokens)
	}
	if maxToolCalls != 10 {
		t.Fatalf("max_tool_calls: want 10, got %d", maxToolCalls)
	}
	if maxWall != 300 {
		t.Fatalf("max_wall_clock: want 300, got %d", maxWall)
	}
}

// ═══════════════════════════════════════════════
// F2: Workflow task_type rejected at API boundary
// ═══════════════════════════════════════════════

func TestTask_WorkflowTypeRejected(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("wfuser", "wfuser@test.com")

	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":        "Workflow Task",
		"task_type":   "workflow",
		"schedule":    "@daily",
		"workflow_id":  "00000000-0000-0000-0000-000000000001",
		"model_id":    "m",
	})
	if w.Code != http.StatusBadRequest {
		t.Fatalf("workflow task_type: want 400, got %d: %s", w.Code, w.Body.String())
	}
	// Verify error message mentions "not yet implemented"
	body := w.Body.String()
	if !strings.Contains(body, "not yet implemented") {
		t.Fatalf("expected 'not yet implemented' in error, got: %s", body)
	}
}

// ═══════════════════════════════════════════════
// Task Type RBAC (v0.28.7)
// ═══════════════════════════════════════════════

func TestTask_StarlarkTypeRequiresPackageID(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("staruser", "staruser@test.com")

	// Starlark tasks require system_function (package_id)
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name":        "Starlark Task",
		"task_type":   "starlark",
		"schedule":    "@daily",
		"user_prompt": "x",
		"model_id":    "m",
	})
	if w.Code != http.StatusBadRequest {
		t.Fatalf("starlark task_type without package_id: want 400, got %d: %s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, "system_function") && !strings.Contains(body, "package_id") {
		t.Fatalf("expected 'system_function' or 'package_id' in error, got: %s", body)
	}
}

func TestTask_SystemTypeNonAdminDenied(t *testing.T) {
	h := setupHarness(t)

	// Seed Everyone group with task.create so the route middleware passes
	database.TestDB.Exec(dialectSQL(
		`INSERT INTO groups (id, name, source, permissions) VALUES ($1, $2, $3, $4)
		 ON CONFLICT (id) DO UPDATE SET permissions = $4`),
		"00000000-0000-0000-0000-000000000001", "Everyone", "system", `["task.create"]`,
	)

	// Enable registration so registerUser works
	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")

	_, userToken := h.registerUser("sysuser", "sysuser@test.com", "password123!")

	w := h.request("POST", "/api/v1/tasks", userToken, map[string]interface{}{
		"name":            "System Task",
		"task_type":       "system",
		"system_function": "session_cleanup",
		"schedule":        "@daily",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("system task as non-admin: want 403, got %d: %s", w.Code, w.Body.String())
	}
}

func TestTask_ActionTypeNonAdminDenied(t *testing.T) {
	h := setupHarness(t)

	// Seed Everyone group with task.create but NOT task.action
	database.TestDB.Exec(dialectSQL(
		`INSERT INTO groups (id, name, source, permissions) VALUES ($1, $2, $3, $4)
		 ON CONFLICT (id) DO UPDATE SET permissions = $4`),
		"00000000-0000-0000-0000-000000000001", "Everyone", "system", `["task.create"]`,
	)

	// Enable registration so registerUser works
	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")

	_, userToken := h.registerUser("actuser", "actuser@test.com", "password123!")

	w := h.request("POST", "/api/v1/tasks", userToken, map[string]interface{}{
		"name":      "Action Task",
		"task_type": "action",
		"schedule":  "webhook",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("action task without task.action: want 403, got %d: %s", w.Code, w.Body.String())
	}
}

// ═══════════════════════════════════════════════
// F8: Team Task Routes
// ═══════════════════════════════════════════════

func TestTask_TeamTaskCRUD(t *testing.T) {
	h := setupHarness(t)
	adminID, adminToken := h.createAdminUser("teamadm", "teamadm@test.com")

	// Create a team (admin users bypass RequireTeamAdmin middleware)
	teamID := seedInsertReturningID(t,
		`INSERT INTO teams (name, description, created_by) VALUES ($1, $2, $3) RETURNING id`,
		"Task Team", "For team task tests", adminID,
	)

	// Create team task via team route
	w := h.request("POST", "/api/v1/teams/"+teamID+"/tasks", adminToken, map[string]interface{}{
		"name":        "Team Task",
		"task_type":   "prompt",
		"schedule":    "@daily",
		"user_prompt": "team work",
		"model_id":    "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create team task: want 201, got %d: %s", w.Code, w.Body.String())
	}

	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Verify scope was injected as "team"
	if task["scope"].(string) != "team" {
		t.Fatalf("team task scope: want 'team', got %q", task["scope"])
	}
	if task["team_id"] == nil || task["team_id"].(string) != teamID {
		t.Fatalf("team task team_id: want %q, got %v", teamID, task["team_id"])
	}

	// List team tasks (member route — admin bypasses membership check)
	w = h.request("GET", "/api/v1/teams/"+teamID+"/tasks", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list team tasks: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var listResp map[string]interface{}
	decode(w, &listResp)
	data := listResp["data"].([]interface{})
	if len(data) != 1 {
		t.Fatalf("expected 1 team task, got %d", len(data))
	}

	// Update via team route
	w = h.request("PUT", "/api/v1/teams/"+teamID+"/tasks/"+taskID, adminToken, map[string]interface{}{
		"name": "Updated Team Task",
	})
	if w.Code != http.StatusOK {
		t.Fatalf("update team task: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var updated map[string]interface{}
	decode(w, &updated)
	if updated["name"].(string) != "Updated Team Task" {
		t.Fatalf("name not updated: got %q", updated["name"])
	}

	// List runs via team member route
	w = h.request("GET", "/api/v1/teams/"+teamID+"/tasks/"+taskID+"/runs", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list team task runs: want 200, got %d: %s", w.Code, w.Body.String())
	}

	// Delete via team route
	w = h.request("DELETE", "/api/v1/teams/"+teamID+"/tasks/"+taskID, adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("delete team task: want 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify gone
	w = h.request("GET", "/api/v1/teams/"+teamID+"/tasks", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list after delete: want 200, got %d", w.Code)
	}
	decode(w, &listResp)
	data = listResp["data"].([]interface{})
	if len(data) != 0 {
		t.Fatalf("expected 0 team tasks after delete, got %d", len(data))
	}
}

// ═══════════════════════════════════════════════
// F9: Admin Run / Kill / Delete
// ═══════════════════════════════════════════════

func TestTask_AdminRunNow(t *testing.T) {
	h := setupHarness(t)
	_, adminToken := h.createAdminUser("admrun", "admrun@test.com")

	// Create a task
	w := h.request("POST", "/api/v1/tasks", adminToken, map[string]interface{}{
		"name": "Admin Runnable", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "go", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Admin run via admin route
	w = h.request("POST", "/api/v1/admin/tasks/"+taskID+"/run", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("admin run: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var runResp map[string]interface{}
	decode(w, &runResp)
	if runResp["scheduled"] != true {
		t.Fatal("expected scheduled: true")
	}
}

func TestTask_AdminKillRun(t *testing.T) {
	h := setupHarness(t)
	_, adminToken := h.createAdminUser("admkill", "admkill@test.com")

	// Create a webhook task and trigger it to get a queued run
	w := h.request("POST", "/api/v1/tasks", adminToken, map[string]interface{}{
		"name": "Killable", "task_type": "prompt", "schedule": "webhook",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)
	triggerToken := task["trigger_token"].(string)

	// Trigger to create a queued run
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{"test": true})
	if w.Code != http.StatusAccepted {
		t.Fatalf("trigger: %d", w.Code)
	}
	var trigResp map[string]interface{}
	decode(w, &trigResp)
	runID := trigResp["run_id"].(string)

	// Manually transition the run to "running" so kill works
	database.TestDB.Exec(dialectSQL("UPDATE task_runs SET status = 'running' WHERE id = $1"), runID)

	// Admin kill
	w = h.request("POST", "/api/v1/admin/tasks/"+taskID+"/kill", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("admin kill: want 200, got %d: %s", w.Code, w.Body.String())
	}
	var killResp map[string]interface{}
	decode(w, &killResp)
	if killResp["killed"] != true {
		t.Fatal("expected killed: true")
	}
}

func TestTask_AdminDelete(t *testing.T) {
	h := setupHarness(t)
	_, adminToken := h.createAdminUser("admdel", "admdel@test.com")

	// Create a task
	w := h.request("POST", "/api/v1/tasks", adminToken, map[string]interface{}{
		"name": "Admin Deletable", "task_type": "prompt", "schedule": "@daily",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)

	// Admin delete
	w = h.request("DELETE", "/api/v1/admin/tasks/"+taskID, adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("admin delete: want 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify gone
	w = h.request("GET", "/api/v1/tasks/"+taskID, adminToken, nil)
	if w.Code != http.StatusNotFound {
		t.Fatalf("get deleted: want 404, got %d", w.Code)
	}
}

// ═══════════════════════════════════════════════
// F10: RunNow 409 Conflict (queued run exists)
// ═══════════════════════════════════════════════

func TestTask_RunNowConflictQueued(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("conflict", "conflict@test.com")

	// Create a webhook task
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Conflict Test", "task_type": "prompt", "schedule": "webhook",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)
	triggerToken := task["trigger_token"].(string)

	// Trigger to create a queued run
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{"test": true})
	if w.Code != http.StatusAccepted {
		t.Fatalf("trigger: %d", w.Code)
	}

	// RunNow should 409 because a queued run exists
	w = h.request("POST", "/api/v1/tasks/"+taskID+"/run", token, nil)
	if w.Code != http.StatusConflict {
		t.Fatalf("run-now with queued run: want 409, got %d: %s", w.Code, w.Body.String())
	}
}

func TestTask_RunNowConflictActive(t *testing.T) {
	h := setupHarness(t)
	_, token := h.createAdminUser("active", "active@test.com")

	// Create a webhook task
	w := h.request("POST", "/api/v1/tasks", token, map[string]interface{}{
		"name": "Active Conflict", "task_type": "prompt", "schedule": "webhook",
		"user_prompt": "x", "model_id": "m",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create: %d", w.Code)
	}
	var task map[string]interface{}
	decode(w, &task)
	taskID := task["id"].(string)
	triggerToken := task["trigger_token"].(string)

	// Trigger and promote to running
	w = h.request("POST", "/api/v1/hooks/t/"+triggerToken, "", map[string]interface{}{"test": true})
	if w.Code != http.StatusAccepted {
		t.Fatalf("trigger: %d", w.Code)
	}
	var trigResp map[string]interface{}
	decode(w, &trigResp)
	runID := trigResp["run_id"].(string)
	database.TestDB.Exec(dialectSQL("UPDATE task_runs SET status = 'running' WHERE id = $1"), runID)

	// RunNow should 409 because an active run exists
	w = h.request("POST", "/api/v1/tasks/"+taskID+"/run", token, nil)
	if w.Code != http.StatusConflict {
		t.Fatalf("run-now with active run: want 409, got %d: %s", w.Code, w.Body.String())
	}
}

