package services

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	einomodel "github.com/cloudwego/eino-ext/components/model/openai"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/schema"
	"wa-assistant/backend/models"
)

func TestToolRecoveryReturnsBudgetToAgentWithoutAborting(t *testing.T) {
	agenticTestDB(t)
	s := &agentTurnState{calls: 6}
	tools, err := makeAgentTools(1, ChatOptions{DryRun: true}, s)
	if err != nil {
		t.Fatal(err)
	}
	for _, base := range tools {
		info, _ := base.Info(context.Background())
		if info.Name == "check_answer" || info.Name == "request_handoff" {
			continue
		}
		out, err := base.(tool.InvokableTool).InvokableRun(context.Background(), `{}`)
		if err != nil || !strings.Contains(out, "tool_budget_exhausted") || !strings.Contains(out, "check_answer") || s.calls != 6 {
			t.Errorf("%s aborts the agent instead of allowing a checked answer: out=%s err=%v", info.Name, out, err)
		}
	}
}

// Reproduce a parallel batch crossing the tool cap using the actual native
// adapter and Eino. Earlier facts must remain usable; no unchecked text escapes.
func TestToolRecoveryNativeLoopFinishesFromExistingEvidence(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Apa itu kelas Laravel?", Answer: "Kelas Laravel adalah kursus web.", Active: true, ReviewStatus: "published", Scope: "general"}
	if err := db.Create(&k).Error; err != nil {
		t.Fatal(err)
	}
	InvalidateKB(1)
	s := &agentTurnState{calls: 5, knowledge: []models.Knowledge{k}, userMessage: k.Question, policy: responsePolicyForLength(selectAIResponsePolicy(k.Question, "", "", 1), "short", k.Question)}
	reviews, requests := 0, 0
	s.reviewAnswer = func(context.Context, string, string, string) (answerEvidenceReview, error) {
		reviews++
		return answerEvidenceReview{Supported: true}, nil
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests++
		var body struct {
			Tools []struct {
				Function struct{ Name string } `json:"function"`
			} `json:"tools"`
			Messages []struct{ Role, Content, ToolCallID string } `json:"messages"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Error(err)
			w.WriteHeader(400)
			return
		}
		var calls []schema.ToolCall
		if requests == 1 {
			calls = []schema.ToolCall{
				{ID: "read", Type: "function", Function: schema.FunctionCall{Name: "read_knowledge", Arguments: fmt.Sprintf(`{"source_id":%d}`, k.ID)}},
				{ID: "search", Type: "function", Function: schema.FunctionCall{Name: "search_knowledge", Arguments: `{"query":"kelas Laravel"}`}},
			}
		} else {
			if len(body.Tools) != 2 {
				t.Errorf("exhausted data tools still offered: %d", len(body.Tools))
			}
			for _, item := range body.Tools {
				if item.Function.Name != "check_answer" && item.Function.Name != "request_handoff" {
					t.Errorf("exhausted tool still selectable: %s", item.Function.Name)
				}
			}
			results, blocked := 0, false
			for _, msg := range body.Messages {
				if msg.Role == "tool" {
					results++
					blocked = blocked || strings.Contains(msg.Content, "tool_budget_exhausted")
				}
			}
			if results != 2 || !blocked {
				t.Error("parallel tool transcript or explicit budget feedback missing")
			}
			calls = []schema.ToolCall{{ID: "check", Type: "function", Function: schema.FunctionCall{Name: "check_answer", Arguments: fmt.Sprintf(`{"answer":%q,"source_ids":[%d],"kind":"answer"}`, k.Answer, k.ID)}}}
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]any{"id": fmt.Sprint(requests), "choices": []any{map[string]any{"index": 0, "finish_reason": "tool_calls", "message": map[string]any{"role": "assistant", "reasoning_content": "private native reasoning", "tool_calls": calls}}}})
	}))
	defer server.Close()
	p := aiPreset{Key: "deepseek", Model: "deepseek-flash", APIKey: "fixture-key"}
	cfg := nativeAIModelConfig(p, 2200, .2)
	cfg.BaseURL = server.URL
	cm, err := einomodel.NewChatModel(context.Background(), cfg)
	if err != nil {
		t.Fatal(err)
	}
	tools, err := makeAgentTools(1, ChatOptions{DryRun: true}, s)
	if err != nil {
		t.Fatal(err)
	}
	store := NewSimulationLogStore(t.TempDir())
	id := NewSimulationID()
	recorder, err := store.Start(1, id, NewSimulationID(), nil)
	if err != nil {
		t.Fatal(err)
	}
	out, err := executeAnswerAgent(recorder.Context(context.Background()), cm, tools, []*schema.Message{schema.UserMessage(k.Question)}, s)
	if err != nil || out == nil || out.Content != k.Answer || s.approved == nil || requests != 2 || reviews != 1 || s.calls != 6 {
		t.Fatalf("existing evidence cannot finish after tool cap: out=%v requests=%d reviews=%d calls=%d err=%v", out, requests, reviews, s.calls, err)
	}
	recorder.Finish("completed", map[string]any{"reply": out.Content})
	record, err := store.Read(1, id)
	if err != nil {
		t.Fatal(err)
	}
	bounded, synthesizing := false, false
	for _, event := range record.Events {
		if event.Stage == "tool_completed" && event.Status == "error" {
			t.Fatal("handled budget limit was logged as execution failure")
		}
		bounded = bounded || event.Stage == "tool_completed" && event.Status == "rejected" && event.Data["issue"] == "tool_budget_exhausted"
		synthesizing = synthesizing || event.Stage == "answer_synthesis"
	}
	if !bounded || !synthesizing {
		t.Fatal("budget recovery missing from realtime log")
	}
}
