package trust

// Tests for the project-trust interactive glue (US-018, #134): the first-run
// prompt, the tool-call confirmation, the summary preview, and the
// BeforeToolCall gating hook. The trust store itself is covered in
// manager_test.go; here we exercise the interactive pieces.

import (
	"bufio"
	"bytes"
	"context"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/runtime"
)

// newTrustManagerAt builds a Manager backed by path.
func newTrustManagerAt(t *testing.T, path string) *Manager {
	t.Helper()
	m, err := NewManager(path)
	if err != nil {
		t.Fatalf("NewManager: %v", err)
	}
	return m
}

// newTrustManager builds a Manager backed by a fresh temp file and returns the
// manager plus its path (so a test can reload to verify persistence).
func newTrustManager(t *testing.T) (*Manager, string) {
	t.Helper()
	path := filepath.Join(t.TempDir(), "trust.json")
	return newTrustManagerAt(t, path), path
}

// readerOf wraps a string in the *bufio.Reader the trust prompts expect.
func readerOf(s string) *bufio.Reader { return bufio.NewReader(strings.NewReader(s)) }

// TestEnsureTrustPromptTrustedThisDir verifies choice 1 + scope 1 (this
// directory) persists a Trusted decision for cwd.
func TestEnsureTrustPromptTrustedThisDir(t *testing.T) {
	mgr, _ := newTrustManager(t)
	cwd := t.TempDir()
	var out bytes.Buffer
	EnsureTrustPrompt(&out, readerOf("1\n1\n"), mgr, cwd)

	if got := mgr.NearestTrustDecision(cwd); !got.Found || got.Decision != Trusted || got.Path != cwd {
		t.Errorf("after prompt, NearestTrustDecision(%q) = %+v, want Trusted/@cwd", cwd, got)
	}
	if !strings.Contains(out.String(), "First time in this directory") {
		t.Errorf("output missing prompt: %q", out.String())
	}
}

// TestEnsureTrustPromptRejectParent verifies choice 3 (reject) + scope 2
// (parent) persists an Untrusted decision for the parent directory.
func TestEnsureTrustPromptRejectParent(t *testing.T) {
	mgr, _ := newTrustManager(t)
	cwd := t.TempDir()
	parent := filepath.Dir(cwd)
	EnsureTrustPrompt(&bytes.Buffer{}, readerOf("3\n2\n"), mgr, cwd)

	if got := mgr.NearestTrustDecision(cwd); !got.Found || got.Decision != Untrusted || got.Path != parent {
		t.Errorf("after reject-parent, NearestTrustDecision(%q) = %+v, want Untrusted/@parent", cwd, got)
	}
}

// TestEnsureTrustPromptJustOnce verifies choice 2 (just once) grants session
// trust without persisting: IsTrusted is true now but false after a reload.
func TestEnsureTrustPromptJustOnce(t *testing.T) {
	mgr, path := newTrustManager(t)
	cwd := t.TempDir()
	EnsureTrustPrompt(&bytes.Buffer{}, readerOf("2\n"), mgr, cwd)

	if !mgr.IsTrusted(cwd) {
		t.Error("IsTrusted(cwd) = false after just-once, want true")
	}
	// Reload from the same file: session trust must not survive.
	m2 := newTrustManagerAt(t, path)
	if m2.IsTrusted(cwd) {
		t.Error("reload IsTrusted(cwd) = true, want false (session trust must not persist)")
	}
}

// TestEnsureTrustPromptSkipsWhenDecided verifies that when a decision already
// exists, EnsureTrustPrompt is a no-op: it writes nothing and reads nothing.
func TestEnsureTrustPromptSkipsWhenDecided(t *testing.T) {
	mgr, _ := newTrustManager(t)
	cwd := t.TempDir()
	if err := mgr.SetDecision(cwd, Trusted); err != nil {
		t.Fatalf("SetDecision: %v", err)
	}
	var out bytes.Buffer
	// Empty reader: if the prompt tried to read it would block/EOF; it must not.
	EnsureTrustPrompt(&out, readerOf(""), mgr, cwd)
	if out.Len() != 0 {
		t.Errorf("EnsureTrustPrompt wrote %q when a decision existed, want no output", out.String())
	}
}

// TestEstablishTrustApprove verifies --approve grants session trust up front
// without prompting: IsTrusted is true immediately, nothing is written, and the
// (empty) reader is never consumed. Session trust must not persist across a
// reload — --approve is per-run, not a saved decision.
func TestEstablishTrustApprove(t *testing.T) {
	mgr, path := newTrustManager(t)
	cwd := t.TempDir()
	var out bytes.Buffer
	EstablishTrust(&out, readerOf(""), mgr, cwd, true)

	if !mgr.IsTrusted(cwd) {
		t.Error("IsTrusted(cwd) = false after --approve, want true")
	}
	if out.Len() != 0 {
		t.Errorf("EstablishTrust wrote %q with --approve, want no prompt", out.String())
	}
	m2 := newTrustManagerAt(t, path)
	if m2.IsTrusted(cwd) {
		t.Error("reload IsTrusted(cwd) = true, want false (--approve must not persist)")
	}
}

// TestEstablishTrustWithoutApprove verifies that without --approve, EstablishTrust
// defers to the first-launch prompt (which runs for an undecided directory).
func TestEstablishTrustWithoutApprove(t *testing.T) {
	mgr, _ := newTrustManager(t)
	cwd := t.TempDir()
	var out bytes.Buffer
	// "2\n" answers the just-once prompt; if the prompt did not run, this input
	// would be left unread and IsTrusted would stay false.
	EstablishTrust(&out, readerOf("2\n"), mgr, cwd, false)

	if !strings.Contains(out.String(), "First time in this directory") {
		t.Errorf("without --approve, expected the trust prompt; got %q", out.String())
	}
	if !mgr.IsTrusted(cwd) {
		t.Error("IsTrusted(cwd) = false after just-once via EstablishTrust, want true")
	}
}

// TestConfirmToolCall verifies the y/n/a responses.
func TestConfirmToolCall(t *testing.T) {
	call := agentcore.AgentToolCall{Name: "bash", Arguments: []byte(`{"command":"ls"}`)}
	cases := []struct {
		in     string
		allow  bool
		always bool
	}{
		{"y\n", true, false},
		{"yes\n", true, false},
		{"a\n", true, true},
		{"always\n", true, true},
		{"n\n", false, false},
		{"\n", false, false},
		{"garbage\n", false, false},
	}
	for _, c := range cases {
		var out bytes.Buffer
		allow, always := ConfirmToolCall(&out, readerOf(c.in), call)
		if allow != c.allow || always != c.always {
			t.Errorf("ConfirmToolCall(%q) = (%v,%v), want (%v,%v)", c.in, allow, always, c.allow, c.always)
		}
	}
}

// TestToolCallSummary verifies the one-line preview for each side-effect tool.
func TestToolCallSummary(t *testing.T) {
	cases := []struct {
		name string
		args string
		want string
	}{
		{"bash", `{"command":"rm -rf /tmp/x"}`, "command: rm -rf /tmp/x"},
		{"write", `{"path":"/a/b.txt","content":"..."}`, "path: /a/b.txt"},
		{"edit", `{"path":"/a/b.txt","old_string":"x"}`, "path: /a/b.txt"},
		{"bash", `{}`, ""},
		{"bash", ``, ""},
		{"bash", `not-json`, "not-json"},
	}
	for _, c := range cases {
		got := toolCallSummary(agentcore.AgentToolCall{Name: c.name, Arguments: []byte(c.args)})
		if got != c.want {
			t.Errorf("toolCallSummary(%s,%s) = %q, want %q", c.name, c.args, got, c.want)
		}
	}
}

// TestToolCallSummaryTruncates verifies a very long command is truncated.
func TestToolCallSummaryTruncates(t *testing.T) {
	long := strings.Repeat("x", 500)
	got := toolCallSummary(agentcore.AgentToolCall{Name: "bash", Arguments: []byte(`{"command":"` + long + `"}`)})
	if !strings.HasSuffix(got, " …") {
		t.Errorf("expected truncated output ending with ' …', got %q", got)
	}
	if len([]rune(got)) > 250 {
		t.Errorf("output not truncated: len=%d", len([]rune(got)))
	}
}

// TestTrustBeforeToolCallGating verifies the hook: nil manager -> nil hook;
// trusted dir -> allow; untrusted + deny -> block; untrusted + always -> allow
// and grant session trust so the next call is allowed without a prompt.
func TestTrustBeforeToolCallGating(t *testing.T) {
	cwd := t.TempDir()
	mu := &sync.Mutex{}
	call := agentcore.AgentToolCall{Name: "write", Arguments: []byte(`{"path":"/tmp/x"}`)}

	// nil manager -> no hook.
	if h := BeforeToolCall(nil, cwd, nil, nil, mu); h != nil {
		t.Error("nil manager should yield nil hook")
	}

	// Trusted dir: hook returns nil (allow) without prompting.
	mgr, _ := newTrustManager(t)
	if err := mgr.SetDecision(cwd, Trusted); err != nil {
		t.Fatalf("SetDecision: %v", err)
	}
	hook := BeforeToolCall(mgr, cwd, readerOf(""), &bytes.Buffer{}, mu)
	if dec := hook(context.Background(), call); dec != nil {
		t.Errorf("trusted dir: hook returned %+v, want nil", dec)
	}

	// Untrusted dir, user denies: hook blocks with an error result.
	mgr2, _ := newTrustManager(t)
	var out bytes.Buffer
	hook2 := BeforeToolCall(mgr2, cwd, readerOf("n\n"), &out, mu)
	dec := hook2(context.Background(), call)
	if dec == nil || !dec.Block {
		t.Errorf("untrusted + deny: hook returned %+v, want a Block decision", dec)
	}

	// Untrusted dir, user says "always": hook allows and grants session trust,
	// so a second call is allowed WITHOUT reading any more input.
	mgr3, _ := newTrustManager(t)
	var out3 bytes.Buffer
	// Only one line of input ("a\n"); a second prompt would block on EOF.
	hook3 := BeforeToolCall(mgr3, cwd, readerOf("a\n"), &out3, mu)
	if dec := hook3(context.Background(), call); dec != nil {
		t.Errorf("untrusted + always: hook returned %+v, want nil (allow)", dec)
	}
	if !mgr3.IsTrusted(cwd) {
		t.Error("after 'always', IsTrusted(cwd) = false, want true (session trust granted)")
	}
	// Second call: no input left, but session trust means no prompt.
	if dec := hook3(context.Background(), call); dec != nil {
		t.Errorf("second call after 'always': hook returned %+v, want nil (no re-prompt)", dec)
	}
}

// TestTrustBeforeToolCallSkipsNonSideEffect verifies read-only tools are never
// gated, even in an untrusted directory.
func TestTrustBeforeToolCallSkipsNonSideEffect(t *testing.T) {
	cwd := t.TempDir()
	mgr, _ := newTrustManager(t)
	mu := &sync.Mutex{}
	hook := BeforeToolCall(mgr, cwd, readerOf(""), &bytes.Buffer{}, mu)
	for _, name := range []string{"read", "grep", "find", "todo", "webfetch"} {
		if dec := hook(context.Background(), agentcore.AgentToolCall{Name: name}); dec != nil {
			t.Errorf("non-side-effect tool %q was gated (%+v), want nil", name, dec)
		}
	}
}

// TestRegisterTrustCommand exercises the /trust action closure: status on an
// undecided dir, on (default) persists Trusted, once grants session trust, off
// revokes session trust AND persists Untrusted (the M2 regression - without
// ClearSessionTrust, an active "always"/once grant would keep IsTrusted true
// until restart), and an unknown arg yields usage.
func TestRegisterTrustCommand(t *testing.T) {
	cwd := t.TempDir()
	mgr, _ := newTrustManager(t)
	reg := runtime.NewSlashRegistry()
	RegisterCommand(reg, mgr, cwd)
	cmd, ok := reg.Lookup("trust")
	if !ok {
		t.Fatal("/trust command not registered")
	}

	if got := cmd.Action("status"); !strings.Contains(got, "undecided") {
		t.Errorf("status on undecided dir = %q, want 'undecided'", got)
	}

	// on (default arg) persists Trusted for cwd.
	if got := cmd.Action(""); !strings.Contains(got, "trusted") {
		t.Errorf("on = %q, want 'trusted'", got)
	}
	if res := mgr.NearestTrustDecision(cwd); !res.Found || res.Decision != Trusted {
		t.Errorf("after on, nearest = %+v, want Trusted/@cwd", res)
	}

	// Fresh dir: once grants session trust (in-memory), off must revoke it and
	// persist Untrusted so IsTrusted is false immediately.
	cwd2 := t.TempDir()
	mgr2, _ := newTrustManager(t)
	reg2 := runtime.NewSlashRegistry()
	RegisterCommand(reg2, mgr2, cwd2)
	cmd2, _ := reg2.Lookup("trust")
	cmd2.Action("once")
	if !mgr2.IsTrusted(cwd2) {
		t.Error("after once, IsTrusted = false, want true")
	}
	if got := cmd2.Action("off"); !strings.Contains(got, "untrusted") {
		t.Errorf("off = %q, want 'untrusted'", got)
	}
	if mgr2.IsTrusted(cwd2) {
		t.Error("after off, IsTrusted = true, want false (session grant must be revoked)")
	}
	if res := mgr2.NearestTrustDecision(cwd2); !res.Found || res.Decision != Untrusted {
		t.Errorf("after off, nearest = %+v, want Untrusted/@cwd", res)
	}

	// Unknown arg yields usage text.
	if got := cmd.Action("bogus"); !strings.Contains(got, "usage") {
		t.Errorf("unknown arg = %q, want 'usage'", got)
	}
}
