package agents

import (
	"context"
	"log/slog"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/command"
	"github.com/enmanuel/agents/pkg/decision"
)

// newTestRobot creates a minimal Robot for testing without requiring
// Matrix or network. Fields are initialized directly.
func newTestRobot(t *testing.T) *Robot {
	t.Helper()
	cfg := &config.AgentConfig{
		Agent: config.AgentMeta{
			ID:          "test-robot",
			Name:        "Test Robot",
			Type:        "robot",
			Description: "robot for tests",
			Version:     "1.0.0",
		},
	}
	r := &Robot{
		cfg:        cfg,
		logger:     slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})),
		done:       make(chan struct{}),
		commands:   make(map[string]CommandHandler),
		cmdAliases: command.BuiltinNames(),
		startTime:  time.Now(),
	}
	r.registerBuiltinCommands()
	return r
}

// TestRobotCmdHelp verifies !help lists built-in commands.
func TestRobotCmdHelp(t *testing.T) {
	r := newTestRobot(t)

	reply := r.cmdHelp(context.Background(), decision.MessageContext{})

	if !strings.Contains(reply, "Comandos disponibles") {
		t.Error("help reply missing header")
	}
	for _, cmd := range []string{"help", "ping", "status", "info", "version"} {
		if !strings.Contains(reply, "!"+cmd) {
			t.Errorf("help reply missing command !%s", cmd)
		}
	}
	// Robot should NOT show agent-only commands
	for _, cmd := range []string{"!tools", "!tool", "!clear", "!prompts"} {
		if strings.Contains(reply, cmd+"`") {
			t.Errorf("help reply should not contain agent-only command %s", cmd)
		}
	}
}

// TestRobotCmdHelpWithCustom verifies !help includes custom commands.
func TestRobotCmdHelpWithCustom(t *testing.T) {
	r := newTestRobot(t)

	r.RegisterCommand(
		command.Spec{Name: "deploy", Description: "Deploy to env", Usage: "!deploy <env>"},
		func(_ context.Context, _ decision.MessageContext) string { return "deployed" },
	)

	reply := r.cmdHelp(context.Background(), decision.MessageContext{})

	if !strings.Contains(reply, "Comandos del robot") {
		t.Error("help reply missing 'Comandos del robot' section")
	}
	if !strings.Contains(reply, "!deploy") {
		t.Error("help reply missing custom command !deploy")
	}
}

// TestRobotCmdPing verifies !ping returns pong.
func TestRobotCmdPing(t *testing.T) {
	r := newTestRobot(t)

	reply := r.cmdPing(context.Background(), decision.MessageContext{})

	if !strings.HasPrefix(reply, "pong") {
		t.Errorf("ping reply should start with 'pong', got %q", reply)
	}
}

// TestRobotCmdStatus verifies !status includes type and uptime.
func TestRobotCmdStatus(t *testing.T) {
	r := newTestRobot(t)

	reply := r.cmdStatus(context.Background(), decision.MessageContext{})

	if !strings.Contains(reply, "robot") {
		t.Error("status reply missing type 'robot'")
	}
	if !strings.Contains(reply, "Uptime") {
		t.Error("status reply missing Uptime")
	}
}

// TestRobotCmdInfo verifies !info shows robot identity.
func TestRobotCmdInfo(t *testing.T) {
	r := newTestRobot(t)

	reply := r.cmdInfo(context.Background(), decision.MessageContext{})

	if !strings.Contains(reply, "Test Robot") {
		t.Error("info reply missing robot name")
	}
	if !strings.Contains(reply, "test-robot") {
		t.Error("info reply missing robot ID")
	}
	if !strings.Contains(reply, "robot") {
		t.Error("info reply missing type 'robot'")
	}
}

// TestRobotCmdVersion verifies !version returns name + version.
func TestRobotCmdVersion(t *testing.T) {
	r := newTestRobot(t)

	reply := r.cmdVersion(context.Background(), decision.MessageContext{})

	if reply != "Test Robot 1.0.0" {
		t.Errorf("version reply = %q, want %q", reply, "Test Robot 1.0.0")
	}
}

// TestRobotIgnoresNonCommand verifies that handleEvent silently ignores
// non-command messages (no error, no reply).
func TestRobotIgnoresNonCommand(t *testing.T) {
	r := newTestRobot(t)

	// handleEvent with empty Command should not panic.
	// Since we can't easily mock the Matrix client, we verify the method
	// returns without error by checking it doesn't reach command dispatch.
	msgCtx := decision.MessageContext{
		Command: "", // non-command
		Content: "hola bot",
	}

	// The robot should just return without doing anything.
	// We can't call handleEvent directly because it needs an *event.Event,
	// but we can verify the logic by checking the command map behavior.
	if _, ok := r.commands[""]; ok {
		t.Error("empty string should not be a registered command")
	}

	// Verify no commands match empty string.
	if _, ok := r.cmdAliases[""]; ok {
		t.Error("empty string should not be in aliases")
	}

	_ = msgCtx // used to document test intent
}

// TestRobotCustomCommand verifies RegisterCommand works and the handler executes.
func TestRobotCustomCommand(t *testing.T) {
	r := newTestRobot(t)

	executed := false
	r.RegisterCommand(
		command.Spec{
			Name:        "deploy",
			Aliases:     []string{"d"},
			Description: "Deploy to env",
			Usage:       "!deploy <env>",
		},
		func(_ context.Context, msgCtx decision.MessageContext) string {
			executed = true
			if len(msgCtx.Args) == 0 {
				return "Uso: !deploy <env>"
			}
			return "Deploying to " + msgCtx.Args[0]
		},
	)

	// Verify command is registered
	handler, ok := r.commands["deploy"]
	if !ok {
		t.Fatal("deploy command not registered")
	}

	// Execute the handler
	reply := handler(context.Background(), decision.MessageContext{
		Command: "deploy",
		Args:    []string{"staging"},
	})

	if !executed {
		t.Error("handler was not executed")
	}
	if reply != "Deploying to staging" {
		t.Errorf("reply = %q, want %q", reply, "Deploying to staging")
	}

	// Verify alias works
	canonical, ok := r.cmdAliases["d"]
	if !ok {
		t.Fatal("alias 'd' not registered")
	}
	if canonical != "deploy" {
		t.Errorf("alias canonical = %q, want %q", canonical, "deploy")
	}

	// Verify custom spec is tracked (for !help)
	if len(r.customSpecs) != 1 {
		t.Fatalf("customSpecs len = %d, want 1", len(r.customSpecs))
	}
	if r.customSpecs[0].Name != "deploy" {
		t.Errorf("customSpecs[0].Name = %q, want %q", r.customSpecs[0].Name, "deploy")
	}
}

// TestRobotStopAndDone verifies lifecycle methods work correctly.
func TestRobotStopAndDone(t *testing.T) {
	r := &Robot{
		done: make(chan struct{}),
	}

	ctx, cancel := context.WithCancel(context.Background())
	r.cancel = cancel

	started := make(chan struct{})
	go func() {
		close(started)
		<-ctx.Done()
		close(r.done)
	}()

	<-started

	r.Stop()

	select {
	case <-r.Done():
		// ok
	case <-time.After(2 * time.Second):
		t.Fatal("Done() did not close within 2s after Stop()")
	}
}

// TestRobotStopNilCancel verifies Stop is safe when cancel is nil.
func TestRobotStopNilCancel(t *testing.T) {
	r := &Robot{
		done: make(chan struct{}),
	}
	// cancel is nil — must not panic.
	r.Stop()
}

// TestRunnerInterfaceSatisfied verifies that both Agent and Robot
// satisfy the Runner interface at compile time.
func TestRunnerInterfaceSatisfied(t *testing.T) {
	// These are compile-time checks — if they compile, the test passes.
	var _ Runner = (*Agent)(nil)
	var _ Runner = (*Robot)(nil)
}

// TestRobotBuiltinCommandCount verifies the robot has exactly the expected
// built-in commands and not more.
func TestRobotBuiltinCommandCount(t *testing.T) {
	r := newTestRobot(t)

	expected := map[string]bool{
		"help":    true,
		"ping":    true,
		"status":  true,
		"info":    true,
		"version": true,
	}

	for name := range r.commands {
		if !expected[name] {
			t.Errorf("unexpected built-in command %q in robot", name)
		}
	}

	for name := range expected {
		if _, ok := r.commands[name]; !ok {
			t.Errorf("missing built-in command %q in robot", name)
		}
	}
}
