package workflow

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"sync"
	"testing"
)

// ---- sahte yetenekler ----

// fakeLLM Complete çağrılarını kaydeder; fn verilmişse ona devreder.
type fakeLLM struct {
	mu    sync.Mutex
	calls []LLMRequest
	fn    func(ctx context.Context, req LLMRequest) (LLMResult, error)
}

func (f *fakeLLM) Complete(ctx context.Context, req LLMRequest) (LLMResult, error) {
	f.mu.Lock()
	f.calls = append(f.calls, req)
	f.mu.Unlock()
	if f.fn == nil {
		return LLMResult{Text: "sahte yanıt"}, nil
	}
	return f.fn(ctx, req)
}

func (f *fakeLLM) requests() []LLMRequest {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]LLMRequest(nil), f.calls...)
}

// fakeTools çağrıları "ad + girdi JSON'u" olarak kaydeder.
type fakeTools struct {
	mu    sync.Mutex
	calls []string
	out   map[string]any
	err   error
}

func (f *fakeTools) RunTool(_ context.Context, _, name string, input map[string]any) (map[string]any, error) {
	raw, _ := json.Marshal(input)
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls = append(f.calls, name+" "+string(raw))
	if f.err != nil {
		return nil, f.err
	}
	return f.out, nil
}

type fakeKnowledge struct {
	mu       sync.Mutex
	queries  []string
	passages []string
}

func (f *fakeKnowledge) Passages(_ context.Context, _, query string, _ int) ([]string, error) {
	f.mu.Lock()
	f.queries = append(f.queries, query)
	f.mu.Unlock()
	return f.passages, nil
}

func (f *fakeKnowledge) asked() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]string(nil), f.queries...)
}

type fakeMemory struct {
	mu   sync.Mutex
	seen []string
	msgs []MemoryMessage
}

func (f *fakeMemory) Recent(_ context.Context, _, convID string, _ int) ([]MemoryMessage, error) {
	f.mu.Lock()
	f.seen = append(f.seen, convID)
	f.mu.Unlock()
	return f.msgs, nil
}

type fakeSkills struct {
	mu    sync.Mutex
	query string
	args  map[string]any
	out   SkillRun
}

func (f *fakeSkills) RunSkill(_ context.Context, _, query string, args map[string]any) (SkillRun, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.query, f.args = query, args
	return f.out, nil
}

// ---- akış kurucuları ----

// testWorkflow akışı kurar ve Normalize uygular (kenar kimlikleri otomatik dolar).
func testWorkflow(nodes []Node, edges []Edge) *Workflow {
	w := &Workflow{ID: "wf_test", UserID: "u1", Name: "Test akışı", Nodes: nodes, Edges: edges}
	Normalize(w)
	return w
}

func wfNode(id string, t NodeType, cfg map[string]any) Node {
	return Node{ID: id, Type: t, Config: cfg}
}

func wfEdge(from, branch, to string) Edge {
	return Edge{From: from, Branch: branch, To: to}
}

// runnableWorkflow yetenek gerektirmeyen doğrusal akıştır (input → transform → output).
func runnableWorkflow() Workflow {
	return testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("t1", NodeTransform, map[string]any{"template": "Merhaba: {{input}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "t1"), wfEdge("t1", BranchNext, "out")},
	).Clone()
}

// draftWorkflow kaydedilebilir ama ÇALIŞTIRILAMAZ taslaktır (output node'u yok).
func draftWorkflow() Workflow {
	return testWorkflow([]Node{wfNode("in", NodeInput, nil)}, nil).Clone()
}

// llmWorkflow tek LLM node'u içerir: LLM yeteneği olmadan çalıştırılamaz.
func llmWorkflow() Workflow {
	return testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("llm1", NodeLLM, map[string]any{"prompt": "Şunu yaz: {{input}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "llm1"), wfEdge("llm1", BranchNext, "out")},
	).Clone()
}

// runWF akışı çalıştırır; hata olsa da doldurulmuş kaydı döndürür (adımlar incelenebilsin).
func runWF(t *testing.T, wf *Workflow, deps Deps, env RunEnv) (*Execution, error) {
	t.Helper()
	exec := &Execution{ID: "exec_test", UserID: env.UserID, WorkflowID: wf.ID, Status: RunRunning, Steps: []Step{}}
	return exec, Run(context.Background(), wf, deps, env, exec, nil)
}

// Doğrusal akış: girdi LLM'e, LLM çıktısı transform'a, nihai çıktı output'a akmalı;
// adımlar sırayla ve tamamı "ok" olmalı, token sayaçları toplanmalı.
func TestRun_LinearFlow(t *testing.T) {
	llm := &fakeLLM{fn: func(_ context.Context, req LLMRequest) (LLMResult, error) {
		return LLMResult{Text: "Merhaba " + req.Prompt, PromptTokens: 3, CompletionTokens: 4}, nil
	}}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("llm1", NodeLLM, map[string]any{"prompt": "dünyaya selam: {{input}}"}),
			wfNode("t1", NodeTransform, map[string]any{"template": "Özet: {{prev}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "llm1"), wfEdge("llm1", BranchNext, "t1"), wfEdge("t1", BranchNext, "out")},
	)
	exec, err := runWF(t, wf, Deps{LLM: llm}, RunEnv{UserID: "u1", Input: "dünya"})
	if err != nil {
		t.Fatal(err)
	}
	if want := "Özet: Merhaba dünyaya selam: dünya"; exec.Output != want {
		t.Errorf("çıktı: %q, beklenen %q", exec.Output, want)
	}
	wantSteps := []string{"in", "llm1", "t1", "out"}
	if len(exec.Steps) != len(wantSteps) {
		t.Fatalf("adım sayısı: %d (%v)", len(exec.Steps), wantSteps)
	}
	for i, s := range exec.Steps {
		if s.NodeID != wantSteps[i] || s.Index != i+1 || s.Status != StepOK || s.FinishedAt == nil {
			t.Errorf("adım %d: %+v", i, s)
		}
	}
	if exec.PromptTokens != 3 || exec.CompletionTokens != 4 {
		t.Errorf("token sayacı: %d/%d (3/4 bekleniyordu)", exec.PromptTokens, exec.CompletionTokens)
	}
	if got := llm.requests(); len(got) != 1 || got[0].Prompt != "dünyaya selam: dünya" {
		t.Errorf("LLM istemi: %+v", got)
	}
	if exec.Steps[1].Input != "dünyaya selam: dünya" {
		t.Errorf("adım kaydında işlenmiş istem görünmüyor: %q", exec.Steps[1].Input)
	}
}

// Koşul: doğru dal işlenmeli, "prev" koşulun boolean sonucuyla DEĞİŞTİRİLMEMELİ
// (yoksa dalın ilk node'u yanlış girdiyi görür).
func TestRun_ConditionBranches(t *testing.T) {
	build := func() *Workflow {
		return testWorkflow(
			[]Node{
				wfNode("in", NodeInput, nil),
				wfNode("cond", NodeCondition, map[string]any{"left": "{{input}}", "op": "contains", "right": "merhaba"}),
				wfNode("t_yes", NodeTransform, map[string]any{"template": "evet+{{prev}}"}),
				wfNode("t_no", NodeTransform, map[string]any{"template": "hayır+{{prev}}"}),
				wfNode("out", NodeOutput, nil),
			},
			[]Edge{
				wfEdge("in", BranchNext, "cond"),
				wfEdge("cond", BranchTrue, "t_yes"), wfEdge("cond", BranchFalse, "t_no"),
				wfEdge("t_yes", BranchNext, "out"), wfEdge("t_no", BranchNext, "out"),
			},
		)
	}
	t.Run("doğru dal", func(t *testing.T) {
		exec, err := runWF(t, build(), Deps{}, RunEnv{UserID: "u1", Input: "Merhaba dünya"})
		if err != nil {
			t.Fatal(err)
		}
		if exec.Output != "evet+Merhaba dünya" {
			t.Errorf("çıktı: %q", exec.Output)
		}
		if exec.Steps[1].Branch != BranchTrue {
			t.Errorf("seçilen dal: %q", exec.Steps[1].Branch)
		}
		for _, s := range exec.Steps {
			if s.NodeID == "t_no" {
				t.Error("false dalı çalıştırılmamalıydı")
			}
		}
	})
	t.Run("yanlış dal", func(t *testing.T) {
		exec, err := runWF(t, build(), Deps{}, RunEnv{UserID: "u1", Input: "görüşürüz"})
		if err != nil {
			t.Fatal(err)
		}
		if exec.Output != "hayır+görüşürüz" {
			t.Errorf("çıktı: %q", exec.Output)
		}
		if exec.Steps[1].Branch != BranchFalse {
			t.Errorf("seçilen dal: %q", exec.Steps[1].Branch)
		}
	})
}

// Döngü: her öğe için gövde çalışır, {{item}}/{{index}} doğru sırayla dolar,
// sonuçlar toplanır ve döngüden sonraki node diziyi alır.
func TestRun_LoopIteratesAndCollects(t *testing.T) {
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("loop", NodeLoop, map[string]any{"items": `["a","b","c"]`}),
			wfNode("body", NodeTransform, map[string]any{"template": "{{index}}:{{item}}"}),
			wfNode("out", NodeOutput, map[string]any{"template": "{{loop}}"}),
		},
		[]Edge{
			wfEdge("in", BranchNext, "loop"),
			wfEdge("loop", BranchBody, "body"),
			wfEdge("loop", BranchNext, "out"),
		},
	)
	exec, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if exec.Output != `["0:a","1:b","2:c"]` {
		t.Errorf("döngü çıktısı: %q", exec.Output)
	}
	var iter []int
	for _, s := range exec.Steps {
		if s.NodeID == "body" {
			iter = append(iter, s.Iteration)
		}
	}
	if len(iter) != 3 || iter[0] != 1 || iter[1] != 2 || iter[2] != 3 {
		t.Errorf("yineleme numaraları: %v (1,2,3 bekleniyordu)", iter)
	}
	// Döngü dışındaki adımların yinelemesi 0'dır.
	if exec.Steps[0].Iteration != 0 || exec.Steps[len(exec.Steps)-1].Iteration != 0 {
		t.Error("döngü dışındaki adımların Iteration değeri 0 olmalı")
	}
}

// Döngü öğe sınırı: MaxLoopIterations aşılırsa sessizce kesmek yerine HATA verilir.
func TestRun_LoopItemLimit(t *testing.T) {
	items := make([]string, MaxLoopIterations+1)
	for i := range items {
		items[i] = `"öğe"`
	}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("loop", NodeLoop, map[string]any{"items": "[" + strings.Join(items, ",") + "]"}),
			wfNode("body", NodeTransform, map[string]any{"template": "{{item}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{
			wfEdge("in", BranchNext, "loop"),
			wfEdge("loop", BranchBody, "body"),
			wfEdge("loop", BranchNext, "out"),
		},
	)
	_, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err == nil || !strings.Contains(err.Error(), "döngü en çok") {
		t.Fatalf("hata bekleniyordu, gelen: %v", err)
	}
}

// Nesne çıktısı: tool sonucu nesnedir; alan (tool1.x) ve dizi erişimi (liste.1) çalışır.
func TestRun_ToolObjectOutputAndFieldAccess(t *testing.T) {
	tools := &fakeTools{out: map[string]any{"x": "değer", "liste": []any{"a", "b"}}}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("tool1", NodeTool, map[string]any{"name": "fiko.now", "input": map[string]any{"sorgu": "{{input}}"}}),
			wfNode("t1", NodeTransform, map[string]any{"template": "{{tool1.x}}-{{tool1.liste.1}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "tool1"), wfEdge("tool1", BranchNext, "t1"), wfEdge("t1", BranchNext, "out")},
	)
	exec, err := runWF(t, wf, Deps{Tools: tools}, RunEnv{UserID: "u1", Input: "saat kaç"})
	if err != nil {
		t.Fatal(err)
	}
	if exec.Output != "değer-b" {
		t.Errorf("alan erişimi: %q", exec.Output)
	}
	if exec.ToolCalls != 1 {
		t.Errorf("tool çağrı sayacı: %d", exec.ToolCalls)
	}
	if len(tools.calls) != 1 || !strings.Contains(tools.calls[0], `"sorgu":"saat kaç"`) {
		t.Errorf("tool girdisi çözümlenmemiş: %v", tools.calls)
	}
}

// RAG/memory/skill node'ları yeteneklerini doğru argümanlarla çağırır.
func TestRun_PassesDependenciesThrough(t *testing.T) {
	kb := &fakeKnowledge{passages: []string{"parça1", "parça2"}}
	mem := &fakeMemory{msgs: []MemoryMessage{{Role: "user", Content: "selam"}, {Role: "assistant", Content: "merhaba"}}}
	sk := &fakeSkills{out: SkillRun{Skill: "çizim", Tool: "fiko.draw", Output: map[string]any{"ok": true}}}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("mem", NodeMemory, map[string]any{"limit": 5}),
			wfNode("rag", NodeRAG, map[string]any{"query": "hakkında: {{input}}"}),
			wfNode("sk", NodeSkill, map[string]any{"query": "yap", "arguments": map[string]any{"metin": "{{rag}}"}}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{
			wfEdge("in", BranchNext, "mem"), wfEdge("mem", BranchNext, "rag"),
			wfEdge("rag", BranchNext, "sk"), wfEdge("sk", BranchNext, "out"),
		},
	)
	exec, err := runWF(t, wf, Deps{Knowledge: kb, Memory: mem, Skills: sk},
		RunEnv{UserID: "u1", ConversationID: "conv_9", Input: "uçak"})
	if err != nil {
		t.Fatal(err)
	}
	if exec.Steps[1].Output != "Kullanıcı: selam\nAsistan: merhaba" {
		t.Errorf("memory biçimi: %q", exec.Steps[1].Output)
	}
	if exec.Steps[2].Output != "parça1\n\nparça2" {
		t.Errorf("rag çıktısı: %q", exec.Steps[2].Output)
	}
	if got := kb.asked(); len(got) != 1 || got[0] != "hakkında: uçak" {
		t.Errorf("rag sorgusu: %v", got)
	}
	if len(mem.seen) != 1 || mem.seen[0] != "conv_9" {
		t.Errorf("memory konuşma kimliği: %v", mem.seen)
	}
	if sk.query != "yap" || sk.args["metin"] != "parça1\n\nparça2" {
		t.Errorf("skill argümanları: %q / %v", sk.query, sk.args)
	}
	if exec.ToolCalls != 1 {
		t.Errorf("skill'in çalıştırdığı tool sayacı: %d", exec.ToolCalls)
	}
}

// Bilinmeyen değişken HATADIR: şablon yazım hatası sessizce boş metne dönüşmemeli.
func TestRun_UnknownTemplateVariableIsError(t *testing.T) {
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("t1", NodeTransform, map[string]any{"template": "{{olmayan}}"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "t1"), wfEdge("t1", BranchNext, "out")},
	)
	_, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err == nil || !strings.Contains(err.Error(), "bilinmeyen değişken") {
		t.Fatalf("hata bekleniyordu, gelen: %v", err)
	}
}

// Henüz çalışmamış bir node'un değişkeni boş metindir (hata değil): akışta "sıra" vardır,
// ama değer henüz üretilmemiştir.
func TestRun_NotYetRunNodeIsEmpty(t *testing.T) {
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("t1", NodeTransform, map[string]any{"template": "[{{sonraki}}]"}),
			wfNode("sonraki", NodeTransform, map[string]any{"template": "değer"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{
			wfEdge("in", BranchNext, "t1"), wfEdge("t1", BranchNext, "sonraki"),
			wfEdge("sonraki", BranchNext, "out"),
		},
	)
	exec, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if exec.Steps[1].Output != "[]" {
		t.Errorf("çalışmamış node değişkeni: %q", exec.Steps[1].Output)
	}
	if exec.Output != "değer" {
		t.Errorf("nihai çıktı: %q", exec.Output)
	}
}

// output node'u zinciri BİTİRİR: sonraki node'lar çalışmaz.
func TestRun_OutputTerminatesChain(t *testing.T) {
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("out1", NodeOutput, map[string]any{"template": "dur"}),
			wfNode("sonraki", NodeTransform, map[string]any{"template": "değer"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{
			wfEdge("in", BranchNext, "out1"), wfEdge("out1", BranchNext, "sonraki"),
			wfEdge("sonraki", BranchNext, "out"),
		},
	)
	exec, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	if exec.Output != "dur" {
		t.Errorf("çıktı: %q", exec.Output)
	}
	for _, s := range exec.Steps {
		if s.NodeID == "sonraki" {
			t.Error("output sonrası node çalıştırılmamalıydı")
		}
	}
}

// İptal: bağlam iptal edilirse yürütme hata döndürür.
func TestRun_ContextCancelled(t *testing.T) {
	llm := &fakeLLM{fn: func(ctx context.Context, _ LLMRequest) (LLMResult, error) {
		return LLMResult{}, ctx.Err()
	}}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	exec := &Execution{ID: "e", UserID: "u1", Status: RunRunning, Steps: []Step{}}
	wf := llmWorkflow().Clone()
	if err := Run(ctx, &wf, Deps{LLM: llm}, RunEnv{UserID: "u1", Input: "x"}, exec, nil); !errors.Is(err, context.Canceled) {
		t.Fatalf("iptal hatası bekleniyordu, gelen: %v", err)
	}
}

// Node hatası: adım "error" kapanır, hata node adını içerir ve zincir DURUR.
func TestRun_NodeErrorStopsChain(t *testing.T) {
	llm := &fakeLLM{fn: func(context.Context, LLMRequest) (LLMResult, error) {
		return LLMResult{}, errors.New("model yanıt vermedi")
	}}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("llm1", NodeLLM, map[string]any{"prompt": "{{input}}"}),
			wfNode("t1", NodeTransform, map[string]any{"template": "asla çalışmaz"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{wfEdge("in", BranchNext, "llm1"), wfEdge("llm1", BranchNext, "t1"), wfEdge("t1", BranchNext, "out")},
	)
	exec, err := runWF(t, wf, Deps{LLM: llm}, RunEnv{UserID: "u1", Input: "x"})
	if err == nil || !strings.Contains(err.Error(), "model yanıt vermedi") {
		t.Fatalf("hata bekleniyordu, gelen: %v", err)
	}
	if failed := exec.Steps[1]; failed.Status != StepError || failed.FinishedAt == nil || failed.Error == "" {
		t.Errorf("hatalı adım kapatılmamış: %+v", failed)
	}
	if exec.Steps[0].Status != StepOK {
		t.Error("önceki adım başarılı olmalıydı")
	}
	for _, s := range exec.Steps {
		if s.Status == StepRunning {
			t.Error(`"çalışıyor" görünen adım kalmamalı`)
		}
		if s.NodeID == "t1" {
			t.Error("hata sonrası zincir devam etmemeliydi")
		}
	}
}

// Yürütme öncesi denetimler: çalıştırılamayan akış *ValidationError, eksik yetenek *CapabilityError.
func TestRun_RejectsNotRunnableAndMissingDeps(t *testing.T) {
	t.Run("output yok", func(t *testing.T) {
		wf := draftWorkflow().Clone()
		exec := &Execution{ID: "e", UserID: "u1", Steps: []Step{}}
		err := Run(context.Background(), &wf, Deps{}, RunEnv{UserID: "u1"}, exec, nil)
		var ve *ValidationError
		if !errors.As(err, &ve) || !ve.NotRunnable {
			t.Fatalf("NotRunnable ValidationError bekleniyordu, gelen: %v", err)
		}
	})
	t.Run("yetenek bağlı değil", func(t *testing.T) {
		wf := llmWorkflow().Clone()
		exec := &Execution{ID: "e", UserID: "u1", Steps: []Step{}}
		err := Run(context.Background(), &wf, Deps{}, RunEnv{UserID: "u1", Input: "x"}, exec, nil)
		var ce *CapabilityError
		if !errors.As(err, &ce) {
			t.Fatalf("CapabilityError bekleniyordu, gelen: %v", err)
		}
		if len(ce.Types) != 1 || ce.Types[0] != NodeLLM {
			t.Errorf("eksik yetenekler: %v", ce.Types)
		}
		if !strings.Contains(err.Error(), "llm") {
			t.Errorf("hata mesajı türü yüksekte söylemiyor: %q", err.Error())
		}
	})
}

// notify her adım değişiminde (running ve kapanmış hâliyle) çağrılır.
func TestRun_NotifySeesProgress(t *testing.T) {
	var seen []Execution
	wf := runnableWorkflow().Clone()
	exec := &Execution{ID: "e", UserID: "u1", Steps: []Step{}}
	if err := Run(context.Background(), &wf, Deps{}, RunEnv{UserID: "u1", Input: "x"}, exec, func(s Execution) {
		seen = append(seen, s)
	}); err != nil {
		t.Fatal(err)
	}
	if len(seen) != 2*len(exec.Steps) {
		t.Errorf("bildirim sayısı: %d (%d adım için 2'şer bekleniyordu)", len(seen), len(exec.Steps))
	}
	last := seen[len(seen)-1]
	if last.Steps[len(last.Steps)-1].Status == StepRunning {
		t.Error("son bildirimde tüm adımlar kapanmış olmalı")
	}
	// Bildirim kopyalarıdır: çağıranın verisi yürütmeyi bozmamalı.
	last.Steps[0].Status = "BOZUK"
	if exec.Steps[0].Status == "BOZUK" {
		t.Error("bildirim kopyası yürütme kaydıyla bellek paylaşıyor")
	}
}

// Adım sınırı: uzun bir döngü sonsuza dek sürmemeli, MaxSteps'te hata vermeli.
func TestRun_StepLimitStopsRun(t *testing.T) {
	items := make([]string, MaxLoopIterations)
	for i := range items {
		items[i] = `"öğe"`
	}
	wf := testWorkflow(
		[]Node{
			wfNode("in", NodeInput, nil),
			wfNode("loop", NodeLoop, map[string]any{"items": "[" + strings.Join(items, ",") + "]"}),
			wfNode("b1", NodeTransform, map[string]any{"template": "1"}),
			wfNode("b2", NodeTransform, map[string]any{"template": "2"}),
			wfNode("b3", NodeTransform, map[string]any{"template": "3"}),
			wfNode("b4", NodeTransform, map[string]any{"template": "4"}),
			wfNode("out", NodeOutput, nil),
		},
		[]Edge{
			wfEdge("in", BranchNext, "loop"), wfEdge("loop", BranchBody, "b1"),
			wfEdge("b1", BranchNext, "b2"), wfEdge("b2", BranchNext, "b3"),
			wfEdge("b3", BranchNext, "b4"), wfEdge("loop", BranchNext, "out"),
		},
	)
	_, err := runWF(t, wf, Deps{}, RunEnv{UserID: "u1", Input: "x"})
	if err == nil || !strings.Contains(err.Error(), "adım sınırı") {
		t.Fatalf("adım sınırı hatası bekleniyordu, gelen: %v", err)
	}
}

// evalCondition tablosu: sayısal karşılaştırmada sayı olmayan değer HATA verir (sessizce false değil).
func TestEvalCondition(t *testing.T) {
	cases := []struct {
		op, l, r string
		want     bool
		wantErr  bool
	}{
		{"eq", "5", "5.0", true, false},
		{"eq", "5", "6", false, false},
		{"ne", "5", "6", true, false},
		{"contains", "Merhaba Dünya", "dünya", true, false},
		{"not_contains", "Merhaba", "dünya", true, false},
		{"starts_with", "Merhaba", "mer", true, false},
		{"ends_with", "Merhaba", "HABA", true, false},
		{"empty", "  ", "x", true, false},
		{"not_empty", "a", "", true, false},
		{"gt", "10", "9", true, false},
		{"gte", "10", "10", true, false},
		{"lt", "10", "9", false, false},
		{"lte", "10", "10", true, false},
		{"gt", "çok", "9", false, true},
		{"eq", "on", "on", true, false},
	}
	for _, c := range cases {
		got, err := evalCondition(c.op, c.l, c.r)
		if c.wantErr {
			if err == nil {
				t.Errorf("%s(%q,%q): hata bekleniyordu", c.op, c.l, c.r)
			}
			continue
		}
		if err != nil {
			t.Errorf("%s(%q,%q): %v", c.op, c.l, c.r, err)
			continue
		}
		if got != c.want {
			t.Errorf("%s(%q,%q) = %v, beklenen %v", c.op, c.l, c.r, got, c.want)
		}
	}
}
