package runtime

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

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/agenttool"
	"github.com/smallnest/pigo/internal/provider"
)

// collectStream drains a LoopEventStream, returning the event types in order
// and the run result messages.
func collectStream(t *testing.T, s *LoopEventStream) ([]string, []agentcore.AgentMessage) {
	t.Helper()
	var kinds []string
	for ev := range s.Events() {
		kinds = append(kinds, ev.EventType())
	}
	msgs, err := s.Result(context.Background())
	if err != nil {
		t.Fatalf("stream result: %v", err)
	}
	return kinds, msgs
}

// oneToolAssistant builds an assistant message with a single tool call.
func oneToolAssistant(id, name string) agentcore.AssistantMessage {
	return agentcore.AssistantMessage{
		RoleField:  agentcore.RoleAssistant,
		StopReason: agentcore.StopReasonToolUse,
		Content:    agentcore.ContentList{agentcore.NewToolCallContent(id, name, json.RawMessage(`{}`))},
	}
}

// scriptedStream returns a StreamFn that emits one StreamDoneEvent per call,
// consuming msgs in order. Extra calls beyond msgs emit a plain end_turn.
func scriptedStream(msgs []agentcore.AssistantMessage) provider.StreamFn {
	i := 0
	return func(ctx context.Context, model string, llm provider.LlmContext, cfg provider.StreamConfig) (*provider.AssistantMessageEventStream, error) {
		var msg agentcore.AssistantMessage
		if i < len(msgs) {
			msg = msgs[i]
		} else {
			msg = agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn}
		}
		i++
		s := provider.NewAssistantMessageEventStream(0)
		go func() {
			_ = s.Emit(ctx, provider.StreamDoneEvent{Message: msg})
			s.Close()
		}()
		return s, nil
	}
}

func newRunCfg(stream provider.StreamFn, tools ...agentcore.AgentTool) RunConfig {
	reg := agenttool.NewToolRegistry()
	for _, tl := range tools {
		_ = reg.Register(tl)
	}
	return RunConfig{
		LoopConfig: LoopConfig{Model: "fake", Stream: stream},
		Batch:      agenttool.BatchConfig{ToolExecutorConfig: agenttool.ToolExecutorConfig{Registry: reg}},
	}
}

func TestAgentLoopNoToolCallsSingleTurn(t *testing.T) {
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn, Content: agentcore.ContentList{agentcore.NewTextContent("hi")}},
	}))
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, msgs := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))

	want := []string{agentcore.EventAgentStart, agentcore.EventTurnStart, agentcore.EventMessageEnd, agentcore.EventTurnEnd, agentcore.EventTelemetry, agentcore.EventAgentEnd}
	assertEventKinds(t, kinds, want)
	if len(msgs) != 1 {
		t.Fatalf("run produced %d messages, want 1: %+v", len(msgs), msgs)
	}
}

func TestAgentLoopInnerLoopFeedsToolResults(t *testing.T) {
	// Turn 1: tool call. Turn 2: no tool call → inner loop ends.
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		oneToolAssistant("c1", "echo"),
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn, Content: agentcore.ContentList{agentcore.NewTextContent("done")}},
	}), echoTool("echo", agentcore.ToolExecutionParallel, false))
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, msgs := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))

	// Two turns; a tool executed in the first.
	if countKind(kinds, agentcore.EventTurnStart) != 2 {
		t.Errorf("expected 2 turns, got kinds %v", kinds)
	}
	if countKind(kinds, agentcore.EventToolExecutionEnd) != 1 {
		t.Errorf("expected 1 tool execution, got kinds %v", kinds)
	}
	// Messages produced: assistant(tool) + toolResult + assistant(done) = 3.
	if len(msgs) != 3 {
		t.Fatalf("expected 3 new messages, got %d: %+v", len(msgs), msgs)
	}
	if _, ok := msgs[1].(agentcore.ToolResultMessage); !ok {
		t.Errorf("expected message[1] to be a tool result, got %T", msgs[1])
	}
}

func TestAgentLoopFollowUpMessagesContinue(t *testing.T) {
	served := false
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn, Content: agentcore.ContentList{agentcore.NewTextContent("first")}},
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn, Content: agentcore.ContentList{agentcore.NewTextContent("second")}},
	}))
	cfg.GetFollowUpMessages = func(ctx context.Context, agentCtx *agentcore.AgentContext) []agentcore.AgentMessage {
		if served {
			return nil
		}
		served = true
		return []agentcore.AgentMessage{agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("more")}}}
	}
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, _ := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	if countKind(kinds, agentcore.EventTurnStart) != 2 {
		t.Errorf("follow-up should drive a second turn, got kinds %v", kinds)
	}
}

func TestAgentLoopShouldStopAfterTurn(t *testing.T) {
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		oneToolAssistant("c1", "echo"),
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn},
	}), echoTool("echo", agentcore.ToolExecutionParallel, false))
	cfg.ShouldStopAfterTurn = func(ctx context.Context, agentCtx *agentcore.AgentContext) bool { return true }
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, _ := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	// Stops after the first turn_end, so only one turn.
	if countKind(kinds, agentcore.EventTurnStart) != 1 {
		t.Errorf("shouldStopAfterTurn=true must stop after one turn, got %v", kinds)
	}
	if kinds[len(kinds)-1] != agentcore.EventAgentEnd {
		t.Errorf("run must end with agent_end, got %v", kinds)
	}
}

func TestAgentLoopSteeringInjected(t *testing.T) {
	var injectedSeen bool
	steer := agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("steer")}}
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		oneToolAssistant("c1", "echo"),
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn},
	}), echoTool("echo", agentcore.ToolExecutionParallel, false))
	pulled := false
	cfg.GetSteeringMessages = func(ctx context.Context) []agentcore.AgentMessage {
		if pulled {
			return nil
		}
		pulled = true
		return []agentcore.AgentMessage{steer}
	}
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}
	collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	for _, m := range agentCtx.Messages {
		if um, ok := m.(agentcore.UserMessage); ok && len(um.Content) == 1 {
			if tc, ok := um.Content[0].(agentcore.TextContent); ok && tc.Text == "steer" {
				injectedSeen = true
			}
		}
	}
	if !injectedSeen {
		t.Errorf("steering message was not injected into the context")
	}
}

func TestAgentLoopPrepareNextTurnSwapsModel(t *testing.T) {
	var seenModels []string
	streamFn := func(ctx context.Context, model string, llm provider.LlmContext, cfg provider.StreamConfig) (*provider.AssistantMessageEventStream, error) {
		seenModels = append(seenModels, model)
		var msg agentcore.AssistantMessage
		if len(seenModels) == 1 {
			msg = oneToolAssistant("c1", "echo")
		} else {
			msg = agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn}
		}
		s := provider.NewAssistantMessageEventStream(0)
		go func() { _ = s.Emit(ctx, provider.StreamDoneEvent{Message: msg}); s.Close() }()
		return s, nil
	}
	cfg := newRunCfg(streamFn, echoTool("echo", agentcore.ToolExecutionParallel, false))
	newModel := "swapped-model"
	cfg.PrepareNextTurn = func(ctx context.Context, agentCtx *agentcore.AgentContext) *TurnUpdate {
		return &TurnUpdate{Model: &newModel}
	}
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}
	collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	if len(seenModels) != 2 || seenModels[1] != newModel {
		t.Errorf("prepareNextTurn should swap model to %q, saw %v", newModel, seenModels)
	}
}

func TestAgentLoopLengthFailsToolCalls(t *testing.T) {
	// Turn 1: tool call but truncated (length). Turn 2: end.
	truncated := oneToolAssistant("c1", "echo")
	truncated.StopReason = agentcore.StopReasonLength
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		truncated,
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn},
	}), echoTool("echo", agentcore.ToolExecutionParallel, false))
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, msgs := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	// The tool must NOT have executed (truncated → failed instead).
	if countKind(kinds, agentcore.EventToolExecutionEnd) != 0 {
		t.Errorf("truncated message must not execute tools, got %v", kinds)
	}
	// A failed tool result must have been synthesized.
	var foundFail bool
	for _, m := range msgs {
		if tr, ok := m.(agentcore.ToolResultMessage); ok && tr.IsError && tr.ToolCallID == "c1" {
			foundFail = true
		}
	}
	if !foundFail {
		t.Errorf("expected a synthesized failed tool result for the truncated call")
	}
}

func TestAgentLoopErrorStopEndsRun(t *testing.T) {
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonError, ErrorMessage: "boom"},
	}))
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, _ := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	if countKind(kinds, agentcore.EventTurnStart) != 1 {
		t.Errorf("error stop must end after one turn, got %v", kinds)
	}
	if kinds[len(kinds)-1] != agentcore.EventAgentEnd {
		t.Errorf("run must end with agent_end, got %v", kinds)
	}
}

func TestAgentLoopAllTerminateStopsRun(t *testing.T) {
	term := true
	termTool := execTool{
		name: "quit",
		run: func(ctx context.Context, id string, args json.RawMessage, onUpdate agentcore.ToolUpdateFunc) (agentcore.AgentToolResult, error) {
			return agentcore.AgentToolResult{Content: agentcore.ContentList{agentcore.NewTextContent("bye")}, Terminate: &term}, nil
		},
	}
	cfg := newRunCfg(scriptedStream([]agentcore.AssistantMessage{
		oneToolAssistant("c1", "quit"),
		{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn}, // should never be reached
	}), termTool)
	agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}

	kinds, _ := collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
	if countKind(kinds, agentcore.EventTurnStart) != 1 {
		t.Errorf("terminate must end the run after one turn, got %v", kinds)
	}
}

func assertEventKinds(t *testing.T, got, want []string) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("event kinds = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("event[%d] = %q, want %q (full %v)", i, got[i], want[i], got)
		}
	}
}

func countKind(kinds []string, want string) int {
	n := 0
	for _, k := range kinds {
		if k == want {
			n++
		}
	}
	return n
}
