package handlers

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

	"github.com/gin-gonic/gin"

	"chat-switchboard/config"
	authpkg "chat-switchboard/auth"
	"chat-switchboard/database"
	"chat-switchboard/middleware"
	"chat-switchboard/store"
	postgres "chat-switchboard/store/postgres"
	sqlite "chat-switchboard/store/sqlite"
)

// ═══════════════════════════════════════════════════════════
// Extended harness: CRUD + instances + assignments + entry
// ═══════════════════════════════════════════════════════════

type workflowInstanceHarness struct {
	*testHarness
	stores     store.Stores
	adminToken string
	adminID    string
}

func setupWorkflowInstanceHarness(t *testing.T) *workflowInstanceHarness {
	t.Helper()
	database.RequireTestDB(t)
	database.TruncateAll(t)

	cfg := &config.Config{
		JWTSecret: testJWTSecret,
		BasePath:  "",
	}

	var stores store.Stores
	if database.IsSQLite() {
		stores = sqlite.NewStores(database.TestDB)
	} else {
		stores = postgres.NewStores(database.TestDB)
	}
	userCache := middleware.NewUserStatusCache()

	r := gin.New()
	api := r.Group("/api/v1")

	auth := NewAuthHandler(cfg, stores, nil, authpkg.NewBuiltinProvider())
	api.POST("/auth/login", auth.Login)
	api.POST("/auth/register", auth.Register)

	protected := api.Group("")
	protected.Use(middleware.Auth(cfg, stores.Users, userCache))

	// Workflow CRUD
	wfH := NewWorkflowHandler(stores)
	protected.GET("/workflows", wfH.List)
	protected.POST("/workflows", wfH.Create)
	protected.GET("/workflows/:id", wfH.Get)
	protected.PATCH("/workflows/:id", wfH.Update)
	protected.DELETE("/workflows/:id", wfH.Delete)
	protected.GET("/workflows/:id/stages", wfH.ListStages)
	protected.POST("/workflows/:id/stages", wfH.CreateStage)
	protected.PUT("/workflows/:id/stages/:sid", wfH.UpdateStage)
	protected.DELETE("/workflows/:id/stages/:sid", wfH.DeleteStage)
	protected.PATCH("/workflows/:id/stages/reorder", wfH.ReorderStages)
	protected.POST("/workflows/:id/publish", wfH.Publish)
	protected.GET("/workflows/:id/versions/:version", wfH.GetVersion)

	// Workflow instances
	wfInstH := NewWorkflowInstanceHandler(stores, nil, nil, nil)
	protected.POST("/workflows/:id/start", wfInstH.Start)
	protected.GET("/channels/:id/workflow/status", wfInstH.GetStatus)
	protected.POST("/channels/:id/workflow/advance", wfInstH.Advance)
	protected.POST("/channels/:id/workflow/reject", wfInstH.Reject)

	// Assignments
	wfAssignH := NewWorkflowAssignmentHandler(stores)
	protected.GET("/workflow-assignments/mine", wfAssignH.ListMine)
	protected.POST("/workflow-assignments/:id/claim", wfAssignH.Claim)
	protected.POST("/workflow-assignments/:id/complete", wfAssignH.Complete)

	// Teams (admin routes for creating teams + adding members)
	teamH := NewTeamHandler(stores, nil)
	admin := api.Group("/admin")
	admin.Use(middleware.Auth(cfg, stores.Users, userCache), middleware.RequireAdmin())
	admin.POST("/teams", teamH.CreateTeam)
	admin.POST("/teams/:id/members", teamH.AddMember)

	// Team-scoped assignment listing (mirrors main.go teamScoped)
	teamScoped := protected.Group("/teams/:teamId")
	teamAssignH := NewWorkflowAssignmentHandler(stores)
	teamScoped.GET("/assignments", teamAssignH.ListForTeam)

	// Visitor entry
	wfEntry := NewWorkflowEntryHandler(stores)
	r.POST("/api/v1/workflow-entry/:scope/:slug", wfEntry.StartVisitor)

	// Seed admin user
	adminID := seedInsertReturningID(t,
		`INSERT INTO users (username, email, password_hash, role, handle, auth_source) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
		"wfinst-admin", "wfinst-admin@test.com", "$2a$10$test", "admin", "wfinst-admin", "builtin",
	)
	token := makeToken(adminID, "wfinst-admin@test.com", "admin")

	return &workflowInstanceHarness{
		testHarness: &testHarness{router: r, t: t},
		stores:      stores,
		adminToken:  token,
		adminID:     adminID,
	}
}

// createPublishedWorkflow is a helper that creates a workflow with N stages,
// activates it, and publishes it. Returns workflow ID + stage IDs.
func (h *workflowInstanceHarness) createPublishedWorkflow(name string, numStages int) (string, []string) {
	h.t.Helper()

	resp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name":       name,
		"entry_mode": "public_link",
	})
	if resp.Code != http.StatusCreated {
		h.t.Fatalf("create workflow: %d: %s", resp.Code, resp.Body.String())
	}
	var wf struct{ ID string `json:"id"` }
	json.Unmarshal(resp.Body.Bytes(), &wf)

	stageIDs := make([]string, numStages)
	for i := 0; i < numStages; i++ {
		resp = h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
			"name":         fmt.Sprintf("Stage %d", i),
			"ordinal":      i,
			"history_mode": "full",
		})
		if resp.Code != http.StatusCreated {
			h.t.Fatalf("create stage %d: %d: %s", i, resp.Code, resp.Body.String())
		}
		var st struct{ ID string `json:"id"` }
		json.Unmarshal(resp.Body.Bytes(), &st)
		stageIDs[i] = st.ID
	}

	// Activate + publish
	h.request("PATCH", "/api/v1/workflows/"+wf.ID, h.adminToken, map[string]interface{}{
		"is_active": true,
	})
	resp = h.request("POST", "/api/v1/workflows/"+wf.ID+"/publish", h.adminToken, nil)
	if resp.Code != http.StatusCreated {
		h.t.Fatalf("publish: %d: %s", resp.Code, resp.Body.String())
	}

	return wf.ID, stageIDs
}

// ═══════════════════════════════════════════════════════════
// #15 — Instance Lifecycle (currently untested)
// ═══════════════════════════════════════════════════════════

// TestWorkflowInstanceLifecycle exercises: start → status → advance → complete.
func TestWorkflowInstanceLifecycle(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Lifecycle Test", 2)

	// ── Start ───────────────────────────────
	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	if resp.Code != http.StatusCreated {
		t.Fatalf("Start: got %d, body: %s", resp.Code, resp.Body.String())
	}
	var startResp struct {
		ChannelID       string `json:"channel_id"`
		WorkflowVersion int    `json:"workflow_version"`
		CurrentStage    int    `json:"current_stage"`
	}
	json.Unmarshal(resp.Body.Bytes(), &startResp)
	if startResp.ChannelID == "" {
		t.Fatal("Start: channel_id empty")
	}
	if startResp.CurrentStage != 0 {
		t.Errorf("Start: current_stage = %d, want 0", startResp.CurrentStage)
	}
	chID := startResp.ChannelID

	// ── Status ──────────────────────────────
	resp = h.request("GET", "/api/v1/channels/"+chID+"/workflow/status", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("Status: got %d, body: %s", resp.Code, resp.Body.String())
	}
	var status struct {
		WorkflowID *string         `json:"workflow_id"`
		Status     string          `json:"status"`
		Stage      int             `json:"current_stage"`
		StageData  json.RawMessage `json:"stage_data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &status)
	if status.Status != "active" {
		t.Errorf("Status: got %q, want \"active\"", status.Status)
	}
	if status.WorkflowID == nil || *status.WorkflowID != wfID {
		t.Errorf("Status: workflow_id mismatch")
	}

	// ── Advance (stage 0 → 1) ───────────────
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"name": "Jane", "email": "jane@test.com"},
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("Advance 0→1: got %d, body: %s", resp.Code, resp.Body.String())
	}
	var advResp struct {
		Status       string `json:"status"`
		CurrentStage int    `json:"current_stage"`
	}
	json.Unmarshal(resp.Body.Bytes(), &advResp)
	if advResp.Status != "active" {
		t.Errorf("Advance 0→1: status = %q, want \"active\"", advResp.Status)
	}
	if advResp.CurrentStage != 1 {
		t.Errorf("Advance 0→1: current_stage = %d, want 1", advResp.CurrentStage)
	}

	// ── Verify merged data persisted ────────
	resp = h.request("GET", "/api/v1/channels/"+chID+"/workflow/status", h.adminToken, nil)
	json.Unmarshal(resp.Body.Bytes(), &status)
	var data map[string]string
	json.Unmarshal(status.StageData, &data)
	if data["name"] != "Jane" || data["email"] != "jane@test.com" {
		t.Errorf("Stage data after advance: got %v", data)
	}

	// ── Advance (stage 1 → complete) ────────
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"resolution": "approved"},
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("Advance 1→complete: got %d, body: %s", resp.Code, resp.Body.String())
	}
	json.Unmarshal(resp.Body.Bytes(), &advResp)
	if advResp.Status != "completed" {
		t.Errorf("Completion: status = %q, want \"completed\"", advResp.Status)
	}

	// ── Status after completion ──────────────
	resp = h.request("GET", "/api/v1/channels/"+chID+"/workflow/status", h.adminToken, nil)
	json.Unmarshal(resp.Body.Bytes(), &status)
	if status.Status != "completed" {
		t.Errorf("Post-complete status: got %q, want \"completed\"", status.Status)
	}

	// ── Advance on completed should fail ────
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"extra": "data"},
	})
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Advance after complete: got %d, want 400", resp.Code)
	}
}

// TestWorkflowInstanceStart_InactiveWorkflow verifies start fails on inactive workflow.
func TestWorkflowInstanceStart_InactiveWorkflow(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	// Create workflow with a stage but don't activate
	resp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name": "Inactive WF",
	})
	var wf struct{ ID string `json:"id"` }
	json.Unmarshal(resp.Body.Bytes(), &wf)

	h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
		"name": "Stage 0", "ordinal": 0, "history_mode": "full",
	})

	resp = h.request("POST", "/api/v1/workflows/"+wf.ID+"/start", h.adminToken, nil)
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Start inactive: got %d, want 400", resp.Code)
	}
}

// TestWorkflowInstanceStart_NoPublishedVersion verifies start fails without a published version.
func TestWorkflowInstanceStart_NoPublishedVersion(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	resp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name": "Unpublished WF",
	})
	var wf struct{ ID string `json:"id"` }
	json.Unmarshal(resp.Body.Bytes(), &wf)

	h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
		"name": "Stage 0", "ordinal": 0, "history_mode": "full",
	})
	// Activate but don't publish
	h.request("PATCH", "/api/v1/workflows/"+wf.ID, h.adminToken, map[string]interface{}{
		"is_active": true,
	})

	resp = h.request("POST", "/api/v1/workflows/"+wf.ID+"/start", h.adminToken, nil)
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Start unpublished: got %d, want 400", resp.Code)
	}
}

// ═══════════════════════════════════════════════════════════
// #15 — Reject validation
// ═══════════════════════════════════════════════════════════

// TestWorkflowReject exercises reject validation: reason required, stage-0 guard.
func TestWorkflowReject(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Reject Test", 3)

	// Start
	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)
	chID := start.ChannelID

	// ── Reject from stage 0 should fail ─────
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/reject", h.adminToken, map[string]interface{}{
		"reason": "want to go back",
	})
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Reject at stage 0: got %d, want 400", resp.Code)
	}

	// ── Reject without reason should fail ───
	// Advance to stage 1 first
	h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"step": "0"},
	})

	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/reject", h.adminToken, nil)
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Reject no reason: got %d, want 400", resp.Code)
	}

	// ── Reject with empty reason should fail ─
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/reject", h.adminToken, map[string]interface{}{
		"reason": "",
	})
	if resp.Code != http.StatusBadRequest {
		t.Errorf("Reject empty reason: got %d, want 400", resp.Code)
	}

	// ── Valid reject ────────────────────────
	resp = h.request("POST", "/api/v1/channels/"+chID+"/workflow/reject", h.adminToken, map[string]interface{}{
		"reason": "Missing email field",
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("Valid reject: got %d, body: %s", resp.Code, resp.Body.String())
	}
	var rejResp struct {
		CurrentStage int    `json:"current_stage"`
		Reason       string `json:"reason"`
	}
	json.Unmarshal(resp.Body.Bytes(), &rejResp)
	if rejResp.CurrentStage != 0 {
		t.Errorf("After reject: current_stage = %d, want 0", rejResp.CurrentStage)
	}
	if rejResp.Reason != "Missing email field" {
		t.Errorf("Reject reason: got %q", rejResp.Reason)
	}
}

// ═══════════════════════════════════════════════════════════
// #1 — Advance body field is "data" (not "stage_data")
// ═══════════════════════════════════════════════════════════

// TestWorkflowAdvance_BodyFieldName verifies the advance endpoint reads "data"
// from the request body, not "stage_data" (which the old ICD incorrectly documented).
func TestWorkflowAdvance_BodyFieldName(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Field Name Test", 2)

	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)
	chID := start.ChannelID

	// Send data under the correct "data" key
	h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"collected": "yes"},
	})

	// Verify data was merged
	resp = h.request("GET", "/api/v1/channels/"+chID+"/workflow/status", h.adminToken, nil)
	var status struct{ StageData json.RawMessage `json:"stage_data"` }
	json.Unmarshal(resp.Body.Bytes(), &status)
	var data map[string]string
	json.Unmarshal(status.StageData, &data)
	if data["collected"] != "yes" {
		t.Errorf("Data sent under 'data' key not merged: got %v", data)
	}
}

// ═══════════════════════════════════════════════════════════
// #2 — GetStatus response uses "status" (not "workflow_status")
// ═══════════════════════════════════════════════════════════

// TestWorkflowGetStatus_ResponseShape verifies the exact JSON field names
// returned by the status endpoint.
func TestWorkflowGetStatus_ResponseShape(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Status Shape", 1)

	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)

	resp = h.request("GET", "/api/v1/channels/"+start.ChannelID+"/workflow/status", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("GetStatus: %d", resp.Code)
	}

	// Parse as raw map to check exact field names
	var raw map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &raw)

	if _, ok := raw["status"]; !ok {
		t.Error("Response missing 'status' field — should be 'status', not 'workflow_status'")
	}
	if _, ok := raw["workflow_status"]; ok {
		t.Error("Response has 'workflow_status' — should be 'status' per ICD")
	}
	if _, ok := raw["workflow_id"]; !ok {
		t.Error("Response missing 'workflow_id'")
	}
	if _, ok := raw["current_stage"]; !ok {
		t.Error("Response missing 'current_stage'")
	}
	if _, ok := raw["last_activity_at"]; !ok {
		t.Error("Response missing 'last_activity_at'")
	}
}

// ═══════════════════════════════════════════════════════════
// #4 — StartVisitor response shape
// ═══════════════════════════════════════════════════════════

// TestWorkflowVisitorEntry_ResponseShape verifies the visitor start endpoint
// returns the correct field names (session_id + redirect_to, not session_token).
func TestWorkflowVisitorEntry_ResponseShape(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Visitor Test", 1)

	// Read slug
	resp := h.request("GET", "/api/v1/workflows/"+wfID, h.adminToken, nil)
	var wf struct{ Slug string `json:"slug"` }
	json.Unmarshal(resp.Body.Bytes(), &wf)

	// Visitor start (no auth, no body)
	resp = h.request("POST", "/api/v1/workflow-entry/global/"+wf.Slug, "", nil)
	if resp.Code != http.StatusCreated {
		t.Fatalf("Visitor start: got %d, body: %s", resp.Code, resp.Body.String())
	}

	var raw map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &raw)

	if _, ok := raw["channel_id"]; !ok {
		t.Error("Response missing 'channel_id'")
	}
	if _, ok := raw["session_id"]; !ok {
		t.Error("Response missing 'session_id' — should be 'session_id', not 'session_token'")
	}
	if _, ok := raw["redirect_to"]; !ok {
		t.Error("Response missing 'redirect_to'")
	}
	// These should NOT be present
	if _, ok := raw["session_token"]; ok {
		t.Error("Response has 'session_token' — field was renamed to 'session_id'")
	}
	if _, ok := raw["workflow"]; ok {
		t.Error("Response has 'workflow' object — not in actual response")
	}
}

// TestWorkflowVisitorEntry_TeamOnlyBlocked verifies team_only workflows reject visitors.
func TestWorkflowVisitorEntry_TeamOnlyBlocked(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	resp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name":       "Team Only WF",
		"entry_mode": "team_only",
	})
	var wf struct {
		ID   string `json:"id"`
		Slug string `json:"slug"`
	}
	json.Unmarshal(resp.Body.Bytes(), &wf)

	h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
		"name": "S0", "ordinal": 0, "history_mode": "full",
	})
	h.request("PATCH", "/api/v1/workflows/"+wf.ID, h.adminToken, map[string]interface{}{
		"is_active": true,
	})
	h.request("POST", "/api/v1/workflows/"+wf.ID+"/publish", h.adminToken, nil)

	resp = h.request("POST", "/api/v1/workflow-entry/global/"+wf.Slug, "", nil)
	if resp.Code != http.StatusForbidden {
		t.Errorf("Visitor on team_only: got %d, want 403", resp.Code)
	}
}

// ═══════════════════════════════════════════════════════════
// #6/#7 — webhook_url/webhook_secret round-trip
// EXPECTED TO FAIL until stores are fixed to SELECT/INSERT these columns.
// ═══════════════════════════════════════════════════════════

// TestWorkflowWebhookFields verifies webhook_url and webhook_secret survive
// create → get round-trip. This WILL FAIL until the Postgres and SQLite
// workflow stores are updated to include these columns in their queries.
func TestWorkflowWebhookFields(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	resp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name":        "Webhook WF",
		"webhook_url": "https://hooks.example.com/wf",
	})
	if resp.Code != http.StatusCreated {
		t.Fatalf("Create with webhook_url: %d: %s", resp.Code, resp.Body.String())
	}
	var wf struct {
		ID         string `json:"id"`
		WebhookURL string `json:"webhook_url"`
	}
	json.Unmarshal(resp.Body.Bytes(), &wf)

	// BUG #6: webhook_url is not included in the store's INSERT/SELECT,
	// so it will be empty on the create response.
	if wf.WebhookURL != "https://hooks.example.com/wf" {
		t.Errorf("Create response webhook_url: got %q, want %q (BUG #6: store doesn't persist webhook_url)",
			wf.WebhookURL, "https://hooks.example.com/wf")
	}

	// Also verify via GET
	resp = h.request("GET", "/api/v1/workflows/"+wf.ID, h.adminToken, nil)
	json.Unmarshal(resp.Body.Bytes(), &wf)
	if wf.WebhookURL != "https://hooks.example.com/wf" {
		t.Errorf("GET webhook_url: got %q, want %q (BUG #6: store doesn't SELECT webhook_url)",
			wf.WebhookURL, "https://hooks.example.com/wf")
	}

	// BUG #7: PATCH webhook_url should work
	resp = h.request("PATCH", "/api/v1/workflows/"+wf.ID, h.adminToken, map[string]interface{}{
		"webhook_url": "https://hooks.example.com/updated",
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("PATCH webhook_url: %d: %s", resp.Code, resp.Body.String())
	}
	json.Unmarshal(resp.Body.Bytes(), &wf)
	if wf.WebhookURL != "https://hooks.example.com/updated" {
		t.Errorf("PATCH webhook_url: got %q, want %q (BUG #7: Update() ignores webhook_url)",
			wf.WebhookURL, "https://hooks.example.com/updated")
	}
}

// ═══════════════════════════════════════════════════════════
// #18 — ListMine should include unassigned-for-my-teams
// EXPECTED TO FAIL until ListMine query is fixed.
// ═══════════════════════════════════════════════════════════

// TestWorkflowAssignment_ListMineIncludesTeamUnassigned verifies that
// GET /workflow-assignments/mine returns both claimed assignments AND
// unassigned assignments for the user's teams.
func TestWorkflowAssignment_ListMineIncludesTeamUnassigned(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	// Create a team and add a member
	memberID := seedInsertReturningID(t,
		`INSERT INTO users (username, email, password_hash, role, handle, auth_source) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
		"wf-member", "wf-member@test.com", "$2a$10$test", "user", "wf-member", "builtin",
	)
	memberToken := makeToken(memberID, "wf-member@test.com", "user")

	resp := h.request("POST", "/api/v1/admin/teams", h.adminToken, map[string]string{
		"name": "Review Team", "description": "Test team",
	})
	if resp.Code != http.StatusCreated {
		t.Fatalf("Create team: %d: %s", resp.Code, resp.Body.String())
	}
	var team map[string]interface{}
	decode(resp, &team)
	teamID := team["id"].(string)

	h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), h.adminToken,
		map[string]string{"user_id": memberID, "role": "member"})

	// Create a workflow with an assignment stage
	wfResp := h.request("POST", "/api/v1/workflows", h.adminToken, map[string]interface{}{
		"name":       "Assignment WF",
		"entry_mode": "team_only",
	})
	var wf struct{ ID string `json:"id"` }
	json.Unmarshal(wfResp.Body.Bytes(), &wf)

	h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
		"name": "Intake", "ordinal": 0, "history_mode": "full",
	})
	h.request("POST", "/api/v1/workflows/"+wf.ID+"/stages", h.adminToken, map[string]interface{}{
		"name":               "Review",
		"ordinal":            1,
		"history_mode":       "full",
		"assignment_team_id": teamID,
	})
	h.request("PATCH", "/api/v1/workflows/"+wf.ID, h.adminToken, map[string]interface{}{
		"is_active": true,
	})
	h.request("POST", "/api/v1/workflows/"+wf.ID+"/publish", h.adminToken, nil)

	// Start instance and advance to stage 1 (creates an assignment)
	resp = h.request("POST", "/api/v1/workflows/"+wf.ID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)

	h.request("POST", "/api/v1/channels/"+start.ChannelID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"intake": "done"},
	})

	// BUG #18: ListMine should include the unassigned assignment for
	// the member's team, but currently only returns claimed assignments.
	resp = h.request("GET", "/api/v1/workflow-assignments/mine", memberToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("ListMine: %d: %s", resp.Code, resp.Body.String())
	}
	var listResp struct {
		Data []struct {
			ID     string  `json:"id"`
			Status string  `json:"status"`
			TeamID string  `json:"team_id"`
			To     *string `json:"assigned_to"`
		} `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &listResp)

	if len(listResp.Data) == 0 {
		t.Error("ListMine returned 0 assignments — BUG #18: should include unassigned assignments for user's teams")
	}

	// If we do get results, verify the shape
	for _, a := range listResp.Data {
		if a.TeamID != teamID {
			t.Errorf("Assignment team_id: got %q, want %q", a.TeamID, teamID)
		}
	}
}

// ═══════════════════════════════════════════════════════════
// Assignment Claim + Complete (basic happy path)
// ═══════════════════════════════════════════════════════════

// TestWorkflowAssignment_ClaimAndComplete exercises the claim → complete flow.
func TestWorkflowAssignment_ClaimAndComplete(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	// Seed assignment directly (simulates the advance handler creating one)
	wfID, _ := h.createPublishedWorkflow("Assign WF", 2)

	// Start + advance to create assignment stage
	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)

	// Manually insert an assignment (since our stages don't have assignment_team_id)
	teamID := seedInsertReturningID(t,
		`INSERT INTO teams (name, description, created_by) VALUES ($1, $2, $3) RETURNING id`,
		"Claim Team", "Test", h.adminID,
	)
	assignID := seedInsertReturningID(t,
		`INSERT INTO workflow_assignments (channel_id, stage, team_id) VALUES ($1, $2, $3) RETURNING id`,
		start.ChannelID, 0, teamID,
	)

	// Claim
	resp = h.request("POST", "/api/v1/workflow-assignments/"+assignID+"/claim", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("Claim: got %d, body: %s", resp.Code, resp.Body.String())
	}
	var claimResp struct {
		Claimed      bool   `json:"claimed"`
		AssignmentID string `json:"assignment_id"`
	}
	json.Unmarshal(resp.Body.Bytes(), &claimResp)
	if !claimResp.Claimed {
		t.Error("Claim: claimed should be true")
	}

	// Double-claim should conflict
	resp = h.request("POST", "/api/v1/workflow-assignments/"+assignID+"/claim", h.adminToken, nil)
	if resp.Code != http.StatusConflict {
		t.Errorf("Double claim: got %d, want 409", resp.Code)
	}

	// Complete
	resp = h.request("POST", "/api/v1/workflow-assignments/"+assignID+"/complete", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("Complete: got %d, body: %s", resp.Code, resp.Body.String())
	}

	// Double-complete should conflict
	resp = h.request("POST", "/api/v1/workflow-assignments/"+assignID+"/complete", h.adminToken, nil)
	if resp.Code != http.StatusConflict {
		t.Errorf("Double complete: got %d, want 409", resp.Code)
	}
}

// ═══════════════════════════════════════════════════════════
// #15 — Status on non-workflow channel returns 404
// ═══════════════════════════════════════════════════════════

func TestWorkflowGetStatus_NonWorkflowChannel(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	resp := h.request("GET", "/api/v1/channels/00000000-0000-0000-0000-000000000000/workflow/status", h.adminToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("Status on non-existent channel: got %d, want 404", resp.Code)
	}
}

// ═══════════════════════════════════════════════════════════
// #15 — Data merge across multiple advances
// ═══════════════════════════════════════════════════════════

// TestWorkflowAdvance_DataMerge verifies that stage data accumulates
// across multiple advance calls (merge, not replace).
func TestWorkflowAdvance_DataMerge(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)
	wfID, _ := h.createPublishedWorkflow("Merge Test", 3)

	resp := h.request("POST", "/api/v1/workflows/"+wfID+"/start", h.adminToken, nil)
	var start struct{ ChannelID string `json:"channel_id"` }
	json.Unmarshal(resp.Body.Bytes(), &start)
	chID := start.ChannelID

	// Advance stage 0 → 1 with key "a"
	h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"a": "1"},
	})

	// Advance stage 1 → 2 with key "b"
	h.request("POST", "/api/v1/channels/"+chID+"/workflow/advance", h.adminToken, map[string]interface{}{
		"data": map[string]string{"b": "2"},
	})

	// Check accumulated data contains both keys
	resp = h.request("GET", "/api/v1/channels/"+chID+"/workflow/status", h.adminToken, nil)
	var status struct{ StageData json.RawMessage `json:"stage_data"` }
	json.Unmarshal(resp.Body.Bytes(), &status)
	var merged map[string]string
	json.Unmarshal(status.StageData, &merged)

	if merged["a"] != "1" {
		t.Errorf("Merged data missing key 'a': got %v", merged)
	}
	if merged["b"] != "2" {
		t.Errorf("Merged data missing key 'b': got %v", merged)
	}
}

// ═══════════════════════════════════════════════════════════
// #17 — PATCH non-existent workflow
// ═══════════════════════════════════════════════════════════

// TestWorkflowPatchNonExistent verifies PATCH to a non-existent ID returns 404.
// Documents the confusing code path: Update affects 0 rows (no error),
// then GetByID returns sql.ErrNoRows → 404.
func TestWorkflowPatchNonExistent(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	resp := h.request("PATCH", "/api/v1/workflows/00000000-0000-0000-0000-000000000000", h.adminToken, map[string]interface{}{
		"name": "ghost",
	})
	if resp.Code != http.StatusNotFound {
		t.Errorf("PATCH non-existent: got %d, want 404", resp.Code)
	}
}

// ═══════════════════════════════════════════════════════════
// #19 — ListForTeam with ?status= filter
// ═══════════════════════════════════════════════════════════

// TestWorkflowAssignment_ListForTeamStatusFilter verifies the ?status=
// query param on GET /teams/:teamId/assignments.
func TestWorkflowAssignment_ListForTeamStatusFilter(t *testing.T) {
	h := setupWorkflowInstanceHarness(t)

	// Create team
	resp := h.request("POST", "/api/v1/admin/teams", h.adminToken, map[string]string{
		"name": "Filter Team", "description": "Test",
	})
	if resp.Code != http.StatusCreated {
		t.Fatalf("Create team: %d: %s", resp.Code, resp.Body.String())
	}
	var team map[string]interface{}
	decode(resp, &team)
	teamID := team["id"].(string)

	// Add self as member (needed for access)
	h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), h.adminToken,
		map[string]string{"user_id": h.adminID, "role": "admin"})

	// Create real channels for FK satisfaction
	ch1ID := database.SeedTestChannel(t, h.adminID, "filter-ch-1")
	ch2ID := database.SeedTestChannel(t, h.adminID, "filter-ch-2")

	// Seed two assignments: one unassigned, one claimed
	seedExec(t,
		`INSERT INTO workflow_assignments (channel_id, stage, team_id, status) VALUES ($1, $2, $3, $4)`,
		ch1ID, 0, teamID, "unassigned",
	)
	seedExec(t,
		`INSERT INTO workflow_assignments (channel_id, stage, team_id, status, assigned_to) VALUES ($1, $2, $3, $4, $5)`,
		ch2ID, 1, teamID, "claimed", h.adminID,
	)

	// Default (unassigned)
	resp = h.request("GET", "/api/v1/teams/"+teamID+"/assignments", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("ListForTeam default: %d: %s", resp.Code, resp.Body.String())
	}
	var lr struct {
		Data []struct{ Status string `json:"status"` } `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &lr)
	if len(lr.Data) != 1 {
		t.Errorf("Default filter: expected 1 unassigned, got %d", len(lr.Data))
	} else if lr.Data[0].Status != "unassigned" {
		t.Errorf("Default filter: expected status 'unassigned', got %q", lr.Data[0].Status)
	}

	// Explicit ?status=claimed
	resp = h.request("GET", "/api/v1/teams/"+teamID+"/assignments?status=claimed", h.adminToken, nil)
	json.Unmarshal(resp.Body.Bytes(), &lr)
	if len(lr.Data) != 1 {
		t.Errorf("?status=claimed: expected 1, got %d", len(lr.Data))
	} else if lr.Data[0].Status != "claimed" {
		t.Errorf("?status=claimed: expected status 'claimed', got %q", lr.Data[0].Status)
	}
}
