//go:build integration

// Package cmd contains integration tests for the rig command.
//
// Run with: go test -tags=integration ./internal/cmd -run TestRigAdd -v
package cmd

import (
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/steveyegge/gastown/internal/beads"
	"github.com/steveyegge/gastown/internal/config"
	"github.com/steveyegge/gastown/internal/git"
	"github.com/steveyegge/gastown/internal/rig"
)

// createTestGitRepo creates a minimal git repository for testing.
// Returns the path to the bare repo URL (suitable for cloning).
func createTestGitRepo(t *testing.T, name string) string {
	t.Helper()

	// Create a regular repo with initial commit
	repoDir := filepath.Join(t.TempDir(), name)
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatalf("mkdir repo: %v", err)
	}

	// Initialize git repo with explicit main branch
	// (system default may vary, causing checkout failures)
	cmds := [][]string{
		{"git", "init", "--initial-branch=main"},
		{"git", "config", "user.email", "test@test.com"},
		{"git", "config", "user.name", "Test User"},
	}
	for _, args := range cmds {
		cmd := exec.Command(args[0], args[1:]...)
		cmd.Dir = repoDir
		if out, err := cmd.CombinedOutput(); err != nil {
			t.Fatalf("git %v: %v\n%s", args, err, out)
		}
	}

	// Create initial file and commit
	readmePath := filepath.Join(repoDir, "README.md")
	if err := os.WriteFile(readmePath, []byte("# Test Repo\n"), 0644); err != nil {
		t.Fatalf("write README: %v", err)
	}

	commitCmds := [][]string{
		{"git", "add", "."},
		{"git", "commit", "-m", "Initial commit"},
	}
	for _, args := range commitCmds {
		cmd := exec.Command(args[0], args[1:]...)
		cmd.Dir = repoDir
		if out, err := cmd.CombinedOutput(); err != nil {
			t.Fatalf("git %v: %v\n%s", args, err, out)
		}
	}

	// Return the path as a file:// URL
	return repoDir
}

// setupTestTown creates a minimal Gas Town workspace for testing.
// Returns townRoot and a cleanup function.
func setupTestTown(t *testing.T) string {
	t.Helper()

	townRoot := t.TempDir()

	// Create mayor directory (required for rigs.json)
	mayorDir := filepath.Join(townRoot, "mayor")
	if err := os.MkdirAll(mayorDir, 0755); err != nil {
		t.Fatalf("mkdir mayor: %v", err)
	}

	// Create empty rigs.json
	rigsPath := filepath.Join(mayorDir, "rigs.json")
	rigsConfig := &config.RigsConfig{
		Version: 1,
		Rigs:    make(map[string]config.RigEntry),
	}
	if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
		t.Fatalf("save rigs.json: %v", err)
	}

	// Create .beads directory for routes
	beadsDir := filepath.Join(townRoot, ".beads")
	if err := os.MkdirAll(beadsDir, 0755); err != nil {
		t.Fatalf("mkdir .beads: %v", err)
	}

	return townRoot
}

// mockBdCommand creates a fake bd binary that simulates bd behavior.
// This avoids needing bd installed for tests.
func mockBdCommand(t *testing.T) {
	t.Helper()

	binDir := t.TempDir()
	bdPath := filepath.Join(binDir, "bd")

	// Create a script that simulates bd init and other commands
	script := `#!/bin/sh
# Mock bd for testing

case "$1" in
  init)
    # Create .beads directory and config.yaml
    mkdir -p .beads
    prefix="gt"
    for arg in "$@"; do
      case "$arg" in
        --prefix=*) prefix="${arg#--prefix=}" ;;
        --prefix)
          # Next arg is the prefix
          shift
          if [ -n "$1" ] && [ "$1" != "--"* ]; then
            prefix="$1"
          fi
          ;;
      esac
      shift
    done
    # Handle positional --prefix VALUE
    shift  # skip 'init'
    while [ $# -gt 0 ]; do
      case "$1" in
        --prefix)
          shift
          prefix="$1"
          ;;
      esac
      shift
    done
    echo "prefix: $prefix" > .beads/config.yaml
    exit 0
    ;;
  migrate)
    exit 0
    ;;
  show)
    echo '{"error":"not found"}' >&2
    exit 1
    ;;
  create)
    # Return minimal JSON for agent bead creation
    echo '{}'
    exit 0
    ;;
  mol|list)
    exit 0
    ;;
  *)
    exit 0
    ;;
esac
`
	if err := os.WriteFile(bdPath, []byte(script), 0755); err != nil {
		t.Fatalf("write mock bd: %v", err)
	}

	// Prepend to PATH
	t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
}

// TestRigAddCreatesCorrectStructure verifies that gt rig add creates
// the expected directory structure.
func TestRigAddCreatesCorrectStructure(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "testproject")

	// Load rigs config
	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	// Create rig manager and add rig
	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	_, err = mgr.AddRig(rig.AddRigOptions{
		Name:        "testrig",
		GitURL:      gitURL,
		BeadsPrefix: "tr",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	rigPath := filepath.Join(townRoot, "testrig")

	// Verify directory structure
	expectedDirs := []string{
		"",                // rig root
		"mayor",           // mayor container
		"mayor/rig",       // mayor clone
		"refinery",        // refinery container
		"refinery/rig",    // refinery worktree
		"witness",         // witness dir
		"polecats",        // polecats dir
		"crew",            // crew dir
		".beads",          // beads dir
		"plugins",         // plugins dir
	}

	for _, dir := range expectedDirs {
		path := filepath.Join(rigPath, dir)
		info, err := os.Stat(path)
		if err != nil {
			t.Errorf("expected directory %s to exist: %v", dir, err)
			continue
		}
		if !info.IsDir() {
			t.Errorf("expected %s to be a directory", dir)
		}
	}

	// Verify config.json exists
	configPath := filepath.Join(rigPath, "config.json")
	if _, err := os.Stat(configPath); err != nil {
		t.Errorf("config.json not found: %v", err)
	}

	// Verify .repo.git (bare repo) exists
	bareRepoPath := filepath.Join(rigPath, ".repo.git")
	if _, err := os.Stat(bareRepoPath); err != nil {
		t.Errorf(".repo.git not found: %v", err)
	}

	// Verify mayor/rig is a git repo
	mayorRigPath := filepath.Join(rigPath, "mayor", "rig")
	gitDirPath := filepath.Join(mayorRigPath, ".git")
	if _, err := os.Stat(gitDirPath); err != nil {
		t.Errorf("mayor/rig/.git not found: %v", err)
	}

	// Verify refinery/rig is a git worktree (has .git file pointing to bare repo)
	refineryRigPath := filepath.Join(rigPath, "refinery", "rig")
	refineryGitPath := filepath.Join(refineryRigPath, ".git")
	info, err := os.Stat(refineryGitPath)
	if err != nil {
		t.Errorf("refinery/rig/.git not found: %v", err)
	} else if info.IsDir() {
		t.Errorf("refinery/rig/.git should be a file (worktree), not a directory")
	}

	// Verify Claude settings are created in correct locations (outside git repos).
	// Settings in parent directories are inherited by agents via directory traversal,
	// without polluting the source repos.
	expectedSettings := []struct {
		path string
		desc string
	}{
		{filepath.Join(rigPath, "witness", ".claude", "settings.json"), "witness/.claude/settings.json"},
		{filepath.Join(rigPath, "refinery", ".claude", "settings.json"), "refinery/.claude/settings.json"},
		{filepath.Join(rigPath, "crew", ".claude", "settings.json"), "crew/.claude/settings.json"},
		{filepath.Join(rigPath, "polecats", ".claude", "settings.json"), "polecats/.claude/settings.json"},
	}

	for _, s := range expectedSettings {
		if _, err := os.Stat(s.path); err != nil {
			t.Errorf("%s not found: %v", s.desc, err)
		}
	}

	// Verify settings are NOT created inside source repos (these would be wrong)
	wrongLocations := []struct {
		path string
		desc string
	}{
		{filepath.Join(rigPath, "witness", "rig", ".claude", "settings.json"), "witness/rig/.claude (inside source repo)"},
		{filepath.Join(rigPath, "refinery", "rig", ".claude", "settings.json"), "refinery/rig/.claude (inside source repo)"},
	}

	for _, w := range wrongLocations {
		if _, err := os.Stat(w.path); err == nil {
			t.Errorf("%s should NOT exist (settings would pollute source repo)", w.desc)
		}
	}
}

// TestRigAddInitializesBeads verifies that beads is initialized with
// the correct prefix.
func TestRigAddInitializesBeads(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "beadstest")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	newRig, err := mgr.AddRig(rig.AddRigOptions{
		Name:        "beadstest",
		GitURL:      gitURL,
		BeadsPrefix: "bt",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	// Verify rig config has correct prefix
	if newRig.Config == nil {
		t.Fatal("rig.Config is nil")
	}
	if newRig.Config.Prefix != "bt" {
		t.Errorf("rig.Config.Prefix = %q, want %q", newRig.Config.Prefix, "bt")
	}

	// Verify .beads directory was created
	beadsDir := filepath.Join(townRoot, "beadstest", ".beads")
	if _, err := os.Stat(beadsDir); err != nil {
		t.Errorf(".beads directory not found: %v", err)
	}

	// Verify config.yaml exists with correct prefix
	configPath := filepath.Join(beadsDir, "config.yaml")
	if _, err := os.Stat(configPath); err != nil {
		t.Errorf(".beads/config.yaml not found: %v", err)
	} else {
		content, err := os.ReadFile(configPath)
		if err != nil {
			t.Errorf("reading config.yaml: %v", err)
		} else if !strings.Contains(string(content), "prefix: bt") && !strings.Contains(string(content), "prefix:bt") {
			t.Errorf("config.yaml doesn't contain expected prefix, got: %s", string(content))
		}
	}
}

// TestRigAddUpdatesRoutes verifies that routes.jsonl is updated
// with the new rig's route.
func TestRigAddUpdatesRoutes(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "routetest")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	newRig, err := mgr.AddRig(rig.AddRigOptions{
		Name:        "routetest",
		GitURL:      gitURL,
		BeadsPrefix: "rt",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	// Append route to routes.jsonl (this is done by the CLI command, not AddRig)
	// The CLI command in runRigAdd calls beads.AppendRoute after AddRig succeeds
	if newRig.Config != nil && newRig.Config.Prefix != "" {
		route := beads.Route{
			Prefix: newRig.Config.Prefix + "-",
			Path:   "routetest",
		}
		if err := beads.AppendRoute(townRoot, route); err != nil {
			t.Fatalf("AppendRoute: %v", err)
		}
	}

	// Save rigs config (normally done by the command)
	if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
		t.Fatalf("save rigs.json: %v", err)
	}

	// Load routes and verify the new route exists
	townBeadsDir := filepath.Join(townRoot, ".beads")
	routes, err := beads.LoadRoutes(townBeadsDir)
	if err != nil {
		t.Fatalf("LoadRoutes: %v", err)
	}

	// Find route for our rig
	var foundRoute *beads.Route
	for _, r := range routes {
		if r.Prefix == "rt-" {
			foundRoute = &r
			break
		}
	}

	if foundRoute == nil {
		t.Error("route with prefix 'rt-' not found in routes.jsonl")
		t.Logf("routes: %+v", routes)
	} else {
		// The path should point to the rig (or mayor/rig if .beads is tracked in source)
		if !strings.HasPrefix(foundRoute.Path, "routetest") {
			t.Errorf("route path = %q, want prefix 'routetest'", foundRoute.Path)
		}
	}
}

// TestRigAddUpdatesRigsJson verifies that rigs.json is updated
// with the new rig entry.
func TestRigAddUpdatesRigsJson(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "jsontest")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	_, err = mgr.AddRig(rig.AddRigOptions{
		Name:        "jsontest",
		GitURL:      gitURL,
		BeadsPrefix: "jt",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	// Save rigs config (normally done by the command)
	if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
		t.Fatalf("save rigs.json: %v", err)
	}

	// Reload and verify
	rigsConfig2, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("reload rigs.json: %v", err)
	}

	entry, ok := rigsConfig2.Rigs["jsontest"]
	if !ok {
		t.Error("rig 'jsontest' not found in rigs.json")
		t.Logf("rigs: %+v", rigsConfig2.Rigs)
	} else {
		if entry.GitURL != gitURL {
			t.Errorf("GitURL = %q, want %q", entry.GitURL, gitURL)
		}
		if entry.BeadsConfig == nil {
			t.Error("BeadsConfig is nil")
		} else if entry.BeadsConfig.Prefix != "jt" {
			t.Errorf("BeadsConfig.Prefix = %q, want %q", entry.BeadsConfig.Prefix, "jt")
		}
	}
}

// TestRigAddDerivesPrefix verifies that when no prefix is specified,
// one is derived from the rig name.
func TestRigAddDerivesPrefix(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "myproject")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	newRig, err := mgr.AddRig(rig.AddRigOptions{
		Name:   "myproject",
		GitURL: gitURL,
		// No BeadsPrefix - should be derived
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	// For a single-word name like "myproject", the prefix should be first 2 chars
	if newRig.Config.Prefix != "my" {
		t.Errorf("derived prefix = %q, want %q", newRig.Config.Prefix, "my")
	}
}

// TestRigAddCreatesRigConfig verifies that config.json contains
// the correct rig configuration.
func TestRigAddCreatesRigConfig(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "configtest")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	_, err = mgr.AddRig(rig.AddRigOptions{
		Name:        "configtest",
		GitURL:      gitURL,
		BeadsPrefix: "ct",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	// Read and verify config.json
	configPath := filepath.Join(townRoot, "configtest", "config.json")
	data, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatalf("reading config.json: %v", err)
	}

	var rigCfg rig.RigConfig
	if err := json.Unmarshal(data, &rigCfg); err != nil {
		t.Fatalf("parsing config.json: %v", err)
	}

	if rigCfg.Type != "rig" {
		t.Errorf("Type = %q, want 'rig'", rigCfg.Type)
	}
	if rigCfg.Name != "configtest" {
		t.Errorf("Name = %q, want 'configtest'", rigCfg.Name)
	}
	if rigCfg.GitURL != gitURL {
		t.Errorf("GitURL = %q, want %q", rigCfg.GitURL, gitURL)
	}
	if rigCfg.Beads == nil {
		t.Error("Beads config is nil")
	} else if rigCfg.Beads.Prefix != "ct" {
		t.Errorf("Beads.Prefix = %q, want 'ct'", rigCfg.Beads.Prefix)
	}
	if rigCfg.DefaultBranch == "" {
		t.Error("DefaultBranch is empty")
	}
}

// TestRigAddCreatesAgentDirs verifies that agent state files are created.
func TestRigAddCreatesAgentDirs(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "agenttest")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	_, err = mgr.AddRig(rig.AddRigOptions{
		Name:        "agenttest",
		GitURL:      gitURL,
		BeadsPrefix: "at",
	})
	if err != nil {
		t.Fatalf("AddRig: %v", err)
	}

	rigPath := filepath.Join(townRoot, "agenttest")

	// Verify agent directories exist (state.json files are no longer created)
	expectedDirs := []string{
		"witness",
		"refinery",
		"mayor",
	}

	for _, dir := range expectedDirs {
		path := filepath.Join(rigPath, dir)
		info, err := os.Stat(path)
		if err != nil {
			t.Errorf("expected directory %s to exist: %v", dir, err)
		} else if !info.IsDir() {
			t.Errorf("expected %s to be a directory", dir)
		}
	}
}

// TestRigAddRejectsInvalidNames verifies that rig names with invalid
// characters are rejected.
func TestRigAddRejectsInvalidNames(t *testing.T) {
	mockBdCommand(t)
	townRoot := setupTestTown(t)
	gitURL := createTestGitRepo(t, "validname")

	rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsPath)
	if err != nil {
		t.Fatalf("load rigs.json: %v", err)
	}

	g := git.NewGit(townRoot)
	mgr := rig.NewManager(townRoot, rigsConfig, g)

	// Characters that break agent ID parsing (hyphens, dots, spaces)
	// Note: underscores are allowed
	invalidNames := []string{
		"my-rig",       // hyphens break agent ID parsing
		"my.rig",       // dots break parsing
		"my rig",       // spaces are invalid
		"my-multi-rig", // multiple hyphens
	}

	for _, name := range invalidNames {
		t.Run(name, func(t *testing.T) {
			_, err := mgr.AddRig(rig.AddRigOptions{
				Name:   name,
				GitURL: gitURL,
			})
			if err == nil {
				t.Errorf("AddRig(%q) should have failed", name)
			} else if !strings.Contains(err.Error(), "invalid characters") {
				t.Errorf("AddRig(%q) error = %v, want 'invalid characters'", name, err)
			}
		})
	}
}
