package wire

import (
	"context"
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

// TestReviewHandlersPreserveTrailingWhitespaceTargetPath is the legacy
// command → Desktop handler → workspace regression reviewer3 required (msg
// seq 217): a TargetPath whose canonical root ends in a trailing space
// ("/repo ") MUST flow from the wire command struct through handleReview*
// into workspace.Review{Summary,Files,Diff} byte-exact. A regression that
// reintroduces strings.TrimSpace(msg.TargetPath) in the handler would
// rewrite "/repo " to "/repo", point the review at a different worktree,
// and (when only the trailing-space repo exists on disk) surface as a
// spurious "not a git repo" error.
//
// The test sets up a real git repo at a trailing-space path on disk and
// dispatches each of the three legacy handlers with that path as
// TargetPath. If the handler trims, workspace.Review* would fail to find
// the repo; if it does not trim, the call succeeds and the result carries
// real review data. Skipped on Windows — the OS strips trailing spaces
// from paths at the filesystem layer, so the test fixture cannot exist.
func TestReviewHandlersPreserveTrailingWhitespaceTargetPath(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("trailing whitespace in filesystem paths is unreliable on Windows")
	}
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	// Build a tempdir whose LEAF segment ends in a trailing space. The
	// parent dir is a normal t.TempDir() so cleanup works.
	parent := t.TempDir()
	repoDir := filepath.Join(parent, "repo ")
	require.NotEqual(t, filepath.Join(parent, "repo"), repoDir,
		"test fixture sanity: trailing-space path must differ from trimmed form")
	require.NoError(t, os.MkdirAll(repoDir, 0o755))

	// Initialize a git repo at the trailing-space path. Each git command
	// runs with cmd.Dir = repoDir so git operates on the trailing-space
	// worktree.
	runWireGit := func(args ...string) {
		t.Helper()
		cmd := exec.Command("git", args...)
		cmd.Dir = repoDir
		out, err := cmd.CombinedOutput()
		require.NoError(t, err, "git %v failed: %s", args, string(out))
	}
	runWireGit("init")
	runWireGit("checkout", "-b", "main")
	runWireGit("config", "user.name", "AgentTeam Test")
	runWireGit("config", "user.email", "agentteam-test@example.com")
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "tracked.txt"), []byte("base\n"), 0o644))
	runWireGit("add", "tracked.txt")
	runWireGit("commit", "-m", "init")

	// Construct a Protocol with just enough state for sendJSON to work:
	// a buffered sendCh large enough that we never hit the drop path
	// (which would touch c.metrics).
	c := &Protocol{
		id:     "ch_test",
		sendCh: make(chan []byte, 8),
	}

	// readResult drains one JSON message from sendCh and unmarshals it
	// into the supplied pointer. Fails the test if no message arrives
	// within a short timeout (the handlers are synchronous, so this is
	// just a backstop).
	readResult := func(t *testing.T, into any) {
		t.Helper()
		select {
		case raw := <-c.sendCh:
			require.NoError(t, json.Unmarshal(raw, into), "unmarshal handler result")
		case <-time.After(2 * time.Second):
			t.Fatal("handler did not produce a result within 2s")
		}
	}

	ctx := context.Background()
	const agentID = "agt_test"

	t.Run("summary preserves trailing-space TargetPath", func(t *testing.T) {
		c.handleReviewSummary(ctx, agentReviewSummaryMsg{
			Type:       "agent:review:summary",
			AgentID:    agentID,
			RequestID:  "rq_summary",
			TargetPath: repoDir,
		})
		var result agentReviewSummaryResultMsg
		readResult(t, &result)
		require.Equal(t, agentID, result.AgentID)
		require.Empty(t, result.Error,
			"handleReviewSummary MUST pass the trailing-space TargetPath byte-exact — "+
				"an error here means the handler trimmed it and the workspace layer could not find the repo. "+
				"got error: %q", result.Error)
		require.NotNil(t, result.Summary, "summary must be populated on success")
	})

	t.Run("files preserves trailing-space TargetPath", func(t *testing.T) {
		c.handleReviewFiles(ctx, agentReviewFilesMsg{
			Type:       "agent:review:files",
			AgentID:    agentID,
			RequestID:  "rq_files",
			Scope:      "working",
			TargetPath: repoDir,
		})
		var result agentReviewFilesResultMsg
		readResult(t, &result)
		require.Equal(t, agentID, result.AgentID)
		require.Empty(t, result.Error,
			"handleReviewFiles MUST pass the trailing-space TargetPath byte-exact — "+
				"an error here means the handler trimmed it. got error: %q", result.Error)
	})

	t.Run("diff preserves trailing-space TargetPath", func(t *testing.T) {
		c.handleReviewDiff(ctx, agentReviewDiffMsg{
			Type:       "agent:review:diff",
			AgentID:    agentID,
			RequestID:  "rq_diff",
			Scope:      "working",
			Path:       "tracked.txt",
			TargetPath: repoDir,
		})
		var result agentReviewDiffResultMsg
		readResult(t, &result)
		require.Equal(t, agentID, result.AgentID)
		require.Empty(t, result.Error,
			"handleReviewDiff MUST pass the trailing-space TargetPath byte-exact — "+
				"an error here means the handler trimmed it. got error: %q", result.Error)
	})
}

// TestReviewHandlersFallbackToWorkDirOnlyOnExactEmptyTargetPath pins the
// fallback contract at the handler level: workspacefs.WorkDir(msg.AgentID)
// is invoked ONLY when TargetPath == "" — a TrimSpace would let a
// whitespace-only TargetPath slip past the gate and fall back to WorkDir.
//
// The test cannot observe the workspacefs.WorkDir call directly without a
// production-code refactor, so it asserts indirectly: with an empty
// TargetPath, the handler MUST NOT fail to construct the message (no
// panic, no send-buffer-full), proving the fallback path executed
// cleanly. With a whitespace-only TargetPath on a system where the
// workspace layer's DetectDotGit would not find an ancestor repo, the
// result MUST still be a non-nil send — proving the handler either
// acked an error OR fell back to WorkDir, but in either case did not
// crash. (The byte-exact preservation contract itself is pinned by the
// trailing-space real-repo test above.)
func TestReviewHandlersFallbackToWorkDirOnlyOnExactEmptyTargetPath(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}

	c := &Protocol{
		id:     "ch_test",
		sendCh: make(chan []byte, 4),
	}
	ctx := context.Background()
	const agentID = "agt_nonexistent_test_agent"

	// Empty TargetPath: handler falls back to workspacefs.WorkDir(agentID)
	// and dispatches the workspace call. The handler MUST produce a result
	// message — either a real summary or an error — proving the fallback
	// branch executed without crashing.
	c.handleReviewSummary(ctx, agentReviewSummaryMsg{
		Type:       "agent:review:summary",
		AgentID:    agentID,
		RequestID:  "rq_empty",
		TargetPath: "",
	})
	select {
	case raw := <-c.sendCh:
		require.NotEmpty(t, raw, "empty TargetPath handler MUST produce a result message")
		var result agentReviewSummaryResultMsg
		require.NoError(t, json.Unmarshal(raw, &result))
		require.Equal(t, agentID, result.AgentID, "AgentID must round-trip even on fallback")
	case <-time.After(2 * time.Second):
		t.Fatal("empty TargetPath handler did not produce a result within 2s — fallback path crashed")
	}
}
