package eino

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

	"github.com/sundynix/sundynix-dispatcher/internal/harness"
	"github.com/sundynix/sundynix-dispatcher/internal/llm"
	"github.com/sundynix/sundynix-shared/contract"
)

// ---- RAG 管线集成测试：检索 → 注入 prompt → 生成 → refs 回流（喂忠实度评测）整条链。----

const ragSnippet = "西湖是杭州著名景点，三面环山。"

// kbTool 返回一个把 kb_search 应答成固定命中、并捕获入参的 fakeTools。
func kbTool(capture *contract.ToolCall) *fakeTools {
	return &fakeTools{fn: func(c *contract.ToolCall) *contract.ToolResult {
		if c.Tool == "kb_search" {
			if capture != nil {
				*capture = *c
			}
			return &contract.ToolResult{OK: true, Content: `[{"text":"` + ragSnippet + `","score":0.92}]`}
		}
		return &contract.ToolResult{OK: true, Content: ""}
	}}
}

// 对话 RAG：input→retriever→agent。检索命中应注入 agent 的 system prompt，且 refs 经 runGraph 回流。
func TestRAG_ConversationInjectsAndReturnsRefs(t *testing.T) {
	g := `{"nodes":[
		{"id":"i","kind":"input","config":{"text":"介绍杭州西湖"}},
		{"id":"r","kind":"retriever","config":{"kb":"travel"}},
		{"id":"a","kind":"agent","config":{"system":"你是导游"}}
	],"edges":[{"source":"i","target":"r"},{"source":"r","target":"a"}]}`
	// fakeLLM 回显 system 消息 → 借此断言检索片段已注入 prompt。
	ll := &fakeLLM{ready: true, stream: func(m []llm.ChatMessage) string { return m[0].Content }}
	var captured contract.ToolCall
	o := newOrch(ll, kbTool(&captured), &fakeSink{}, &fakeExec{})
	tk := &contract.Task{ID: "t1", Graph: json.RawMessage(g), Meta: map[string]any{contract.MetaUserID: "u42"}}

	ans, refs, err := o.runComposeGraph(context.Background(), tk, o.tracer("t1"))
	if err != nil {
		t.Fatal(err)
	}
	if captured.Tool != "kb_search" {
		t.Fatal("应调用 kb_search 检索")
	}
	// owner 作用域：未带 "/" 的库名应被前缀成 uid/kb。
	if kb, _ := captured.Args["kb"].(string); kb != "u42/travel" {
		t.Errorf("kb 应按 owner 作用域为 u42/travel，got %q", kb)
	}
	if !strings.Contains(ans, ragSnippet) {
		t.Errorf("检索片段应注入 agent prompt，prompt=%q", ans)
	}
	// refs 回流（喂忠实度评测的 sources）。
	if len(refs) == 0 || !strings.Contains(strings.Join(refs, ""), ragSnippet) {
		t.Errorf("runGraph 应回流含检索片段的 refs，got %v", refs)
	}
}

// refs 回流后应触发「有来源」的忠实度评测路径（grounded judge），而非无来源路径。
func TestRAG_RefsDriveGroundedEval(t *testing.T) {
	ll := &fakeLLM{ready: true}
	o := newOrch(ll, &fakeTools{}, &fakeSink{}, &fakeExec{})
	var sawSources bool
	o.eval = harness.NewEvaluator(func() bool { return true },
		func(_ context.Context, _, user string) (string, error) {
			// grounded judge 的 user 含「检索资料」段；无来源路径则没有。
			if strings.Contains(user, "检索资料") {
				sawSources = true
				return `{"quality":5,"faithfulness":5,"unsupported":[],"reason":"忠实"}`, nil
			}
			return `{"score":3,"reason":"无来源"}`, nil
		})
	sink := &fakeEvalSink{}
	o.evalSink = sink

	o.evaluate(&contract.Task{ID: "t1"}, "介绍西湖", "西湖三面环山。", []string{ragSnippet})

	if !sawSources {
		t.Fatal("有 refs 时应走 grounded（含检索资料）评测路径")
	}
	if ev := sink.get(); ev == nil || ev.Faithful == 0 {
		t.Errorf("应记录忠实度分，got %+v", ev)
	}
}

// 报告 RAG：writeSection 检索命中应注入撰写 prompt。
func TestRAG_ReportSectionInjectsRefs(t *testing.T) {
	// writeSection 走 Chat（非流式）→ fakeLLM.chat 回显 user 消息以验证注入。
	ll := &fakeLLM{ready: true, chat: func(m []llm.ChatMessage) (string, error) {
		return m[len(m)-1].Content, nil // user 消息（含参考资料）
	}}
	o := newOrch(ll, kbTool(nil), &fakeSink{}, &fakeExec{})

	body, _ := o.writeSection(context.Background(), "杭州旅游", "u1/travel", "西湖", o.tracer("t1"), "section:0")
	if !strings.Contains(body, ragSnippet) {
		t.Errorf("报告章节撰写 prompt 应含检索资料，got %q", body)
	}
}

// 优雅降级：检索工具不可用时返回空 refs，agent 仍正常出答案、不报错。
func TestRAG_DegradesWhenRetrieverDown(t *testing.T) {
	g := `{"nodes":[
		{"id":"i","kind":"input","config":{"text":"介绍杭州"}},
		{"id":"r","kind":"retriever","config":{"kb":"travel"}},
		{"id":"a","kind":"agent","config":{"system":"导游答复"}}
	],"edges":[{"source":"i","target":"r"},{"source":"r","target":"a"}]}`
	ll := &fakeLLM{ready: true, stream: func(m []llm.ChatMessage) string { return m[0].Content }}
	// kb_search 返回失败 → retrieve 降级空。
	ft := &fakeTools{fn: func(c *contract.ToolCall) *contract.ToolResult {
		if c.Tool == "kb_search" {
			return &contract.ToolResult{OK: false, Error: "milvus down"}
		}
		return &contract.ToolResult{OK: true}
	}}
	o := newOrch(ll, ft, &fakeSink{}, &fakeExec{})

	ans, refs, err := o.runComposeGraph(context.Background(), &contract.Task{ID: "t1", Graph: json.RawMessage(g), Meta: map[string]any{}}, o.tracer("t1"))
	if err != nil {
		t.Fatalf("检索失败不应让整图失败: %v", err)
	}
	if len(refs) != 0 {
		t.Errorf("检索失败应空 refs，got %v", refs)
	}
	if !strings.Contains(ans, "导游答复") {
		t.Errorf("无检索仍应正常出答案，got %q", ans)
	}
}
