package handlers

// ══════════════════════════════════════════════════════════════════════════════
// Permission Enforcement Tests (v0.37.1)
//
// Verifies that RequirePermission middleware actually blocks non-admin users
// who lack the required permission, and allows them after the permission is
// granted via group membership.
//
// Every test:
//   1. Creates a non-admin user (no groups, TruncateAll wipes Everyone)
//   2. Attempts the gated operation → expects 403
//   3. Creates a group with the required permission
//   4. Adds the user to that group
//   5. Retries the operation → expects success (200 or 201)
//
// No admin users. No shortcuts.
// ══════════════════════════════════════════════════════════════════════════════

import (
	"net/http"
	"testing"

	"chat-switchboard/database"
)

// ── Helpers ─────────────────────────────────

// seedNonAdminUser creates a non-admin active user with a valid token.
// TruncateAll wipes the Everyone group so this user has ZERO permissions.
func seedNonAdminUser(t *testing.T) (userID, token string) {
	t.Helper()
	userID = database.SeedTestUser(t, "permuser", "permuser@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
	token = makeToken(userID, "permuser@test.com", "user")
	return
}

// seedAdminUser creates an admin user for setup operations (group creation).
func seedAdminUser(t *testing.T) (userID, token string) {
	t.Helper()
	userID = database.SeedTestUser(t, "permadmin", "permadmin@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET role = 'admin', is_active = true WHERE id = $1"), userID)
	token = makeToken(userID, "permadmin@test.com", "admin")
	return
}

// grantPermission creates a group with the given permission and adds the user to it.
// Uses admin token for group creation, returns the group ID.
func grantPermission(t *testing.T, h *testHarness, adminToken, userID string, perm string) string {
	t.Helper()
	w := h.request("POST", "/api/v1/admin/groups", adminToken, map[string]interface{}{
		"name":        "grant-" + perm,
		"scope":       "global",
		"permissions": []string{perm},
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create group for %s: want 201, got %d: %s", perm, w.Code, w.Body.String())
	}
	var group map[string]interface{}
	decode(w, &group)
	groupID := group["id"].(string)

	w = h.request("POST", "/api/v1/admin/groups/"+groupID+"/members", adminToken, map[string]interface{}{
		"user_id": userID,
	})
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("add member to group %s: want 200/201, got %d: %s", perm, w.Code, w.Body.String())
	}
	return groupID
}

// seedChannel creates a channel using the admin token and returns its ID.
func seedChannel(t *testing.T, h *testHarness, adminToken string) string {
	t.Helper()
	w := h.request("POST", "/api/v1/channels", adminToken, map[string]interface{}{
		"title": "test-channel",
		"type":  "direct",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("seed channel: want 201, got %d: %s", w.Code, w.Body.String())
	}
	var ch map[string]interface{}
	decode(w, &ch)
	return ch["id"].(string)
}

// ══════════════════════════════════════════════
// §1  model.use — completions, summarize, title
// ══════════════════════════════════════════════

func TestPermEnforce_ModelUse_Completions(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	adminID, adminToken := seedAdminUser(t)
	_ = adminID

	// User needs channel.create to make a channel for completions,
	// so grant that first (otherwise 403 on channel create, not completions).
	grantPermission(t, h, adminToken, userID, "channel.create")

	// Create channel as user (now allowed)
	w := h.request("POST", "/api/v1/channels", userToken, map[string]interface{}{
		"title": "perm-test", "type": "direct",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("create channel: want 201, got %d: %s", w.Code, w.Body.String())
	}
	var ch map[string]interface{}
	decode(w, &ch)
	channelID := ch["id"].(string)

	// Completions without model.use → 403
	w = h.request("POST", "/api/v1/chat/completions", userToken, map[string]interface{}{
		"channel_id": channelID,
		"message":    "hello",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("completions without model.use: want 403, got %d: %s", w.Code, w.Body.String())
	}

	// Grant model.use
	grantPermission(t, h, adminToken, userID, "model.use")

	// Completions with model.use → should pass middleware (may fail downstream
	// due to no provider configured, but NOT 403)
	w = h.request("POST", "/api/v1/chat/completions", userToken, map[string]interface{}{
		"channel_id": channelID,
		"message":    "hello",
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("completions WITH model.use: still got 403: %s", w.Body.String())
	}
	// Accept 400/500/502 — anything except 403 means the permission gate passed
}

// NOTE: Summarize and GenerateTitle share the model.use permission with
// completions. Their route-level RequirePermission wiring is in main.go
// but the test harness (setupHarness) doesn't register those routes.
// The completions test above proves model.use enforcement works.
// Summarize/title coverage will come when the test harness is extended.

// ══════════════════════════════════════════════
// §2  channel.create
// ══════════════════════════════════════════════

func TestPermEnforce_ChannelCreate(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Without channel.create → 403
	w := h.request("POST", "/api/v1/channels", userToken, map[string]interface{}{
		"title": "blocked", "type": "direct",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("channel create without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "channel.create")

	w = h.request("POST", "/api/v1/channels", userToken, map[string]interface{}{
		"title": "allowed", "type": "direct",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("channel create WITH permission: want 201, got %d: %s", w.Code, w.Body.String())
	}
}

// ══════════════════════════════════════════════
// §3  channel.invite
// ══════════════════════════════════════════════

func TestPermEnforce_ChannelInvite(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Need channel.create first to have a channel to invite into
	grantPermission(t, h, adminToken, userID, "channel.create")
	w := h.request("POST", "/api/v1/channels", userToken, map[string]interface{}{
		"title": "invite-test", "type": "group",
	})
	var ch map[string]interface{}
	decode(w, &ch)
	channelID := ch["id"].(string)

	// Create another user to invite
	inviteeID := database.SeedTestUser(t, "invitee", "invitee@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), inviteeID)

	// Without channel.invite → 403
	w = h.request("POST", "/api/v1/channels/"+channelID+"/participants", userToken, map[string]interface{}{
		"user_id": inviteeID,
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("invite without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "channel.invite")

	w = h.request("POST", "/api/v1/channels/"+channelID+"/participants", userToken, map[string]interface{}{
		"user_id": inviteeID,
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("invite WITH permission: still got 403: %s", w.Body.String())
	}
}

// ══════════════════════════════════════════════
// §4  kb.create, kb.read, kb.write
// ══════════════════════════════════════════════

func TestPermEnforce_KBCreate(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Without kb.create → 403
	w := h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
		"name": "blocked-kb",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("kb create without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "kb.create")

	w = h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
		"name": "allowed-kb",
	})
	if w.Code != http.StatusCreated {
		t.Fatalf("kb create WITH permission: want 201, got %d: %s", w.Code, w.Body.String())
	}
}

func TestPermEnforce_KBSearch(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Admin creates a KB for the user to search
	grantPermission(t, h, adminToken, userID, "kb.create")
	w := h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
		"name": "search-test-kb",
	})
	var kb map[string]interface{}
	decode(w, &kb)
	kbID := kb["id"].(string)

	// Without kb.read → 403
	w = h.request("POST", "/api/v1/knowledge-bases/"+kbID+"/search", userToken, map[string]interface{}{
		"query": "test",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("kb search without kb.read: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "kb.read")

	// With kb.read the middleware should pass. The handler may panic or 500
	// because the test harness has no embedder configured, but any non-403
	// response proves the permission gate opened. Use a recovery wrapper
	// to catch the nil-embedder panic gracefully.
	func() {
		defer func() {
			if r := recover(); r != nil {
				// Panic in handler (nil embedder) means middleware passed — success
			}
		}()
		w = h.request("POST", "/api/v1/knowledge-bases/"+kbID+"/search", userToken, map[string]interface{}{
			"query": "test",
		})
		if w.Code == http.StatusForbidden {
			t.Fatalf("kb search WITH kb.read: still got 403: %s", w.Body.String())
		}
	}()
}

func TestPermEnforce_KBWrite(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Grant kb.create so user can create the KB
	grantPermission(t, h, adminToken, userID, "kb.create")
	w := h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
		"name": "write-test-kb",
	})
	var kb map[string]interface{}
	decode(w, &kb)
	kbID := kb["id"].(string)

	// Without kb.write → 403 on update
	w = h.request("PUT", "/api/v1/knowledge-bases/"+kbID, userToken, map[string]interface{}{
		"name": "renamed",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("kb update without kb.write: want 403, got %d: %s", w.Code, w.Body.String())
	}

	// Without kb.write → 403 on delete
	w = h.request("DELETE", "/api/v1/knowledge-bases/"+kbID, userToken, nil)
	if w.Code != http.StatusForbidden {
		t.Fatalf("kb delete without kb.write: want 403, got %d: %s", w.Code, w.Body.String())
	}

	// Without kb.write → 403 on rebuild
	w = h.request("POST", "/api/v1/knowledge-bases/"+kbID+"/rebuild", userToken, nil)
	if w.Code != http.StatusForbidden {
		t.Fatalf("kb rebuild without kb.write: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "kb.write")

	// With kb.write → should pass middleware
	w = h.request("PUT", "/api/v1/knowledge-bases/"+kbID, userToken, map[string]interface{}{
		"name": "renamed-ok",
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("kb update WITH kb.write: still got 403: %s", w.Body.String())
	}
}

// ══════════════════════════════════════════════
// §5  persona.create, persona.manage
// ══════════════════════════════════════════════

func TestPermEnforce_PersonaCreate(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Enable user personas policy
	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_personas', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")

	// Without persona.create → 403
	w := h.request("POST", "/api/v1/personas", userToken, map[string]interface{}{
		"name": "blocked-persona",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("persona create without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "persona.create")

	w = h.request("POST", "/api/v1/personas", userToken, map[string]interface{}{
		"name": "allowed-persona",
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("persona create WITH permission: still got 403: %s", w.Body.String())
	}
}

func TestPermEnforce_PersonaManage(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_personas', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")

	// Grant create so user can make a persona
	grantPermission(t, h, adminToken, userID, "persona.create")
	w := h.request("POST", "/api/v1/personas", userToken, map[string]interface{}{
		"name": "manage-test",
	})
	var persona map[string]interface{}
	decode(w, &persona)
	personaID := persona["id"].(string)

	// Without persona.manage → 403 on update
	w = h.request("PUT", "/api/v1/personas/"+personaID, userToken, map[string]interface{}{
		"name": "updated",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("persona update without persona.manage: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "persona.manage")

	w = h.request("PUT", "/api/v1/personas/"+personaID, userToken, map[string]interface{}{
		"name": "updated-ok",
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("persona update WITH persona.manage: still got 403: %s", w.Body.String())
	}
}

// ══════════════════════════════════════════════
// §6  workflow.create
// ══════════════════════════════════════════════

func TestPermEnforce_WorkflowCreate(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Without workflow.create → 403
	w := h.request("POST", "/api/v1/workflows", userToken, map[string]interface{}{
		"name": "blocked-wf",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("workflow create without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "workflow.create")

	w = h.request("POST", "/api/v1/workflows", userToken, map[string]interface{}{
		"name": "allowed-wf",
	})
	if w.Code == http.StatusForbidden {
		t.Fatalf("workflow create WITH permission: still got 403: %s", w.Body.String())
	}
}

// ══════════════════════════════════════════════
// §7  task.create
// ══════════════════════════════════════════════

func TestPermEnforce_TaskCreate(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	// Without task.create → 403
	w := h.request("POST", "/api/v1/tasks", userToken, map[string]interface{}{
		"name":        "blocked-task",
		"task_type":   "prompt",
		"schedule":    "@daily",
		"user_prompt": "test",
		"model_id":    "test-model",
	})
	if w.Code != http.StatusForbidden {
		t.Fatalf("task create without permission: want 403, got %d: %s", w.Code, w.Body.String())
	}

	grantPermission(t, h, adminToken, userID, "task.create")

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

// ══════════════════════════════════════════════
// §8  GET /profile/permissions (new endpoint)
// ══════════════════════════════════════════════

func TestPermEnforce_ProfilePermissions_NoGroups(t *testing.T) {
	h := setupHarness(t)
	_, userToken := seedNonAdminUser(t)

	// User with no groups, no Everyone group → empty permissions
	w := h.request("GET", "/api/v1/profile/permissions", userToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("profile/permissions: want 200, got %d: %s", w.Code, w.Body.String())
	}

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

	perms, ok := resp["permissions"].([]interface{})
	if !ok {
		t.Fatal("response must have 'permissions' array")
	}
	if len(perms) != 0 {
		t.Fatalf("expected 0 permissions (no groups), got %d: %v", len(perms), perms)
	}

	groups, ok := resp["groups"].([]interface{})
	if !ok {
		t.Fatal("response must have 'groups' array")
	}
	// Should still include Everyone group ID (even though it doesn't exist after truncate)
	if len(groups) == 0 {
		t.Fatal("groups should include Everyone group ID")
	}
}

func TestPermEnforce_ProfilePermissions_WithGrant(t *testing.T) {
	h := setupHarness(t)
	userID, userToken := seedNonAdminUser(t)
	_, adminToken := seedAdminUser(t)

	grantPermission(t, h, adminToken, userID, "channel.create")
	grantPermission(t, h, adminToken, userID, "model.use")

	w := h.request("GET", "/api/v1/profile/permissions", userToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("profile/permissions: want 200, got %d: %s", w.Code, w.Body.String())
	}

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

	perms, _ := resp["permissions"].([]interface{})
	permSet := make(map[string]bool)
	for _, p := range perms {
		permSet[p.(string)] = true
	}
	if !permSet["channel.create"] {
		t.Fatal("expected channel.create in permissions")
	}
	if !permSet["model.use"] {
		t.Fatal("expected model.use in permissions")
	}
	if permSet["task.create"] {
		t.Fatal("task.create should NOT be in permissions (not granted)")
	}
}

func TestPermEnforce_ProfilePermissions_Admin(t *testing.T) {
	h := setupHarness(t)
	_, adminToken := seedAdminUser(t)

	w := h.request("GET", "/api/v1/profile/permissions", adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("profile/permissions admin: want 200, got %d: %s", w.Code, w.Body.String())
	}

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

	perms, _ := resp["permissions"].([]interface{})
	// Admin should get ALL permissions
	if len(perms) != len(allPermissionConstants()) {
		t.Fatalf("admin should have all %d permissions, got %d", len(allPermissionConstants()), len(perms))
	}
}

// allPermissionConstants returns the count of auth.AllPermissions.
// Duplicated here to avoid import cycle issues in test assertions.
func allPermissionConstants() []string {
	return []string{
		"model.use", "model.select_any",
		"kb.read", "kb.write", "kb.create",
		"channel.create", "channel.invite",
		"persona.create", "persona.manage",
		"workflow.create",
		"admin.view",
		"token.unlimited",
		"task.create", "task.admin", "task.action", "task.starlark",
	}
}
