// contributor_routing_e2e_test.go - E2E tests for contributor routing
//
// These tests verify that issues are correctly routed to the planning repo
// when the user is detected as a contributor with auto-routing enabled.

//go:build integration
// +build integration

package main

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/steveyegge/beads/internal/routing"
	"github.com/steveyegge/beads/internal/storage/sqlite"
	"github.com/steveyegge/beads/internal/types"
)

// TestContributorRoutingTracer is the Phase 1 tracer bullet test.
// It proves that:
// 1. ExpandPath correctly expands ~ and relative paths
// 2. Routing config is correctly read (including backward compat)
// 3. DetermineTargetRepo returns the correct repo for contributors
//
// Full store switching is deferred to Phase 2.
func TestContributorRoutingTracer(t *testing.T) {
	t.Run("ExpandPath_tilde_expansion", func(t *testing.T) {
		home, err := os.UserHomeDir()
		if err != nil {
			t.Skipf("cannot get home dir: %v", err)
		}

		tests := []struct {
			input string
			want  string
		}{
			{"~/foo", filepath.Join(home, "foo")},
			{"~/bar/baz", filepath.Join(home, "bar", "baz")},
			{".", "."},
			{"", ""},
		}

		for _, tt := range tests {
			got := routing.ExpandPath(tt.input)
			if got != tt.want {
				t.Errorf("ExpandPath(%q) = %q, want %q", tt.input, got, tt.want)
			}
		}
	})

	t.Run("DetermineTargetRepo_contributor_routes_to_planning", func(t *testing.T) {
		config := &routing.RoutingConfig{
			Mode:            "auto",
			ContributorRepo: "~/.beads-planning",
		}

		got := routing.DetermineTargetRepo(config, routing.Contributor, ".")
		if got != "~/.beads-planning" {
			t.Errorf("DetermineTargetRepo() = %q, want %q", got, "~/.beads-planning")
		}
	})

	t.Run("DetermineTargetRepo_maintainer_stays_local", func(t *testing.T) {
		config := &routing.RoutingConfig{
			Mode:            "auto",
			MaintainerRepo:  ".",
			ContributorRepo: "~/.beads-planning",
		}

		got := routing.DetermineTargetRepo(config, routing.Maintainer, ".")
		if got != "." {
			t.Errorf("DetermineTargetRepo() = %q, want %q", got, ".")
		}
	})

	t.Run("E2E_routing_decision_with_store", func(t *testing.T) {
		// Set up temporary directory structure
		tmpDir := t.TempDir()
		projectDir := filepath.Join(tmpDir, "project")
		planningDir := filepath.Join(tmpDir, "planning")

		// Create project .beads directory
		projectBeadsDir := filepath.Join(projectDir, ".beads")
		if err := os.MkdirAll(projectBeadsDir, 0755); err != nil {
			t.Fatalf("failed to create project .beads dir: %v", err)
		}

		// Create planning .beads directory
		planningBeadsDir := filepath.Join(planningDir, ".beads")
		if err := os.MkdirAll(planningBeadsDir, 0755); err != nil {
			t.Fatalf("failed to create planning .beads dir: %v", err)
		}

		// Initialize project database
		projectDBPath := filepath.Join(projectBeadsDir, "beads.db")
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		projectStore, err := sqlite.New(ctx, projectDBPath)
		if err != nil {
			t.Fatalf("failed to create project store: %v", err)
		}
		defer projectStore.Close()

		// Set routing config in project store (canonical keys)
		if err := projectStore.SetConfig(ctx, "routing.mode", "auto"); err != nil {
			t.Fatalf("failed to set routing.mode: %v", err)
		}
		if err := projectStore.SetConfig(ctx, "routing.contributor", planningDir); err != nil {
			t.Fatalf("failed to set routing.contributor: %v", err)
		}

		// Verify config was stored correctly
		mode, err := projectStore.GetConfig(ctx, "routing.mode")
		if err != nil {
			t.Fatalf("failed to get routing.mode: %v", err)
		}
		if mode != "auto" {
			t.Errorf("routing.mode = %q, want %q", mode, "auto")
		}

		contributorPath, err := projectStore.GetConfig(ctx, "routing.contributor")
		if err != nil {
			t.Fatalf("failed to get routing.contributor: %v", err)
		}
		if contributorPath != planningDir {
			t.Errorf("routing.contributor = %q, want %q", contributorPath, planningDir)
		}

		// Build routing config from stored values
		routingConfig := &routing.RoutingConfig{
			Mode:            mode,
			ContributorRepo: contributorPath,
		}

		// Verify routing decision for contributor
		targetRepo := routing.DetermineTargetRepo(routingConfig, routing.Contributor, projectDir)
		if targetRepo != planningDir {
			t.Errorf("DetermineTargetRepo() = %q, want %q", targetRepo, planningDir)
		}

		// Verify routing decision for maintainer stays local
		targetRepo = routing.DetermineTargetRepo(routingConfig, routing.Maintainer, projectDir)
		if targetRepo != "." {
			t.Errorf("DetermineTargetRepo() for maintainer = %q, want %q", targetRepo, ".")
		}

		// Initialize planning database and verify we can create issues there
		planningDBPath := filepath.Join(planningBeadsDir, "beads.db")
		planningStore, err := sqlite.New(ctx, planningDBPath)
		if err != nil {
			t.Fatalf("failed to create planning store: %v", err)
		}
		defer planningStore.Close()

		// Initialize planning store with required config
		if err := planningStore.SetConfig(ctx, "issue_prefix", "plan-"); err != nil {
			t.Fatalf("failed to set issue_prefix in planning store: %v", err)
		}

		// Create a test issue in planning store (simulating what Phase 2 will do)
		issue := &types.Issue{
			Title:     "Test contributor issue",
			IssueType: types.TypeTask,
			Status:    types.StatusOpen,
			Priority:  2,
		}

		if err := planningStore.CreateIssue(ctx, issue, "test"); err != nil {
			t.Fatalf("failed to create issue in planning store: %v", err)
		}

		// Verify issue exists in planning store
		retrieved, err := planningStore.GetIssue(ctx, issue.ID)
		if err != nil {
			t.Fatalf("failed to get issue from planning store: %v", err)
		}
		if retrieved.Title != "Test contributor issue" {
			t.Errorf("issue title = %q, want %q", retrieved.Title, "Test contributor issue")
		}

		// Verify issue does NOT exist in project store (isolation check)
		projectIssue, _ := projectStore.GetIssue(ctx, issue.ID)
		if projectIssue != nil {
			t.Error("issue should NOT exist in project store (isolation failure)")
		}
	})
}

// TestBackwardCompatContributorConfig verifies legacy contributor.* keys still work
func TestBackwardCompatContributorConfig(t *testing.T) {
	// Set up temporary directory
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0755); err != nil {
		t.Fatalf("failed to create .beads dir: %v", err)
	}

	// Initialize database
	dbPath := filepath.Join(beadsDir, "beads.db")
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	store, err := sqlite.New(ctx, dbPath)
	if err != nil {
		t.Fatalf("failed to create store: %v", err)
	}
	defer store.Close()

	// Set LEGACY contributor.* keys (what old versions of bd init --contributor would set)
	if err := store.SetConfig(ctx, "contributor.auto_route", "true"); err != nil {
		t.Fatalf("failed to set contributor.auto_route: %v", err)
	}
	if err := store.SetConfig(ctx, "contributor.planning_repo", "/legacy/planning"); err != nil {
		t.Fatalf("failed to set contributor.planning_repo: %v", err)
	}

	// Simulate backward compat read (as done in create.go)
	routingMode, _ := store.GetConfig(ctx, "routing.mode")
	contributorRepo, _ := store.GetConfig(ctx, "routing.contributor")

	// Fallback to legacy keys
	if routingMode == "" {
		legacyAutoRoute, _ := store.GetConfig(ctx, "contributor.auto_route")
		if legacyAutoRoute == "true" {
			routingMode = "auto"
		}
	}
	if contributorRepo == "" {
		legacyPlanningRepo, _ := store.GetConfig(ctx, "contributor.planning_repo")
		contributorRepo = legacyPlanningRepo
	}

	// Verify backward compat works
	if routingMode != "auto" {
		t.Errorf("backward compat routing.mode = %q, want %q", routingMode, "auto")
	}
	if contributorRepo != "/legacy/planning" {
		t.Errorf("backward compat routing.contributor = %q, want %q", contributorRepo, "/legacy/planning")
	}

	// Build routing config and verify it routes correctly
	config := &routing.RoutingConfig{
		Mode:            routingMode,
		ContributorRepo: contributorRepo,
	}

	targetRepo := routing.DetermineTargetRepo(config, routing.Contributor, ".")
	if targetRepo != "/legacy/planning" {
		t.Errorf("DetermineTargetRepo() with legacy config = %q, want %q", targetRepo, "/legacy/planning")
	}
}

// =============================================================================
// Phase 4: E2E Tests for all sync modes and routing scenarios
// =============================================================================

// contributorRoutingEnv provides a reusable test environment for contributor routing tests
type contributorRoutingEnv struct {
	t           *testing.T
	tmpDir      string
	projectDir  string
	planningDir string
	ctx         context.Context
	cancel      context.CancelFunc
}

// setupContributorRoutingEnv creates isolated project and planning directories with git repos
func setupContributorRoutingEnv(t *testing.T) *contributorRoutingEnv {
	t.Helper()
	tmpDir := t.TempDir()
	projectDir := filepath.Join(tmpDir, "project")
	planningDir := filepath.Join(tmpDir, "planning")

	// Create project directory with git init
	projectBeadsDir := filepath.Join(projectDir, ".beads")
	if err := os.MkdirAll(projectBeadsDir, 0755); err != nil {
		t.Fatalf("failed to create project .beads dir: %v", err)
	}

	// Create planning directory with git init
	planningBeadsDir := filepath.Join(planningDir, ".beads")
	if err := os.MkdirAll(planningBeadsDir, 0755); err != nil {
		t.Fatalf("failed to create planning .beads dir: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)

	return &contributorRoutingEnv{
		t:           t,
		tmpDir:      tmpDir,
		projectDir:  projectDir,
		planningDir: planningDir,
		ctx:         ctx,
		cancel:      cancel,
	}
}

// cleanup releases resources
func (env *contributorRoutingEnv) cleanup() {
	env.cancel()
}

// initProjectStore initializes the project store with routing config
func (env *contributorRoutingEnv) initProjectStore(syncMode string) *sqlite.SQLiteStorage {
	env.t.Helper()
	projectDBPath := filepath.Join(env.projectDir, ".beads", "beads.db")
	store, err := sqlite.New(env.ctx, projectDBPath)
	if err != nil {
		env.t.Fatalf("failed to create project store: %v", err)
	}

	// Set routing config
	if err := store.SetConfig(env.ctx, "routing.mode", "auto"); err != nil {
		env.t.Fatalf("failed to set routing.mode: %v", err)
	}
	if err := store.SetConfig(env.ctx, "routing.contributor", env.planningDir); err != nil {
		env.t.Fatalf("failed to set routing.contributor: %v", err)
	}
	if err := store.SetConfig(env.ctx, "issue_prefix", "proj-"); err != nil {
		env.t.Fatalf("failed to set issue_prefix: %v", err)
	}

	// Set sync mode-specific config
	switch syncMode {
	case "direct":
		// No special config needed - direct is default
	case "sync-branch":
		if err := store.SetConfig(env.ctx, "sync.branch", "beads-sync"); err != nil {
			env.t.Fatalf("failed to set sync.branch: %v", err)
		}
	case "no-db":
		if err := store.SetConfig(env.ctx, "sync.nodb", "true"); err != nil {
			env.t.Fatalf("failed to set sync.nodb: %v", err)
		}
	case "local-only":
		if err := store.SetConfig(env.ctx, "sync.local-only", "true"); err != nil {
			env.t.Fatalf("failed to set sync.local-only: %v", err)
		}
	}

	return store
}

// initPlanningStore initializes the planning store
func (env *contributorRoutingEnv) initPlanningStore() *sqlite.SQLiteStorage {
	env.t.Helper()
	planningDBPath := filepath.Join(env.planningDir, ".beads", "beads.db")
	store, err := sqlite.New(env.ctx, planningDBPath)
	if err != nil {
		env.t.Fatalf("failed to create planning store: %v", err)
	}

	if err := store.SetConfig(env.ctx, "issue_prefix", "plan-"); err != nil {
		env.t.Fatalf("failed to set issue_prefix in planning store: %v", err)
	}

	return store
}

// verifyIssueRouting creates an issue via routing and verifies it lands in the correct store
func verifyIssueRouting(
	t *testing.T,
	ctx context.Context,
	routingConfig *routing.RoutingConfig,
	userRole routing.UserRole,
	targetStore *sqlite.SQLiteStorage,
	otherStore *sqlite.SQLiteStorage,
	expectedRepoPath string,
	description string,
) {
	t.Helper()

	// Verify routing decision
	targetRepo := routing.DetermineTargetRepo(routingConfig, userRole, ".")
	if targetRepo != expectedRepoPath {
		t.Errorf("%s: DetermineTargetRepo() = %q, want %q", description, targetRepo, expectedRepoPath)
		return
	}

	// Create issue in target store
	issue := &types.Issue{
		Title:     "Test " + description,
		IssueType: types.TypeTask,
		Status:    types.StatusOpen,
		Priority:  2,
	}

	if err := targetStore.CreateIssue(ctx, issue, "test"); err != nil {
		t.Fatalf("%s: failed to create issue: %v", description, err)
	}

	// Verify issue exists in target store
	retrieved, err := targetStore.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("%s: failed to get issue from target store: %v", description, err)
	}
	if retrieved == nil {
		t.Errorf("%s: issue not found in target store", description)
		return
	}

	// Verify issue does NOT exist in other store (isolation check)
	if otherStore != nil {
		otherIssue, _ := otherStore.GetIssue(ctx, issue.ID)
		if otherIssue != nil {
			t.Errorf("%s: issue should NOT exist in other store (isolation failure)", description)
		}
	}
}

// TestContributorRoutingDirect verifies routing works in direct mode (no sync-branch, no no-db)
func TestContributorRoutingDirect(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("direct")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Build routing config from stored values
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".",
	}

	verifyIssueRouting(
		t, env.ctx, routingConfig, routing.Contributor,
		planningStore, projectStore, env.planningDir,
		"direct mode contributor routing",
	)
}

// TestContributorRoutingSyncBranch verifies routing works when sync.branch is configured
func TestContributorRoutingSyncBranch(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("sync-branch")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Verify sync.branch is set
	syncBranch, _ := projectStore.GetConfig(env.ctx, "sync.branch")
	if syncBranch != "beads-sync" {
		t.Fatalf("sync.branch not set correctly: got %q, want %q", syncBranch, "beads-sync")
	}

	// Build routing config
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".",
	}

	verifyIssueRouting(
		t, env.ctx, routingConfig, routing.Contributor,
		planningStore, projectStore, env.planningDir,
		"sync-branch mode contributor routing",
	)
}

// TestContributorRoutingNoDb verifies routing works when sync.nodb is enabled
func TestContributorRoutingNoDb(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("no-db")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Verify no-db is set
	nodb, _ := projectStore.GetConfig(env.ctx, "sync.nodb")
	if nodb != "true" {
		t.Fatalf("sync.nodb not set correctly: got %q, want %q", nodb, "true")
	}

	// Build routing config
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".",
	}

	verifyIssueRouting(
		t, env.ctx, routingConfig, routing.Contributor,
		planningStore, projectStore, env.planningDir,
		"no-db mode contributor routing",
	)
}

// TestContributorRoutingDaemon verifies routing works when daemon mode is active.
// Note: In practice, daemon mode is bypassed for routed issues (T013), but the
// routing decision should still be correct.
func TestContributorRoutingDaemon(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("direct")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Build routing config
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".",
	}

	// Verify routing decision is correct (daemon mode bypasses RPC for routed issues)
	// The key behavior is: when routing to a different repo, daemon is bypassed (T013)
	targetRepo := routing.DetermineTargetRepo(routingConfig, routing.Contributor, ".")
	if targetRepo != env.planningDir {
		t.Errorf("daemon mode routing decision: got %q, want %q", targetRepo, env.planningDir)
	}

	// Verify we can create issue directly in planning store (simulating daemon bypass)
	issue := &types.Issue{
		Title:     "Test daemon mode routing bypass",
		IssueType: types.TypeTask,
		Status:    types.StatusOpen,
		Priority:  2,
	}

	if err := planningStore.CreateIssue(env.ctx, issue, "test"); err != nil {
		t.Fatalf("failed to create issue in planning store: %v", err)
	}

	// Verify isolation
	projectIssue, _ := projectStore.GetIssue(env.ctx, issue.ID)
	if projectIssue != nil {
		t.Error("issue should NOT exist in project store (daemon bypass isolation failure)")
	}
}

// TestContributorRoutingLocalOnly verifies routing works when local-only mode is enabled
func TestContributorRoutingLocalOnly(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("local-only")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Verify local-only is set
	localOnly, _ := projectStore.GetConfig(env.ctx, "sync.local-only")
	if localOnly != "true" {
		t.Fatalf("sync.local-only not set correctly: got %q, want %q", localOnly, "true")
	}

	// Build routing config
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".",
	}

	verifyIssueRouting(
		t, env.ctx, routingConfig, routing.Contributor,
		planningStore, projectStore, env.planningDir,
		"local-only mode contributor routing",
	)
}

// TestMaintainerRoutingUnaffected verifies maintainers' issues stay in the project repo
func TestMaintainerRoutingUnaffected(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("direct")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Build routing config
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:            mode,
		ContributorRepo: contributorPath,
		MaintainerRepo:  ".", // Maintainer stays local
	}

	// Verify maintainer routes to local repo (.)
	targetRepo := routing.DetermineTargetRepo(routingConfig, routing.Maintainer, env.projectDir)
	if targetRepo != "." {
		t.Errorf("maintainer routing: got %q, want %q", targetRepo, ".")
	}

	// Create issue in project store (maintainer's issue stays local)
	issue := &types.Issue{
		Title:     "Test maintainer issue stays local",
		IssueType: types.TypeTask,
		Status:    types.StatusOpen,
		Priority:  2,
	}

	if err := projectStore.CreateIssue(env.ctx, issue, "test"); err != nil {
		t.Fatalf("failed to create issue in project store: %v", err)
	}

	// Verify issue exists in project store
	retrieved, err := projectStore.GetIssue(env.ctx, issue.ID)
	if err != nil || retrieved == nil {
		t.Fatalf("maintainer issue not found in project store: %v", err)
	}

	// Verify issue does NOT exist in planning store
	planningIssue, _ := planningStore.GetIssue(env.ctx, issue.ID)
	if planningIssue != nil {
		t.Error("maintainer issue should NOT exist in planning store (isolation failure)")
	}
}

// TestExplicitRepoOverride verifies --repo flag takes precedence over auto-routing
func TestExplicitRepoOverride(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	projectStore := env.initProjectStore("direct")
	defer projectStore.Close()

	planningStore := env.initPlanningStore()
	defer planningStore.Close()

	// Create a third "override" directory
	overrideDir := filepath.Join(env.tmpDir, "override")
	overrideBeadsDir := filepath.Join(overrideDir, ".beads")
	if err := os.MkdirAll(overrideBeadsDir, 0755); err != nil {
		t.Fatalf("failed to create override .beads dir: %v", err)
	}

	overrideDBPath := filepath.Join(overrideBeadsDir, "beads.db")
	overrideStore, err := sqlite.New(env.ctx, overrideDBPath)
	if err != nil {
		t.Fatalf("failed to create override store: %v", err)
	}
	defer overrideStore.Close()

	if err := overrideStore.SetConfig(env.ctx, "issue_prefix", "over-"); err != nil {
		t.Fatalf("failed to set issue_prefix in override store: %v", err)
	}

	// Build routing config WITH explicit override
	mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
	contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

	routingConfig := &routing.RoutingConfig{
		Mode:             mode,
		ContributorRepo:  contributorPath,
		MaintainerRepo:   ".",
		ExplicitOverride: overrideDir, // --repo /path/to/override
	}

	// Verify explicit override takes precedence over auto-routing
	targetRepo := routing.DetermineTargetRepo(routingConfig, routing.Contributor, env.projectDir)
	if targetRepo != overrideDir {
		t.Errorf("explicit repo override: got %q, want %q", targetRepo, overrideDir)
	}

	// Verify maintainer also respects explicit override
	targetRepo = routing.DetermineTargetRepo(routingConfig, routing.Maintainer, env.projectDir)
	if targetRepo != overrideDir {
		t.Errorf("explicit repo override for maintainer: got %q, want %q", targetRepo, overrideDir)
	}

	// Create issue in override store
	issue := &types.Issue{
		Title:     "Test explicit override",
		IssueType: types.TypeTask,
		Status:    types.StatusOpen,
		Priority:  2,
	}

	if err := overrideStore.CreateIssue(env.ctx, issue, "test"); err != nil {
		t.Fatalf("failed to create issue in override store: %v", err)
	}

	// Verify issue exists ONLY in override store
	retrieved, _ := overrideStore.GetIssue(env.ctx, issue.ID)
	if retrieved == nil {
		t.Error("issue not found in override store")
	}

	projectIssue, _ := projectStore.GetIssue(env.ctx, issue.ID)
	if projectIssue != nil {
		t.Error("issue should NOT exist in project store")
	}

	planningIssue, _ := planningStore.GetIssue(env.ctx, issue.ID)
	if planningIssue != nil {
		t.Error("issue should NOT exist in planning store")
	}
}

// TestBEADS_DIRPrecedence verifies BEADS_DIR env var takes precedence over routing config
func TestBEADS_DIRPrecedence(t *testing.T) {
	env := setupContributorRoutingEnv(t)
	defer env.cleanup()

	// Create an external beads directory (simulating BEADS_DIR target)
	externalDir := filepath.Join(env.tmpDir, "external")
	externalBeadsDir := filepath.Join(externalDir, ".beads")
	if err := os.MkdirAll(externalBeadsDir, 0755); err != nil {
		t.Fatalf("failed to create external .beads dir: %v", err)
	}

	externalDBPath := filepath.Join(externalBeadsDir, "beads.db")
	externalStore, err := sqlite.New(env.ctx, externalDBPath)
	if err != nil {
		t.Fatalf("failed to create external store: %v", err)
	}
	defer externalStore.Close()

	if err := externalStore.SetConfig(env.ctx, "issue_prefix", "ext-"); err != nil {
		t.Fatalf("failed to set issue_prefix in external store: %v", err)
	}

	// Set BEADS_DIR to external directory
	t.Setenv("BEADS_DIR", externalBeadsDir)

	// The key insight: BEADS_DIR is checked in FindBeadsDir() BEFORE routing config is read.
	// This test verifies the precedence order documented in CONTRIBUTOR_NAMESPACE_ISOLATION.md:
	// 1. BEADS_DIR environment variable (highest precedence)
	// 2. --repo flag (explicit override)
	// 3. Auto-routing based on user role
	// 4. Current directory's .beads/ (default)

	// When BEADS_DIR is set, all operations should use that directory,
	// regardless of routing config.

	// Create issue in external store
	issue := &types.Issue{
		Title:     "Test BEADS_DIR precedence",
		IssueType: types.TypeTask,
		Status:    types.StatusOpen,
		Priority:  2,
	}

	if err := externalStore.CreateIssue(env.ctx, issue, "test"); err != nil {
		t.Fatalf("failed to create issue in external store: %v", err)
	}

	// Verify issue exists in external store
	retrieved, err := externalStore.GetIssue(env.ctx, issue.ID)
	if err != nil || retrieved == nil {
		t.Fatalf("issue not found in external store: %v", err)
	}

	// Initialize project store (should be ignored when BEADS_DIR is set)
	projectStore := env.initProjectStore("direct")
	defer projectStore.Close()

	// Verify issue does NOT exist in project store
	projectIssue, _ := projectStore.GetIssue(env.ctx, issue.ID)
	if projectIssue != nil {
		t.Error("issue should NOT exist in project store when BEADS_DIR is set")
	}
}

// TestExplicitRoleOverride verifies git config beads.role takes precedence over URL detection
func TestExplicitRoleOverride(t *testing.T) {
	// Test that beads.role=maintainer in git config forces maintainer role
	// even when the remote URL would indicate contributor

	tests := []struct {
		name           string
		configRole     string
		expectedRole   routing.UserRole
		description    string
	}{
		{
			name:         "explicit maintainer",
			configRole:   "maintainer",
			expectedRole: routing.Maintainer,
			description:  "git config beads.role=maintainer should force maintainer",
		},
		{
			name:         "explicit contributor",
			configRole:   "contributor",
			expectedRole: routing.Contributor,
			description:  "git config beads.role=contributor should force contributor",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Use the routing package's gitCommandRunner stub mechanism
			// This is tested more thoroughly in routing_test.go, but we verify
			// the integration here

			// Build a routing config assuming the role override
			routingConfig := &routing.RoutingConfig{
				Mode:            "auto",
				ContributorRepo: "/path/to/planning",
				MaintainerRepo:  ".",
			}

			// Verify routing behavior based on role
			var expectedRepo string
			if tt.expectedRole == routing.Maintainer {
				expectedRepo = "."
			} else {
				expectedRepo = "/path/to/planning"
			}

			targetRepo := routing.DetermineTargetRepo(routingConfig, tt.expectedRole, ".")
			if targetRepo != expectedRepo {
				t.Errorf("%s: got target repo %q, want %q", tt.description, targetRepo, expectedRepo)
			}
		})
	}
}

// TestRoutingWithAllSyncModes is a table-driven test covering all sync mode combinations
func TestRoutingWithAllSyncModes(t *testing.T) {
	syncModes := []struct {
		name       string
		mode       string
		configKey  string
		configVal  string
		extraCheck func(t *testing.T, store *sqlite.SQLiteStorage, ctx context.Context)
	}{
		{
			name:      "direct",
			mode:      "direct",
			configKey: "",
			configVal: "",
		},
		{
			name:      "sync-branch",
			mode:      "sync-branch",
			configKey: "sync.branch",
			configVal: "beads-sync",
			extraCheck: func(t *testing.T, store *sqlite.SQLiteStorage, ctx context.Context) {
				val, _ := store.GetConfig(ctx, "sync.branch")
				if val != "beads-sync" {
					t.Errorf("sync.branch = %q, want %q", val, "beads-sync")
				}
			},
		},
		{
			name:      "no-db",
			mode:      "no-db",
			configKey: "sync.nodb",
			configVal: "true",
		},
		{
			name:      "local-only",
			mode:      "local-only",
			configKey: "sync.local-only",
			configVal: "true",
		},
	}

	for _, sm := range syncModes {
		t.Run(sm.name, func(t *testing.T) {
			env := setupContributorRoutingEnv(t)
			defer env.cleanup()

			projectStore := env.initProjectStore(sm.mode)
			defer projectStore.Close()

			planningStore := env.initPlanningStore()
			defer planningStore.Close()

			// Run extra check if provided
			if sm.extraCheck != nil {
				sm.extraCheck(t, projectStore, env.ctx)
			}

			// Build routing config
			mode, _ := projectStore.GetConfig(env.ctx, "routing.mode")
			contributorPath, _ := projectStore.GetConfig(env.ctx, "routing.contributor")

			routingConfig := &routing.RoutingConfig{
				Mode:            mode,
				ContributorRepo: contributorPath,
				MaintainerRepo:  ".",
			}

			// Verify contributor routing
			targetRepo := routing.DetermineTargetRepo(routingConfig, routing.Contributor, ".")
			if targetRepo != env.planningDir {
				t.Errorf("sync mode %s: contributor target = %q, want %q",
					sm.name, targetRepo, env.planningDir)
			}

			// Verify maintainer routing
			targetRepo = routing.DetermineTargetRepo(routingConfig, routing.Maintainer, ".")
			if targetRepo != "." {
				t.Errorf("sync mode %s: maintainer target = %q, want %q",
					sm.name, targetRepo, ".")
			}
		})
	}
}
