package handlers

import (
	"bytes"
	"context"
	"encoding/json"
	"testing"
	"time"

	"switchboard-core/database"
	"switchboard-core/models"
	"switchboard-core/store"
	"switchboard-core/store/postgres"
	"switchboard-core/store/sqlite"
)

// jsonEq compares two JSON byte slices ignoring whitespace and key order
// (PG JSONB normalizes spacing and may reorder keys).
func jsonEq(a, b json.RawMessage) bool {
	var va, vb interface{}
	if err := json.Unmarshal(a, &va); err != nil {
		return false
	}
	if err := json.Unmarshal(b, &vb); err != nil {
		return false
	}
	na, _ := json.Marshal(va)
	nb, _ := json.Marshal(vb)
	return bytes.Equal(na, nb)
}

// testStores returns an appropriate Stores for the current dialect.
func testStores(t *testing.T) store.Stores {
	t.Helper()
	database.RequireTestDB(t)
	if database.IsSQLite() {
		return sqlite.NewStores(database.TestDB)
	}
	return postgres.NewStores(database.TestDB)
}

// seedWorkflowFixture creates a workflow + 2 stages + published version.
// Returns (workflowID, versionNumber, stage1Name, stage2Name).
func seedWorkflowFixture(t *testing.T, s store.Stores, userID, teamID string) (string, int, string, string) {
	t.Helper()
	ctx := context.Background()

	wf := &models.Workflow{
		TeamID:    &teamID,
		Name:      "Test Workflow",
		Slug:      "test-workflow",
		EntryMode: "team_only",
		IsActive:  true,
		CreatedBy: userID,
	}
	if err := s.Workflows.Create(ctx, wf); err != nil {
		t.Fatalf("create workflow: %v", err)
	}

	s1 := &models.WorkflowStage{
		WorkflowID: wf.ID,
		Ordinal:    0,
		Name:       "intake",
		StageMode:  models.StageModeForm,
		Audience:   models.AudienceTeam,
		StageType:  models.StageTypeSimple,
	}
	s2 := &models.WorkflowStage{
		WorkflowID:       wf.ID,
		Ordinal:          1,
		Name:             "review",
		StageMode:        models.StageModeReview,
		Audience:         models.AudienceTeam,
		StageType:        models.StageTypeSimple,
		AssignmentTeamID: &teamID,
	}
	if err := s.Workflows.CreateStage(ctx, s1); err != nil {
		t.Fatalf("create stage 1: %v", err)
	}
	if err := s.Workflows.CreateStage(ctx, s2); err != nil {
		t.Fatalf("create stage 2: %v", err)
	}

	// Publish version 1
	snapshot, _ := json.Marshal([]models.WorkflowStage{*s1, *s2})
	ver := &models.WorkflowVersion{
		WorkflowID:    wf.ID,
		VersionNumber: 1,
		Snapshot:      snapshot,
	}
	if err := s.Workflows.Publish(ctx, ver); err != nil {
		t.Fatalf("publish version: %v", err)
	}

	return wf.ID, 1, s1.Name, s2.Name
}

// ── Instance Store Tests ────────────────────

func TestWorkflowInstance_CreateAndGet(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Alice", "alice@test.com")
	teamID := database.SeedTestTeam(t, "Team A", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	token := "tok-abc-123"
	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StageData:       json.RawMessage(`{"key":"val"}`),
		Status:          models.InstanceStatusActive,
		StartedBy:       userID,
		EntryToken:      &token,
		Metadata:        json.RawMessage(`{"source":"test"}`),
	}
	if err := s.Workflows.CreateInstance(ctx, inst); err != nil {
		t.Fatalf("create instance: %v", err)
	}
	if inst.ID == "" {
		t.Fatal("expected ID to be set")
	}

	got, err := s.Workflows.GetInstance(ctx, inst.ID)
	if err != nil {
		t.Fatalf("get instance: %v", err)
	}
	if got.WorkflowID != wfID {
		t.Errorf("workflow_id = %q, want %q", got.WorkflowID, wfID)
	}
	if got.CurrentStage != stage1 {
		t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage1)
	}
	if got.Status != models.InstanceStatusActive {
		t.Errorf("status = %q, want active", got.Status)
	}
	if got.StartedBy != userID {
		t.Errorf("started_by = %q, want %q", got.StartedBy, userID)
	}
	if got.EntryToken == nil || *got.EntryToken != token {
		t.Errorf("entry_token = %v, want %q", got.EntryToken, token)
	}
	if !jsonEq(got.StageData, json.RawMessage(`{"key":"val"}`)) {
		t.Errorf("stage_data = %s, want {\"key\":\"val\"}", got.StageData)
	}
	if got.CreatedAt.IsZero() {
		t.Error("created_at is zero")
	}
}

func TestWorkflowInstance_GetByToken(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Bob", "bob@test.com")
	teamID := database.SeedTestTeam(t, "Team B", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	token := "unique-entry-token"
	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StartedBy:       userID,
		EntryToken:      &token,
	}
	if err := s.Workflows.CreateInstance(ctx, inst); err != nil {
		t.Fatalf("create instance: %v", err)
	}

	got, err := s.Workflows.GetInstanceByToken(ctx, token)
	if err != nil {
		t.Fatalf("get by token: %v", err)
	}
	if got.ID != inst.ID {
		t.Errorf("id = %q, want %q", got.ID, inst.ID)
	}
}

func TestWorkflowInstance_Update(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Carol", "carol@test.com")
	teamID := database.SeedTestTeam(t, "Team C", userID)
	wfID, ver, stage1, stage2 := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StageData:       json.RawMessage(`{}`),
		StartedBy:       userID,
		Metadata:        json.RawMessage(`{}`),
	}
	if err := s.Workflows.CreateInstance(ctx, inst); err != nil {
		t.Fatalf("create: %v", err)
	}

	inst.CurrentStage = stage2
	inst.StageData = json.RawMessage(`{"updated":true}`)
	inst.StageEnteredAt = time.Now().UTC().Add(time.Minute)
	if err := s.Workflows.UpdateInstance(ctx, inst); err != nil {
		t.Fatalf("update: %v", err)
	}

	got, _ := s.Workflows.GetInstance(ctx, inst.ID)
	if got.CurrentStage != stage2 {
		t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
	}
	if !jsonEq(got.StageData, json.RawMessage(`{"updated":true}`)) {
		t.Errorf("stage_data = %s, want {\"updated\":true}", got.StageData)
	}
}

func TestWorkflowInstance_List(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Dave", "dave@test.com")
	teamID := database.SeedTestTeam(t, "Team D", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	// Create 3 instances: 2 active, 1 completed
	for i := 0; i < 3; i++ {
		inst := &models.WorkflowInstance{
			WorkflowID:      wfID,
			WorkflowVersion: ver,
			CurrentStage:    stage1,
			StartedBy:       userID,
		}
		if err := s.Workflows.CreateInstance(ctx, inst); err != nil {
			t.Fatalf("create %d: %v", i, err)
		}
		if i == 2 {
			s.Workflows.CompleteInstance(ctx, inst.ID)
		}
	}

	// List all
	all, err := s.Workflows.ListInstances(ctx, wfID, "", store.ListOptions{})
	if err != nil {
		t.Fatalf("list all: %v", err)
	}
	if len(all) != 3 {
		t.Fatalf("len = %d, want 3", len(all))
	}

	// Filter by status
	active, err := s.Workflows.ListInstances(ctx, wfID, "active", store.ListOptions{})
	if err != nil {
		t.Fatalf("list active: %v", err)
	}
	if len(active) != 2 {
		t.Errorf("active count = %d, want 2", len(active))
	}

	// Pagination
	page, err := s.Workflows.ListInstances(ctx, wfID, "", store.ListOptions{Limit: 2, Offset: 1})
	if err != nil {
		t.Fatalf("list paged: %v", err)
	}
	if len(page) != 2 {
		t.Errorf("paged count = %d, want 2", len(page))
	}
}

func TestWorkflowInstance_AdvanceStage(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Eve", "eve@test.com")
	teamID := database.SeedTestTeam(t, "Team E", userID)
	wfID, ver, stage1, stage2 := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StageData:       json.RawMessage(`{"step":"one"}`),
		StartedBy:       userID,
	}
	if err := s.Workflows.CreateInstance(ctx, inst); err != nil {
		t.Fatalf("create: %v", err)
	}

	// Re-read to get the stored (possibly truncated) timestamp
	before, _ := s.Workflows.GetInstance(ctx, inst.ID)
	originalEnteredAt := before.StageEnteredAt

	// Sleep >1s to ensure SQLite second-precision timestamp differs
	time.Sleep(1100 * time.Millisecond)

	newData := json.RawMessage(`{"step":"two"}`)
	if err := s.Workflows.AdvanceStage(ctx, inst.ID, stage2, newData); err != nil {
		t.Fatalf("advance: %v", err)
	}

	got, _ := s.Workflows.GetInstance(ctx, inst.ID)
	if got.CurrentStage != stage2 {
		t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
	}
	if !jsonEq(got.StageData, json.RawMessage(`{"step":"two"}`)) {
		t.Errorf("stage_data = %s, want {\"step\":\"two\"}", got.StageData)
	}
	if !got.StageEnteredAt.After(originalEnteredAt) {
		t.Errorf("stage_entered_at should have advanced; original=%v, got=%v",
			originalEnteredAt, got.StageEnteredAt)
	}
}

func TestWorkflowInstance_Complete(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Frank", "frank@test.com")
	teamID := database.SeedTestTeam(t, "Team F", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StartedBy:       userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	if err := s.Workflows.CompleteInstance(ctx, inst.ID); err != nil {
		t.Fatalf("complete: %v", err)
	}

	got, _ := s.Workflows.GetInstance(ctx, inst.ID)
	if got.Status != models.InstanceStatusCompleted {
		t.Errorf("status = %q, want completed", got.Status)
	}

	// Complete again — should be no-op (already completed, WHERE status='active' won't match)
	if err := s.Workflows.CompleteInstance(ctx, inst.ID); err != nil {
		t.Fatalf("complete again: %v", err)
	}
}

func TestWorkflowInstance_Cancel(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Grace", "grace@test.com")
	teamID := database.SeedTestTeam(t, "Team G", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StartedBy:       userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	if err := s.Workflows.CancelInstance(ctx, inst.ID); err != nil {
		t.Fatalf("cancel: %v", err)
	}

	got, _ := s.Workflows.GetInstance(ctx, inst.ID)
	if got.Status != models.InstanceStatusCancelled {
		t.Errorf("status = %q, want cancelled", got.Status)
	}
}

// ── Assignment Store Tests ──────────────────

func TestWorkflowAssignment_CreateAndList(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Hank", "hank@test.com")
	teamID := database.SeedTestTeam(t, "Team H", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID:      wfID,
		WorkflowVersion: ver,
		CurrentStage:    stage1,
		StartedBy:       userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	a := &models.WorkflowAssignment{
		InstanceID: inst.ID,
		Stage:      stage1,
		TeamID:     teamID,
	}
	if err := s.Workflows.CreateAssignment(ctx, a); err != nil {
		t.Fatalf("create assignment: %v", err)
	}
	if a.ID == "" {
		t.Fatal("expected assignment ID")
	}
	if a.Status != models.AssignmentStatusUnassigned {
		t.Errorf("default status = %q, want unassigned", a.Status)
	}

	list, err := s.Workflows.ListAssignmentsByInstance(ctx, inst.ID)
	if err != nil {
		t.Fatalf("list by instance: %v", err)
	}
	if len(list) != 1 {
		t.Fatalf("len = %d, want 1", len(list))
	}
	if list[0].Stage != stage1 {
		t.Errorf("stage = %q, want %q", list[0].Stage, stage1)
	}
}

func TestWorkflowAssignment_Claim(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Ivy", "ivy@test.com")
	teamID := database.SeedTestTeam(t, "Team I", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	a := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a)

	if err := s.Workflows.ClaimAssignment(ctx, a.ID, userID); err != nil {
		t.Fatalf("claim: %v", err)
	}

	list, _ := s.Workflows.ListAssignmentsByInstance(ctx, inst.ID)
	got := list[0]
	if got.Status != models.AssignmentStatusClaimed {
		t.Errorf("status = %q, want claimed", got.Status)
	}
	if got.AssignedTo == nil || *got.AssignedTo != userID {
		t.Errorf("assigned_to = %v, want %q", got.AssignedTo, userID)
	}
	if got.ClaimedAt == nil {
		t.Error("claimed_at should be set")
	}
}

func TestWorkflowAssignment_ClaimAlreadyClaimed(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Jack", "jack@test.com")
	teamID := database.SeedTestTeam(t, "Team J", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	a := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a)
	s.Workflows.ClaimAssignment(ctx, a.ID, userID)

	// Second claim should fail
	err := s.Workflows.ClaimAssignment(ctx, a.ID, userID)
	if err == nil {
		t.Fatal("expected error on double claim")
	}
}

func TestWorkflowAssignment_Unclaim(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Kate", "kate@test.com")
	teamID := database.SeedTestTeam(t, "Team K", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	a := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a)
	s.Workflows.ClaimAssignment(ctx, a.ID, userID)

	if err := s.Workflows.UnclaimAssignment(ctx, a.ID); err != nil {
		t.Fatalf("unclaim: %v", err)
	}

	list, _ := s.Workflows.ListAssignmentsByInstance(ctx, inst.ID)
	got := list[0]
	if got.Status != models.AssignmentStatusUnassigned {
		t.Errorf("status = %q, want unassigned", got.Status)
	}
	if got.AssignedTo != nil {
		t.Errorf("assigned_to = %v, want nil", got.AssignedTo)
	}
	if got.ClaimedAt != nil {
		t.Errorf("claimed_at = %v, want nil", got.ClaimedAt)
	}
}

func TestWorkflowAssignment_Complete(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Leo", "leo@test.com")
	teamID := database.SeedTestTeam(t, "Team L", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	a := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a)
	s.Workflows.ClaimAssignment(ctx, a.ID, userID)

	reviewData := json.RawMessage(`{"approved":true,"notes":"LGTM"}`)
	if err := s.Workflows.CompleteAssignment(ctx, a.ID, reviewData); err != nil {
		t.Fatalf("complete: %v", err)
	}

	list, _ := s.Workflows.ListAssignmentsByInstance(ctx, inst.ID)
	got := list[0]
	if got.Status != models.AssignmentStatusCompleted {
		t.Errorf("status = %q, want completed", got.Status)
	}
	if !jsonEq(got.ReviewData, json.RawMessage(`{"approved":true,"notes":"LGTM"}`)) {
		t.Errorf("review_data = %s, want {\"approved\":true,\"notes\":\"LGTM\"}", got.ReviewData)
	}
	if got.CompletedAt == nil {
		t.Error("completed_at should be set")
	}
}

func TestWorkflowAssignment_Cancel(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Mia", "mia@test.com")
	teamID := database.SeedTestTeam(t, "Team M", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	// Cancel from unassigned
	a1 := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a1)
	if err := s.Workflows.CancelAssignment(ctx, a1.ID); err != nil {
		t.Fatalf("cancel unassigned: %v", err)
	}
	list, _ := s.Workflows.ListAssignmentsByInstance(ctx, inst.ID)
	if list[0].Status != models.AssignmentStatusCancelled {
		t.Errorf("status = %q, want cancelled", list[0].Status)
	}

	// Cancel from claimed
	a2 := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamID}
	s.Workflows.CreateAssignment(ctx, a2)
	s.Workflows.ClaimAssignment(ctx, a2.ID, userID)
	if err := s.Workflows.CancelAssignment(ctx, a2.ID); err != nil {
		t.Fatalf("cancel claimed: %v", err)
	}
}

// ── v0.3.3 Store Tests ──────────────────────

func TestWorkflowInstance_MarkStale(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Stale-Test", "stale@test.com")
	teamID := database.SeedTestTeam(t, "Team Stale", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	if err := s.Workflows.MarkInstanceStale(ctx, inst.ID); err != nil {
		t.Fatalf("mark stale: %v", err)
	}

	got, _ := s.Workflows.GetInstance(ctx, inst.ID)
	if got.Status != models.InstanceStatusStale {
		t.Errorf("status = %q, want stale", got.Status)
	}

	// Marking again is a no-op (WHERE status='active' won't match)
	if err := s.Workflows.MarkInstanceStale(ctx, inst.ID); err != nil {
		t.Fatalf("mark stale again: %v", err)
	}
}

func TestWorkflowInstance_ListActive(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Active-Test", "active@test.com")
	teamID := database.SeedTestTeam(t, "Team Active", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	// Create 3 instances: 2 active, 1 completed
	var activeIDs []string
	for i := 0; i < 3; i++ {
		inst := &models.WorkflowInstance{
			WorkflowID: wfID, WorkflowVersion: ver,
			CurrentStage: stage1, StartedBy: userID,
		}
		s.Workflows.CreateInstance(ctx, inst)
		if i < 2 {
			activeIDs = append(activeIDs, inst.ID)
		} else {
			s.Workflows.CompleteInstance(ctx, inst.ID)
		}
	}

	active, err := s.Workflows.ListActiveInstances(ctx)
	if err != nil {
		t.Fatalf("list active: %v", err)
	}
	if len(active) != 2 {
		t.Fatalf("count = %d, want 2", len(active))
	}
	for _, inst := range active {
		if inst.Status != models.InstanceStatusActive {
			t.Errorf("instance %s status = %q, want active", inst.ID, inst.Status)
		}
	}
}

func TestWorkflow_StalenessTimeoutHours(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Staleness-Test", "staleness@test.com")
	teamID := database.SeedTestTeam(t, "Team Staleness", userID)

	// Create with staleness_timeout_hours set
	hours := 48
	wf := &models.Workflow{
		TeamID:                &teamID,
		Name:                  "Staleness WF",
		Slug:                  "staleness-wf",
		EntryMode:             "team_only",
		IsActive:              true,
		CreatedBy:             userID,
		StalenessTimeoutHours: &hours,
	}
	if err := s.Workflows.Create(ctx, wf); err != nil {
		t.Fatalf("create: %v", err)
	}

	got, err := s.Workflows.GetByID(ctx, wf.ID)
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.StalenessTimeoutHours == nil || *got.StalenessTimeoutHours != 48 {
		t.Errorf("staleness_timeout_hours = %v, want 48", got.StalenessTimeoutHours)
	}

	// Update via patch
	newHours := 24
	if err := s.Workflows.Update(ctx, wf.ID, models.WorkflowPatch{StalenessTimeoutHours: &newHours}); err != nil {
		t.Fatalf("update: %v", err)
	}

	got2, _ := s.Workflows.GetByID(ctx, wf.ID)
	if got2.StalenessTimeoutHours == nil || *got2.StalenessTimeoutHours != 24 {
		t.Errorf("after update staleness_timeout_hours = %v, want 24", got2.StalenessTimeoutHours)
	}

	// Create without staleness — should be nil
	wf2 := &models.Workflow{
		TeamID:    &teamID,
		Name:      "No Staleness WF",
		Slug:      "no-staleness-wf",
		EntryMode: "team_only",
		IsActive:  true,
		CreatedBy: userID,
	}
	s.Workflows.Create(ctx, wf2)
	got3, _ := s.Workflows.GetByID(ctx, wf2.ID)
	if got3.StalenessTimeoutHours != nil {
		t.Errorf("nil staleness = %v, want nil", got3.StalenessTimeoutHours)
	}
}

func TestWorkflowAssignment_ListByTeam(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Nora", "nora@test.com")
	teamA := database.SeedTestTeam(t, "Team Alpha", userID)
	teamB := database.SeedTestTeam(t, "Team Beta", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamA)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	// 2 assignments for teamA, 1 for teamB
	for i := 0; i < 2; i++ {
		a := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamA}
		s.Workflows.CreateAssignment(ctx, a)
	}
	a3 := &models.WorkflowAssignment{InstanceID: inst.ID, Stage: stage1, TeamID: teamB}
	s.Workflows.CreateAssignment(ctx, a3)

	// Claim one of teamA's so we can test status filter
	listA, _ := s.Workflows.ListAssignmentsByTeam(ctx, teamA, "")
	if len(listA) != 2 {
		t.Fatalf("teamA count = %d, want 2", len(listA))
	}
	s.Workflows.ClaimAssignment(ctx, listA[0].ID, userID)

	// Filter teamA unassigned only
	unassigned, _ := s.Workflows.ListAssignmentsByTeam(ctx, teamA, "unassigned")
	if len(unassigned) != 1 {
		t.Errorf("teamA unassigned = %d, want 1", len(unassigned))
	}

	// teamB should have 1
	listB, _ := s.Workflows.ListAssignmentsByTeam(ctx, teamB, "")
	if len(listB) != 1 {
		t.Errorf("teamB count = %d, want 1", len(listB))
	}
}

// ── Signoff store tests (v0.3.4) ──────────────

func TestWorkflowSignoff_CreateAndList(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Signer", "signer@test.com")
	teamID := database.SeedTestTeam(t, "Signoff Team", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	// Create two signoffs
	so1 := &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1,
		UserID: userID, Decision: models.SignoffApprove, Comment: "LGTM",
	}
	if err := s.Workflows.CreateSignoff(ctx, so1); err != nil {
		t.Fatalf("create signoff: %v", err)
	}
	if so1.ID == "" {
		t.Fatal("signoff ID not populated")
	}
	if so1.CreatedAt.IsZero() {
		t.Fatal("signoff created_at not populated")
	}

	user2 := database.SeedTestUser(t, "Signer2", "signer2@test.com")
	so2 := &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1,
		UserID: user2, Decision: models.SignoffReject, Comment: "Needs work",
	}
	if err := s.Workflows.CreateSignoff(ctx, so2); err != nil {
		t.Fatalf("create signoff 2: %v", err)
	}

	// List should return both, ordered by created_at
	list, err := s.Workflows.ListSignoffs(ctx, inst.ID, stage1)
	if err != nil {
		t.Fatalf("list signoffs: %v", err)
	}
	if len(list) != 2 {
		t.Fatalf("signoff count = %d, want 2", len(list))
	}
	if list[0].Decision != models.SignoffApprove {
		t.Errorf("first signoff decision = %s, want approve", list[0].Decision)
	}
	if list[1].Decision != models.SignoffReject {
		t.Errorf("second signoff decision = %s, want reject", list[1].Decision)
	}

	// UNIQUE constraint: same user + instance + stage should fail
	dup := &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1,
		UserID: userID, Decision: models.SignoffApprove,
	}
	if err := s.Workflows.CreateSignoff(ctx, dup); err == nil {
		t.Fatal("expected UNIQUE violation for duplicate signoff, got nil")
	}
}

func TestWorkflowSignoff_ListEmpty(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	// ListSignoffs on non-existent instance should return empty, not error
	list, err := s.Workflows.ListSignoffs(ctx, "00000000-0000-0000-0000-000000000000", "stage")
	if err != nil {
		t.Fatalf("list signoffs error: %v", err)
	}
	if list != nil && len(list) != 0 {
		t.Fatalf("expected empty list, got %d", len(list))
	}
}

// ── Clone tests (v0.3.5) ──────────────────

func TestWorkflow_Clone(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Cloner", "cloner@test.com")
	teamID := database.SeedTestTeam(t, "Clone Team", userID)
	wfID, _, _, _ := seedWorkflowFixture(t, s, userID, teamID)

	// Read source
	src, err := s.Workflows.GetByID(ctx, wfID)
	if err != nil {
		t.Fatalf("get source: %v", err)
	}
	srcStages, err := s.Workflows.ListStages(ctx, wfID)
	if err != nil {
		t.Fatalf("list source stages: %v", err)
	}

	// Clone
	clone := &models.Workflow{
		TeamID:    src.TeamID,
		Name:      "Copy of " + src.Name,
		Slug:      src.Slug + "-copy",
		EntryMode: src.EntryMode,
		IsActive:  false,
		CreatedBy: userID,
	}
	if err := s.Workflows.Create(ctx, clone); err != nil {
		t.Fatalf("create clone: %v", err)
	}
	for _, st := range srcStages {
		cst := &models.WorkflowStage{
			WorkflowID: clone.ID,
			Ordinal:    st.Ordinal,
			Name:       st.Name,
			StageMode:  st.StageMode,
			Audience:   st.Audience,
			StageType:  st.StageType,
		}
		if err := s.Workflows.CreateStage(ctx, cst); err != nil {
			t.Fatalf("clone stage: %v", err)
		}
	}

	// Verify clone
	if clone.ID == src.ID {
		t.Error("clone ID must differ from source")
	}
	if clone.Name != "Copy of Test Workflow" {
		t.Errorf("name = %q, want 'Copy of Test Workflow'", clone.Name)
	}
	if clone.IsActive {
		t.Error("clone should be inactive")
	}

	clonedStages, _ := s.Workflows.ListStages(ctx, clone.ID)
	if len(clonedStages) != len(srcStages) {
		t.Fatalf("cloned stages = %d, want %d", len(clonedStages), len(srcStages))
	}
	for i, cs := range clonedStages {
		if cs.ID == srcStages[i].ID {
			t.Errorf("stage %d: clone ID should differ from source", i)
		}
		if cs.Name != srcStages[i].Name {
			t.Errorf("stage %d: name = %q, want %q", i, cs.Name, srcStages[i].Name)
		}
		if cs.Ordinal != srcStages[i].Ordinal {
			t.Errorf("stage %d: ordinal = %d, want %d", i, cs.Ordinal, srcStages[i].Ordinal)
		}
	}

	// Source unmodified
	srcAfter, _ := s.Workflows.GetByID(ctx, wfID)
	if srcAfter.Name != src.Name {
		t.Error("source workflow was modified")
	}
	srcStagesAfter, _ := s.Workflows.ListStages(ctx, wfID)
	if len(srcStagesAfter) != len(srcStages) {
		t.Error("source stages were modified")
	}
}

func TestWorkflowSignoff_Count(t *testing.T) {
	database.RequireTestDB(t)
	database.TruncateAll(t)
	s := testStores(t)
	ctx := context.Background()

	userID := database.SeedTestUser(t, "Counter", "counter@test.com")
	teamID := database.SeedTestTeam(t, "Count Team", userID)
	wfID, ver, stage1, _ := seedWorkflowFixture(t, s, userID, teamID)

	inst := &models.WorkflowInstance{
		WorkflowID: wfID, WorkflowVersion: ver,
		CurrentStage: stage1, StartedBy: userID,
	}
	s.Workflows.CreateInstance(ctx, inst)

	// No signoffs yet
	count, err := s.Workflows.CountSignoffs(ctx, inst.ID, stage1, models.SignoffApprove)
	if err != nil {
		t.Fatalf("count signoffs error: %v", err)
	}
	if count != 0 {
		t.Fatalf("expected 0, got %d", count)
	}

	// Add 2 approvals and 1 rejection
	user2 := database.SeedTestUser(t, "Counter2", "counter2@test.com")
	user3 := database.SeedTestUser(t, "Counter3", "counter3@test.com")

	s.Workflows.CreateSignoff(ctx, &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1, UserID: userID, Decision: models.SignoffApprove,
	})
	s.Workflows.CreateSignoff(ctx, &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1, UserID: user2, Decision: models.SignoffApprove,
	})
	s.Workflows.CreateSignoff(ctx, &models.WorkflowSignoff{
		InstanceID: inst.ID, Stage: stage1, UserID: user3, Decision: models.SignoffReject,
	})

	approveCount, _ := s.Workflows.CountSignoffs(ctx, inst.ID, stage1, models.SignoffApprove)
	if approveCount != 2 {
		t.Errorf("approve count = %d, want 2", approveCount)
	}
	rejectCount, _ := s.Workflows.CountSignoffs(ctx, inst.ID, stage1, models.SignoffReject)
	if rejectCount != 1 {
		t.Errorf("reject count = %d, want 1", rejectCount)
	}
}
