//go:build integration

// Package cmd contains integration tests for hook slot verification.
//
// Run with: go test -tags=integration ./internal/cmd -run TestHookSlot -v
package cmd

import (
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/steveyegge/gastown/internal/beads"
)

// setupHookTestTown creates a minimal Gas Town with a polecat for testing hooks.
// Returns townRoot and the path to the polecat's worktree.
func setupHookTestTown(t *testing.T) (townRoot, polecatDir string) {
	t.Helper()

	townRoot = t.TempDir()

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

	// Create routes.jsonl
	routes := []beads.Route{
		{Prefix: "hq-", Path: "."},                     // Town-level beads
		{Prefix: "gt-", Path: "gastown/mayor/rig"},     // Gastown rig
	}
	if err := beads.WriteRoutes(townBeadsDir, routes); err != nil {
		t.Fatalf("write routes: %v", err)
	}

	// Create gastown rig structure
	gasRigPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
	if err := os.MkdirAll(gasRigPath, 0755); err != nil {
		t.Fatalf("mkdir gastown: %v", err)
	}

	// Create gastown .beads directory with its own config
	gasBeadsDir := filepath.Join(gasRigPath, ".beads")
	if err := os.MkdirAll(gasBeadsDir, 0755); err != nil {
		t.Fatalf("mkdir gastown .beads: %v", err)
	}
	if err := os.WriteFile(filepath.Join(gasBeadsDir, "config.yaml"), []byte("prefix: gt\n"), 0644); err != nil {
		t.Fatalf("write gastown config: %v", err)
	}

	// Create polecat worktree with redirect
	polecatDir = filepath.Join(townRoot, "gastown", "polecats", "toast")
	if err := os.MkdirAll(polecatDir, 0755); err != nil {
		t.Fatalf("mkdir polecats: %v", err)
	}

	// Create redirect file for polecat -> mayor/rig/.beads
	polecatBeadsDir := filepath.Join(polecatDir, ".beads")
	if err := os.MkdirAll(polecatBeadsDir, 0755); err != nil {
		t.Fatalf("mkdir polecat .beads: %v", err)
	}
	redirectContent := "../../mayor/rig/.beads"
	if err := os.WriteFile(filepath.Join(polecatBeadsDir, "redirect"), []byte(redirectContent), 0644); err != nil {
		t.Fatalf("write redirect: %v", err)
	}

	return townRoot, polecatDir
}

// initBeadsDB initializes the beads database by running bd init.
func initBeadsDB(t *testing.T, dir string) {
	t.Helper()

	cmd := exec.Command("bd", "--no-daemon", "init")
	cmd.Dir = dir
	if output, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("bd init failed: %v\n%s", err, output)
	}
}

// TestHookSlot_BasicHook verifies that a bead can be hooked to an agent.
func TestHookSlot_BasicHook(t *testing.T) {
	// Skip if bd is not available
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)
	_ = townRoot // Not used directly but shows test context

	// Initialize beads in the rig
	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	b := beads.New(rigDir)

	// Create a test bead
	issue, err := b.Create(beads.CreateOptions{
		Title:    "Test task for hooking",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead: %v", err)
	}
	t.Logf("Created bead: %s", issue.ID)

	// Hook the bead to the polecat
	agentID := "gastown/polecats/toast"
	status := beads.StatusHooked
	if err := b.Update(issue.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agentID,
	}); err != nil {
		t.Fatalf("hook bead: %v", err)
	}

	// Verify the bead is hooked
	hookedBeads, err := b.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agentID,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list hooked beads: %v", err)
	}

	if len(hookedBeads) != 1 {
		t.Errorf("expected 1 hooked bead, got %d", len(hookedBeads))
	}

	if len(hookedBeads) > 0 && hookedBeads[0].ID != issue.ID {
		t.Errorf("hooked bead ID = %s, want %s", hookedBeads[0].ID, issue.ID)
	}
}

// TestHookSlot_Singleton verifies that only one bead can be hooked per agent.
func TestHookSlot_Singleton(t *testing.T) {
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)
	_ = townRoot

	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	b := beads.New(rigDir)
	agentID := "gastown/polecats/toast"
	status := beads.StatusHooked

	// Create and hook first bead
	issue1, err := b.Create(beads.CreateOptions{
		Title:    "First task",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create first bead: %v", err)
	}

	if err := b.Update(issue1.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agentID,
	}); err != nil {
		t.Fatalf("hook first bead: %v", err)
	}

	// Create second bead
	issue2, err := b.Create(beads.CreateOptions{
		Title:    "Second task",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create second bead: %v", err)
	}

	// Hook second bead to same agent
	if err := b.Update(issue2.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agentID,
	}); err != nil {
		t.Fatalf("hook second bead: %v", err)
	}

	// Query hooked beads - both should be hooked (bd allows multiple)
	// The singleton constraint is enforced by gt hook, not bd itself
	hookedBeads, err := b.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agentID,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list hooked beads: %v", err)
	}

	t.Logf("Found %d hooked beads for agent %s", len(hookedBeads), agentID)
	for _, h := range hookedBeads {
		t.Logf("  - %s: %s", h.ID, h.Title)
	}

	// The test documents actual behavior: bd allows multiple hooked beads
	// The gt hook command enforces singleton behavior
	if len(hookedBeads) != 2 {
		t.Errorf("expected 2 hooked beads (bd allows multiple), got %d", len(hookedBeads))
	}
}

// TestHookSlot_Unhook verifies that a bead can be unhooked by changing status.
func TestHookSlot_Unhook(t *testing.T) {
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)
	_ = townRoot

	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	b := beads.New(rigDir)
	agentID := "gastown/polecats/toast"

	// Create and hook a bead
	issue, err := b.Create(beads.CreateOptions{
		Title:    "Task to unhook",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead: %v", err)
	}

	status := beads.StatusHooked
	if err := b.Update(issue.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agentID,
	}); err != nil {
		t.Fatalf("hook bead: %v", err)
	}

	// Unhook by setting status back to open
	openStatus := "open"
	if err := b.Update(issue.ID, beads.UpdateOptions{
		Status: &openStatus,
	}); err != nil {
		t.Fatalf("unhook bead: %v", err)
	}

	// Verify no hooked beads remain
	hookedBeads, err := b.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agentID,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list hooked beads: %v", err)
	}

	if len(hookedBeads) != 0 {
		t.Errorf("expected 0 hooked beads after unhook, got %d", len(hookedBeads))
	}
}

// TestHookSlot_DifferentAgents verifies that different agents can have different hooks.
func TestHookSlot_DifferentAgents(t *testing.T) {
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)

	// Create second polecat directory
	polecat2Dir := filepath.Join(townRoot, "gastown", "polecats", "nux")
	if err := os.MkdirAll(polecat2Dir, 0755); err != nil {
		t.Fatalf("mkdir polecat2: %v", err)
	}

	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	b := beads.New(rigDir)
	agent1 := "gastown/polecats/toast"
	agent2 := "gastown/polecats/nux"
	status := beads.StatusHooked

	// Create and hook bead to first agent
	issue1, err := b.Create(beads.CreateOptions{
		Title:    "Toast's task",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead 1: %v", err)
	}

	if err := b.Update(issue1.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agent1,
	}); err != nil {
		t.Fatalf("hook bead to agent1: %v", err)
	}

	// Create and hook bead to second agent
	issue2, err := b.Create(beads.CreateOptions{
		Title:    "Nux's task",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead 2: %v", err)
	}

	if err := b.Update(issue2.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agent2,
	}); err != nil {
		t.Fatalf("hook bead to agent2: %v", err)
	}

	// Verify each agent has exactly one hook
	agent1Hooks, err := b.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agent1,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list agent1 hooks: %v", err)
	}

	agent2Hooks, err := b.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agent2,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list agent2 hooks: %v", err)
	}

	if len(agent1Hooks) != 1 {
		t.Errorf("agent1 should have 1 hook, got %d", len(agent1Hooks))
	}
	if len(agent2Hooks) != 1 {
		t.Errorf("agent2 should have 1 hook, got %d", len(agent2Hooks))
	}

	// Verify correct assignment
	if len(agent1Hooks) > 0 && agent1Hooks[0].ID != issue1.ID {
		t.Errorf("agent1 hook ID = %s, want %s", agent1Hooks[0].ID, issue1.ID)
	}
	if len(agent2Hooks) > 0 && agent2Hooks[0].ID != issue2.ID {
		t.Errorf("agent2 hook ID = %s, want %s", agent2Hooks[0].ID, issue2.ID)
	}
}

// TestHookSlot_HookPersistence verifies that hooks persist across beads object recreation.
func TestHookSlot_HookPersistence(t *testing.T) {
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)
	_ = townRoot

	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	agentID := "gastown/polecats/toast"
	status := beads.StatusHooked

	// Create first beads instance and hook a bead
	b1 := beads.New(rigDir)
	issue, err := b1.Create(beads.CreateOptions{
		Title:    "Persistent task",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead: %v", err)
	}

	if err := b1.Update(issue.ID, beads.UpdateOptions{
		Status:   &status,
		Assignee: &agentID,
	}); err != nil {
		t.Fatalf("hook bead: %v", err)
	}

	// Create new beads instance (simulates session restart)
	b2 := beads.New(rigDir)

	// Verify hook persists
	hookedBeads, err := b2.List(beads.ListOptions{
		Status:   beads.StatusHooked,
		Assignee: agentID,
		Priority: -1,
	})
	if err != nil {
		t.Fatalf("list hooked beads with new instance: %v", err)
	}

	if len(hookedBeads) != 1 {
		t.Errorf("expected hook to persist, got %d hooked beads", len(hookedBeads))
	}

	if len(hookedBeads) > 0 && hookedBeads[0].ID != issue.ID {
		t.Errorf("persisted hook ID = %s, want %s", hookedBeads[0].ID, issue.ID)
	}
}

// TestHookSlot_StatusTransitions tests valid status transitions for hooked beads.
func TestHookSlot_StatusTransitions(t *testing.T) {
	if _, err := exec.LookPath("bd"); err != nil {
		t.Skip("bd not installed, skipping test")
	}

	townRoot, polecatDir := setupHookTestTown(t)
	_ = townRoot

	rigDir := filepath.Join(polecatDir, "..", "..", "mayor", "rig")
	initBeadsDB(t, rigDir)

	b := beads.New(rigDir)
	agentID := "gastown/polecats/toast"

	// Create a bead
	issue, err := b.Create(beads.CreateOptions{
		Title:    "Status transition test",
		Type:     "task",
		Priority: 2,
	})
	if err != nil {
		t.Fatalf("create bead: %v", err)
	}

	// Test transitions: open -> hooked -> open -> hooked -> closed
	transitions := []struct {
		name   string
		status string
	}{
		{"hook", beads.StatusHooked},
		{"unhook", "open"},
		{"rehook", beads.StatusHooked},
	}

	for _, trans := range transitions {
		t.Run(trans.name, func(t *testing.T) {
			status := trans.status
			opts := beads.UpdateOptions{Status: &status}
			if trans.status == beads.StatusHooked {
				opts.Assignee = &agentID
			}

			if err := b.Update(issue.ID, opts); err != nil {
				t.Errorf("transition to %s failed: %v", trans.status, err)
			}

			// Verify status
			updated, err := b.Show(issue.ID)
			if err != nil {
				t.Errorf("show after %s: %v", trans.name, err)
				return
			}
			if updated.Status != trans.status {
				t.Errorf("status after %s = %s, want %s", trans.name, updated.Status, trans.status)
			}
		})
	}

	// Finally close the bead
	if err := b.Close(issue.ID); err != nil {
		t.Errorf("close hooked bead: %v", err)
	}

	// Verify it's closed
	closed, err := b.Show(issue.ID)
	if err != nil {
		t.Fatalf("show closed bead: %v", err)
	}
	if closed.Status != "closed" {
		t.Errorf("final status = %s, want closed", closed.Status)
	}
}
