package exec_test

import (
	"context"
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/Molecule-AI/molecule-cli/internal/backends"
	_ "github.com/Molecule-AI/molecule-cli/internal/backends/exec" // register
)

// requireUnix skips Windows tests that depend on /bin/sh shell semantics.
// Windows-shell coverage is in TestPlatformShell_Windows below.
func requireUnix(t *testing.T) {
	t.Helper()
	if runtime.GOOS == "windows" {
		t.Skip("Unix-only: shell command semantics differ on Windows")
	}
}

func TestExec_RequiresCmd(t *testing.T) {
	_, err := backends.Build("exec", backends.Config{})
	if err == nil {
		t.Fatal("expected error when cmd is unset")
	}
	if !strings.Contains(err.Error(), "cmd") {
		t.Errorf("error should mention cmd: %v", err)
	}
}

func TestExec_RejectsBadTimeout(t *testing.T) {
	_, err := backends.Build("exec", backends.Config{"cmd": "echo hi", "timeout": "not-a-duration"})
	if err == nil {
		t.Fatal("expected error on bad timeout")
	}
}

func TestExec_RejectsZeroTimeout(t *testing.T) {
	_, err := backends.Build("exec", backends.Config{"cmd": "echo hi", "timeout": "0s"})
	if err == nil {
		t.Fatal("expected error on zero timeout")
	}
}

func TestExec_EchoStdinToStdout(t *testing.T) {
	requireUnix(t)
	be, err := backends.Build("exec", backends.Config{"cmd": "cat"})
	if err != nil {
		t.Fatal(err)
	}
	defer be.Close()

	resp, err := be.HandleA2A(context.Background(), backends.Request{
		Parts: []backends.Part{{Type: "text", Text: "hello world"}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if got := resp.Parts[0].Text; got != "hello world" {
		t.Errorf("got %q, want %q", got, "hello world")
	}
	if !resp.Final {
		t.Error("expected Final=true")
	}
}

func TestExec_ConcatenatesTextPartsOnly(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{"cmd": "cat"})
	defer be.Close()
	resp, _ := be.HandleA2A(context.Background(), backends.Request{
		Parts: []backends.Part{
			{Type: "text", Text: "a"},
			{Type: "data", Data: map[string]interface{}{"k": "v"}}, // ignored
			{Type: "text", Text: "b"},
		},
	})
	if got := resp.Parts[0].Text; got != "ab" {
		t.Errorf("got %q, want ab", got)
	}
}

func TestExec_NonZeroExitSurfacesStderr(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{
		"cmd": "echo my-stderr-text >&2; exit 17",
	})
	defer be.Close()
	_, err := be.HandleA2A(context.Background(), backends.Request{
		Parts: []backends.Part{{Type: "text", Text: "x"}},
	})
	if err == nil {
		t.Fatal("expected error on exit 17")
	}
	if !strings.Contains(err.Error(), "my-stderr-text") {
		t.Errorf("error should include stderr: %v", err)
	}
}

func TestExec_TimeoutKillsRunaway(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{
		"cmd":     "sleep 5",
		"timeout": "100ms",
	})
	defer be.Close()
	_, err := be.HandleA2A(context.Background(), backends.Request{
		Parts: []backends.Part{{Type: "text", Text: "x"}},
	})
	if err == nil {
		t.Fatal("expected timeout error")
	}
	if !strings.Contains(err.Error(), "timed out") {
		t.Errorf("error should mention timeout: %v", err)
	}
}

func TestExec_PassMetaInjectsEnv(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{
		"cmd":       `printf "ws=%s caller=%s msg=%s task=%s method=%s" "$MOLECULE_WORKSPACE_ID" "$MOLECULE_CALLER_ID" "$MOLECULE_MESSAGE_ID" "$MOLECULE_TASK_ID" "$MOLECULE_METHOD"`,
		"pass_meta": "true",
	})
	defer be.Close()
	resp, err := be.HandleA2A(context.Background(), backends.Request{
		WorkspaceID: "ws-1",
		CallerID:    "ws-2",
		MessageID:   "act-3",
		TaskID:      "task-4",
		Method:      "message/send",
		Parts:       []backends.Part{{Type: "text", Text: "x"}},
	})
	if err != nil {
		t.Fatal(err)
	}
	want := "ws=ws-1 caller=ws-2 msg=act-3 task=task-4 method=message/send"
	if got := resp.Parts[0].Text; got != want {
		t.Errorf("env injection: got %q, want %q", got, want)
	}
}

func TestExec_NoPassMetaLeavesEnvUntouched(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{
		"cmd": `printf "%s" "$MOLECULE_WORKSPACE_ID"`,
		// pass_meta unset → defaults to false
	})
	defer be.Close()
	resp, _ := be.HandleA2A(context.Background(), backends.Request{
		WorkspaceID: "ws-secret",
		Parts:       []backends.Part{{Type: "text", Text: "x"}},
	})
	if got := resp.Parts[0].Text; got != "" {
		t.Errorf("expected empty env (pass_meta off); got %q", got)
	}
}

func TestExec_ParentEnvAvailableEvenWhenPassMetaOff(t *testing.T) {
	requireUnix(t)
	// When pass_meta is off, we still inherit the parent process env
	// (cmd.Env nil → os.Environ). Verify by reading PATH.
	be, _ := backends.Build("exec", backends.Config{"cmd": `printf "%s" "$PATH"`})
	defer be.Close()
	resp, _ := be.HandleA2A(context.Background(), backends.Request{
		Parts: []backends.Part{{Type: "text", Text: "x"}},
	})
	if !strings.Contains(resp.Parts[0].Text, os.Getenv("PATH")[:min(len(os.Getenv("PATH")), 8)]) {
		t.Errorf("expected PATH inheritance; got %q", resp.Parts[0].Text)
	}
}

// TestExec_ContextCancelKillsCommand: when the caller's ctx is
// cancelled mid-run, the subprocess is killed immediately (vs waiting
// for our internal timeout to fire).
func TestExec_ContextCancelKillsCommand(t *testing.T) {
	requireUnix(t)
	be, _ := backends.Build("exec", backends.Config{
		"cmd":     "sleep 5",
		"timeout": "30s",
	})
	defer be.Close()
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately
	_, err := be.HandleA2A(ctx, backends.Request{
		Parts: []backends.Part{{Type: "text", Text: "x"}},
	})
	if err == nil {
		t.Fatal("expected error on cancelled ctx")
	}
}

// Tiny helper since math/min is generic since Go 1.21.
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
