package testutil

import (
	"context"
	"testing"

	"github.com/genai-io/san/internal/core"
	"github.com/genai-io/san/internal/llm"
	"github.com/genai-io/san/internal/tool"
	"github.com/genai-io/san/internal/tool/perm"
)

// FakeLLM implements core.LLM for testing, returning queued responses.
type FakeLLM struct {
	Responses []llm.CompletionResponse
	callIdx   int
}

func (f *FakeLLM) InputLimit() int { return 0 }

func (f *FakeLLM) Infer(_ context.Context, _ core.InferRequest) (<-chan core.Chunk, error) {
	ch := make(chan core.Chunk, 1)
	go func() {
		defer close(ch)
		var resp llm.CompletionResponse
		if f.callIdx < len(f.Responses) {
			resp = f.Responses[f.callIdx]
			f.callIdx++
		} else {
			resp = llm.CompletionResponse{Content: "no more responses", StopReason: "end_turn"}
		}
		// Convert via bridge's toInferResponse path
		ch <- core.Chunk{
			Done: true,
			Response: &core.InferResponse{
				Content:      resp.Content,
				Thinking:     resp.Thinking,
				ToolCalls:    legacyToCoreCalls(resp.ToolCalls),
				StopReason:   core.StopReason(resp.StopReason),
				InputTokens:  resp.Usage.InputTokens,
				OutputTokens: resp.Usage.OutputTokens,
			},
		}
	}()
	return ch, nil
}

func legacyToCoreCalls(calls []core.ToolCall) []core.ToolCall {
	if len(calls) == 0 {
		return nil
	}
	out := make([]core.ToolCall, len(calls))
	copy(out, calls)
	return out
}

// NewTestAgent creates a core.Agent backed by a FakeLLM with queued responses.
// All globally registered tools (including dynamically registered fakes) are included.
func NewTestAgent(t *testing.T, responses ...llm.CompletionResponse) (core.Agent, *FakeLLM) {
	t.Helper()
	fakeLLM := &FakeLLM{Responses: responses}
	cwd := t.TempDir()
	return core.NewAgent(core.Config{
		ID:     "test-agent",
		LLM:    fakeLLM,
		System: core.NewSystem(),
		Tools:  buildAllRegisteredTools(cwd),

		CWD:      cwd,
		MaxSteps: 100,
	}), fakeLLM
}

// buildAllRegisteredTools creates a core.Tools wrapping ALL tools in the global registry,
// including dynamically registered fake tools. Unlike AdaptToolRegistry which only finds
// tools that have schemas in GetToolSchemas(), this walks the entire registry directly.
func buildAllRegisteredTools(cwd string) core.Tools {
	var adapted []core.Tool
	for _, name := range tool.Default().List() {
		t, ok := tool.Get(name)
		if !ok {
			continue
		}
		schema := core.ToolSchema{Name: name, Description: t.Description()}
		adapted = append(adapted, tool.AdaptTool(t, schema, func() string { return cwd }))
	}
	return core.NewTools(adapted...)
}

// NewTestAgentWithPermission creates a core.Agent with a permission function wrapping tools.
func NewTestAgentWithPermission(t *testing.T, permFn perm.PermissionFunc, responses ...llm.CompletionResponse) (core.Agent, *FakeLLM) {
	t.Helper()
	fakeLLM := &FakeLLM{Responses: responses}
	cwd := t.TempDir()
	tools := tool.WithPermission(buildAllRegisteredTools(cwd), permFn)
	return core.NewAgent(core.Config{
		ID:       "test-agent",
		LLM:      fakeLLM,
		System:   core.NewSystem(),
		Tools:    tools,
		CWD:      cwd,
		MaxSteps: 100,
	}), fakeLLM
}

// NewTestAgentWithMaxSteps creates a core.Agent with a specific max steps limit.
func NewTestAgentWithMaxSteps(t *testing.T, maxSteps int, responses ...llm.CompletionResponse) (core.Agent, *FakeLLM) {
	t.Helper()
	fakeLLM := &FakeLLM{Responses: responses}
	cwd := t.TempDir()
	return core.NewAgent(core.Config{
		ID:     "test-agent",
		LLM:    fakeLLM,
		System: core.NewSystem(),
		Tools:  buildAllRegisteredTools(cwd),

		CWD:      cwd,
		MaxSteps: maxSteps,
	}), fakeLLM
}

// BuildTestTools adapts all globally registered tools into a core.Tools for use in tests.
func BuildTestTools(t *testing.T) core.Tools {
	t.Helper()
	return buildAllRegisteredTools(t.TempDir())
}

// RunAgent sends a prompt to the agent, drains its outbox, and returns the result.
// It sends SigStop after the first OnTurn event (single cycle).
func RunAgent(ctx context.Context, ag core.Agent, prompt string) (core.Result, error) {
	if err := ctx.Err(); err != nil {
		return core.Result{}, err
	}

	var agentErr error
	done := make(chan struct{})
	go func() {
		defer close(done)
		agentErr = ag.Run(ctx)
	}()

	select {
	case ag.Inbox() <- core.Message{Role: core.RoleUser, Content: prompt}:
	case <-ctx.Done():
		<-done
		return core.Result{}, ctx.Err()
	}

	var result core.Result
	var hasResult bool
	for ev := range ag.Outbox() {
		if ev.Type == core.OnTurn {
			if r, ok := ev.Result(); ok {
				result = r
				hasResult = true
			}
			select {
			case ag.Inbox() <- core.Message{Signal: core.SigStop}:
			case <-ctx.Done():
			}
		}
	}

	<-done

	if agentErr != nil && !hasResult {
		return result, agentErr
	}
	return result, nil
}
