package agent

import (
	"context"
	"encoding/json"
	"strings"
	"testing"

	"reasonix/internal/event"
	"reasonix/internal/hook"
	"reasonix/internal/provider"
	"reasonix/internal/tool"
)

func TestToolHooksMayMutateWorkspaceUsesRunnerCapabilities(t *testing.T) {
	if toolHooksMayMutateWorkspace(hook.NewRunner(nil, "/tmp", nil, nil)) {
		t.Fatal("empty hook runner must not create a checkpoint coverage gap")
	}
	sessionOnly := hook.NewRunner([]hook.ResolvedHook{{Event: hook.SessionStart}}, "/tmp", nil, nil)
	if toolHooksMayMutateWorkspace(sessionOnly) {
		t.Fatal("non-tool hooks must not create a tool mutation coverage gap")
	}
	preTool := hook.NewRunner([]hook.ResolvedHook{{Event: hook.PreToolUse}}, "/tmp", nil, nil)
	if !toolHooksMayMutateWorkspace(preTool) {
		t.Fatal("PreToolUse shell hook must preserve the conservative coverage gap")
	}
	if !toolHooksMayMutateWorkspace(&stubHooks{}) {
		t.Fatal("custom legacy ToolHooks without a capability report must remain conservative")
	}
}

// stubHooks blocks PreToolUse for named tools and records what it saw.
type stubHooks struct {
	blockPre        map[string]bool
	preSeen         []string
	postSeen        []string
	postFailureSeen []string
	preCompactOut   string   // returned from PreCompact (extra summary guidance)
	subagentSeen    []string // last-answer text passed to each SubagentStop
	hasPostLLM      bool     // whether HasPostLLMCall reports a PostLLMCall hook
	postLLMOut      string   // replacement returned from PostLLMCall (when hasPostLLM)
	postLLMSeen     []string // reasoning text each PostLLMCall received
	postLLMTurns    []int    // turn number each PostLLMCall received
}

func (h *stubHooks) PreToolUse(_ context.Context, name string, _ json.RawMessage) (bool, string) {
	h.preSeen = append(h.preSeen, name)
	if h.blockPre[name] {
		return true, "blocked by test hook"
	}
	return false, ""
}

func (h *stubHooks) PostToolUse(_ context.Context, name string, _ json.RawMessage, _ string) {
	h.postSeen = append(h.postSeen, name)
}

func (h *stubHooks) PostToolUseFailure(_ context.Context, name string, _ json.RawMessage, _ string, _ error) {
	h.postFailureSeen = append(h.postFailureSeen, name)
}

func (h *stubHooks) SubagentStop(_ context.Context, last string) {
	h.subagentSeen = append(h.subagentSeen, last)
}
func (h *stubHooks) PreCompact(context.Context, string) string { return h.preCompactOut }

func (h *stubHooks) PostLLMCall(_ context.Context, reasoning string, turn int) string {
	h.postLLMSeen = append(h.postLLMSeen, reasoning)
	h.postLLMTurns = append(h.postLLMTurns, turn)
	if h.hasPostLLM && h.postLLMOut != "" {
		return h.postLLMOut
	}
	return reasoning
}

func (h *stubHooks) HasPostLLMCall() bool { return h.hasPostLLM }

// TestSubagentStopFiresForForegroundTask checks SubagentStop fires (with the
// sub-agent's answer) when a foreground `task` call completes, but not for a
// backgrounded one (which only returns a "started" handle and stops later).
func TestSubagentStopFiresForForegroundTask(t *testing.T) {
	reg := tool.NewRegistry()
	reg.Add(okTool{name: "task"}) // stands in for the real task tool; returns "ok"
	h := &stubHooks{}
	a := New(nil, reg, NewSession(""), Options{Hooks: h}, event.Discard)

	a.executeBatch(context.Background(), &a.turn, []provider.ToolCall{{Name: "task", Arguments: `{"prompt":"x"}`}})
	if len(h.subagentSeen) != 1 || h.subagentSeen[0] != "ok" {
		t.Fatalf("foreground task should fire SubagentStop with the answer, saw %v", h.subagentSeen)
	}

	a.executeBatch(context.Background(), &a.turn, []provider.ToolCall{{Name: "task", Arguments: `{"run_in_background":true}`}})
	if len(h.subagentSeen) != 1 {
		t.Errorf("backgrounded task must not fire SubagentStop, saw %v", h.subagentSeen)
	}
}

// TestPreToolUseHookBlocks proves a gating PreToolUse hook refuses a tool call
// (returning a blocked result, never running the tool or its PostToolUse), while
// an unblocked call runs and fires PostToolUse.
func TestPreToolUseHookBlocks(t *testing.T) {
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "bash", readOnly: false})
	reg.Add(fakeTool{name: "read_file", readOnly: true})

	h := &stubHooks{blockPre: map[string]bool{"bash": true}}
	a := New(nil, reg, NewSession(""), Options{Hooks: h}, event.Discard)

	blocked := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "bash", Arguments: `{"command":"x"}`})
	if !blocked.blocked || !strings.HasPrefix(blocked.output, "blocked:") {
		t.Errorf("PreToolUse block should yield a blocked result, got %+v", blocked)
	}
	if !strings.Contains(blocked.output, "blocked by test hook") {
		t.Errorf("block reason should be surfaced to the model, got %q", blocked.output)
	}

	ok := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "read_file", Arguments: `{"path":"/a"}`})
	if ok.blocked || !strings.Contains(ok.output, "done") {
		t.Errorf("unblocked call should run, got %+v", ok)
	}

	if got := strings.Join(h.preSeen, ","); got != "bash,read_file" {
		t.Errorf("PreToolUse should fire for both calls, saw %q", got)
	}
	// PostToolUse fires only for the call that actually ran.
	if got := strings.Join(h.postSeen, ","); got != "read_file" {
		t.Errorf("PostToolUse should fire only for the run tool, saw %q", got)
	}
}

func TestPostToolUseFailureUsesFailureHook(t *testing.T) {
	reg := tool.NewRegistry()
	reg.Add(failTool{name: "broken"})
	h := &stubHooks{}
	a := New(nil, reg, NewSession(""), Options{Hooks: h}, event.Discard)
	a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "broken", Arguments: `{}`})
	if got := strings.Join(h.postFailureSeen, ","); got != "broken" {
		t.Fatalf("failure hooks = %q", got)
	}
	if len(h.postSeen) != 0 {
		t.Fatalf("success hook fired for failure: %v", h.postSeen)
	}
}
