package runtime

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

	"github.com/flothus/tmux-xterm-research/server-go/internal/configsvc"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/event"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/transport"
)

// newTestAgentWithConfigSvc spins up an Agent wired to a configsvc
// rooted in a temp dir. Mirrors the wiring runlive does in production.
func newTestAgentWithConfigSvc(t *testing.T, tools []string) (*Agent, *configsvc.Service, func()) {
	t.Helper()
	root := t.TempDir()
	orgsDir := filepath.Join(root, "orgs")
	rolesDir := filepath.Join(root, "roles", "v1")
	for _, d := range []string{orgsDir, rolesDir} {
		if err := os.MkdirAll(d, 0o755); err != nil {
			t.Fatal(err)
		}
	}
	if err := os.WriteFile(filepath.Join(rolesDir, "master.md"), []byte("# master"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(orgsDir, "demo.yaml"), []byte(
		"name: demo\nversion: 1\nroles:\n  - id: master\n    provider: claude-code\n",
	), 0o644); err != nil {
		t.Fatal(err)
	}
	svc, err := configsvc.New(orgsDir, filepath.Join(root, "roles"))
	if err != nil {
		t.Fatal(err)
	}
	dbPath := filepath.Join(t.TempDir(), "harness.db")
	st, err := store.Open(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	bus := event.NewBus(st)
	q := transport.New(st, bus)
	// Insert the agent row so RoleAllowlist + lookups work.
	ctx := context.Background()
	_ = st.Tx(ctx, func(qq store.Querier) error {
		_, err := qq.Exec(`INSERT OR IGNORE INTO runs(id, status, prompt) VALUES(?, 'running', '')`, "rTest")
		return err
	})
	_ = st.Tx(ctx, func(qq store.Querier) error {
		_, err := qq.Exec(`INSERT INTO agents(id, run_id, role, status, provider) VALUES(?, ?, ?, 'spawning', ?)`,
			"aTest", "rTest", "master", "claude-code")
		return err
	})
	agent := NewAgent(nil, st, bus, q, "aTest", "master", "rTest", nil, tools, "claude-code", 0)
	agent.SetConfigSvc(svc)
	return agent, svc, func() { _ = st.Close() }
}

// org.list dispatched through the agent's allowlist should succeed and
// emit an agent.config.tool event with the orgs payload.
func TestAgentOrgListViaTool(t *testing.T) {
	a, _, cleanup := newTestAgentWithConfigSvc(t, []string{"org.list"})
	defer cleanup()
	ctx := context.Background()
	sub, cancel := a.bus.Subscribe(16)
	defer cancel()
	if err := a.executeTool(ctx, ToolCall{Name: "org.list"}, nil); err != nil {
		t.Fatalf("tool error: %v", err)
	}
	select {
	case ev := <-sub:
		if ev.Payload["tool"] != "org.list" {
			t.Fatalf("event payload missing tool: %+v", ev.Payload)
		}
	default:
		t.Fatal("expected agent.config.tool event")
	}
}

// A role that does NOT list org.write in tools must be rejected even
// when the configsvc is wired — allowlist takes precedence.
func TestAgentOrgWriteBlockedByAllowlist(t *testing.T) {
	a, _, cleanup := newTestAgentWithConfigSvc(t, []string{"introspect"})
	defer cleanup()
	err := a.executeTool(context.Background(), ToolCall{
		Name: "org.write",
		Args: map[string]any{"name": "x", "yaml": "name: x\nversion: 1\n"},
	}, nil)
	if err == nil || !strings.Contains(err.Error(), "not in role allowlist") {
		t.Fatalf("expected allowlist rejection, got %v", err)
	}
}

// org.write should round-trip — a fresh org appears on disk + in ListOrgs.
func TestAgentOrgWriteRoundTrip(t *testing.T) {
	a, svc, cleanup := newTestAgentWithConfigSvc(t, []string{"org.write"})
	defer cleanup()
	ctx := context.Background()
	body := "name: built\nversion: 1\nroles:\n  - id: a\n    provider: claude-code\n"
	err := a.executeTool(ctx, ToolCall{
		Name: "org.write",
		Args: map[string]any{"name": "built", "yaml": body},
	}, nil)
	if err != nil {
		t.Fatalf("tool error: %v", err)
	}
	orgs, _, err := svc.ListOrgs()
	if err != nil {
		t.Fatal(err)
	}
	var sawBuilt bool
	for _, o := range orgs {
		if o.Name == "built" {
			sawBuilt = true
		}
	}
	if !sawBuilt {
		t.Fatalf("built org not in list: %+v", orgs)
	}
}

// Validation failure surfaces as an error AND emits the validation
// detail so the LLM's next turn can see what went wrong.
func TestAgentOrgWriteSurfacesValidationErrors(t *testing.T) {
	a, _, cleanup := newTestAgentWithConfigSvc(t, []string{"org.write"})
	defer cleanup()
	ctx := context.Background()
	sub, cancel := a.bus.Subscribe(16)
	defer cancel()
	body := "name: bad\nversion: 1\nroles:\n  - id: x\n    delegates_to: [ghost]\n"
	err := a.executeTool(ctx, ToolCall{
		Name: "org.write",
		Args: map[string]any{"name": "bad", "yaml": body},
	}, nil)
	if err == nil {
		t.Fatal("expected validation error to surface as tool error")
	}
	select {
	case ev := <-sub:
		if _, ok := ev.Payload["validation"]; !ok {
			t.Fatalf("event missing validation: %+v", ev.Payload)
		}
	default:
		t.Fatal("expected agent.config.tool event")
	}
}
