package workflow

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"strings"
	"sync"
	"testing"
	"time"
)

// newTestService sessiz loglayan bir servis kurar; testler gürültü almamalı.
func newTestService(t *testing.T, deps Deps, opts Options) *Service {
	t.Helper()
	if opts.Logger == nil {
		opts.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
	}
	s := NewService(NewMemoryRepo(), deps, opts)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.Close(ctx)
	})
	return s
}

// mustCreate akışı kaydeder (geçerli girdi varsayar).
func mustCreate(t *testing.T, s *Service, userID string, w Workflow) *Workflow {
	t.Helper()
	got, err := s.Create(context.Background(), userID, w)
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	return got
}

// waitTerminal yürütmenin bitmesini bekler (sabit süreyle: sonsuz döngüye düşmemek için).
func waitTerminal(t *testing.T, s *Service, userID, execID string) *Execution {
	t.Helper()
	deadline := time.Now().Add(5 * time.Second)
	for {
		e, err := s.GetExecution(context.Background(), userID, execID)
		if err != nil {
			t.Fatalf("yürütme okunamadı: %v", err)
		}
		if e.Status.Terminal() {
			return e
		}
		if time.Now().After(deadline) {
			t.Fatalf("yürütme bitmedi: %+v", e)
		}
		time.Sleep(2 * time.Millisecond)
	}
}

// Kimlik, sahip ve zaman damgaları sunucuya aittir: istemci bunları gönderemez.
func TestService_CreateOwnsServerFields(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	in := runnableWorkflow()
	in.ID, in.UserID = "sahte_kimlik", "başkası"
	in.CreatedAt, in.UpdatedAt = testBase, testBase

	got := mustCreate(t, s, "u1", in)
	if got.UserID != "u1" {
		t.Errorf("sahip istemciden gelmemeli: %q", got.UserID)
	}
	if got.ID == "sahte_kimlik" || !strings.HasPrefix(got.ID, "wf_") {
		t.Errorf("sunucu kimliği üretmeliydi: %q", got.ID)
	}
	if got.CreatedAt.Equal(testBase) || !got.CreatedAt.Equal(got.UpdatedAt) {
		t.Errorf("zaman damgaları: created=%v updated=%v", got.CreatedAt, got.UpdatedAt)
	}
	if got.Nodes[0].ID != "in" {
		t.Errorf("node kimlikleri normalize edilmedi: %q", got.Nodes[0].ID)
	}
}

func TestService_CreateRejectsInvalid(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	w := runnableWorkflow()
	w.Name = ""
	_, err := s.Create(context.Background(), "u1", w)
	var ve *ValidationError
	if !errors.As(err, &ve) {
		t.Fatalf("ValidationError bekleniyordu: %v", err)
	}
	if len(ve.Errors) == 0 || ve.Errors[0].Field != "name" {
		t.Errorf("alan hatası bekleniyordu: %+v", ve.Errors)
	}
	if !strings.Contains(ve.Error(), "name") {
		t.Errorf("hata mesajı alanı söylemiyor: %q", ve.Error())
	}
}

// Update kimlik/sahip/oluşturma zamanını korur; başkasının akışı güncellenemez.
func TestService_UpdateKeepsIdentity(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	created := mustCreate(t, s, "u1", runnableWorkflow())

	in := created.Clone()
	in.Name = "Yeni ad"
	in.ID, in.UserID = "başka", "başkası"
	got, err := s.Update(ctx, "u1", created.ID, in)
	if err != nil {
		t.Fatal(err)
	}
	if got.ID != created.ID || got.UserID != "u1" || !got.CreatedAt.Equal(created.CreatedAt) {
		t.Errorf("sunucu alanları korunmadı: %+v", got)
	}
	if got.Name != "Yeni ad" {
		t.Errorf("ad güncellenmedi: %q", got.Name)
	}
	if _, err := s.Update(ctx, "u2", created.ID, in); !errors.Is(err, ErrNotFound) {
		t.Errorf("başkasının akışı: %v, ErrNotFound bekleniyordu", err)
	}
}

// Duplicate kopyalar, Delete akışı siler (kopya durmalı).
func TestService_DuplicateAndDelete(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	created := mustCreate(t, s, "u1", runnableWorkflow())

	cp, err := s.Duplicate(ctx, "u1", created.ID)
	if err != nil {
		t.Fatal(err)
	}
	if cp.Name != created.Name+" (kopya)" || cp.ID == created.ID {
		t.Errorf("kopya: %+v", cp)
	}
	if len(cp.Nodes) != len(created.Nodes) {
		t.Errorf("kopya node'ları taşınmadı: %d", len(cp.Nodes))
	}
	if err := s.Delete(ctx, "u1", created.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := s.Get(ctx, "u1", created.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("silinen akış okunuyor: %v", err)
	}
	if err := s.Delete(ctx, "u1", created.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("ikinci silme: %v", err)
	}
	if _, err := s.Get(ctx, "u1", cp.ID); err != nil {
		t.Errorf("kopya da silinmiş: %v", err)
	}
}

// Start: kayıt "running" döner, yürütme arka planda biter ve sonuç KALICI yazılır.
func TestService_StartRunsInBackground(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())

	started, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "merhaba"})
	if err != nil {
		t.Fatal(err)
	}
	if started.Status != RunRunning || started.Steps == nil {
		t.Errorf("dönen kayıt running olmalı: %+v", started)
	}
	final := waitTerminal(t, s, "u1", started.ID)
	if final.Status != RunSucceeded {
		t.Fatalf("durum: %s, hata: %s", final.Status, final.Error)
	}
	if final.Output != "Merhaba: merhaba" {
		t.Errorf("çıktı: %q", final.Output)
	}
	if final.FinishedAt == nil || final.LatencyMs < 0 {
		t.Errorf("bitiş alanları eksik: %+v", final)
	}
	if len(final.Steps) != 3 {
		t.Fatalf("adım sayısı: %d", len(final.Steps))
	}
	for _, st := range final.Steps {
		if st.Status != StepOK || st.FinishedAt == nil {
			t.Errorf("adım kapanmamış: %+v", st)
		}
	}
	if final.WorkflowID != wf.ID || final.WorkflowName != wf.Name || final.Input != "merhaba" {
		t.Errorf("yürütme başlığı eksik: %+v", final)
	}
}

// Start, çalıştırılamayan akışı başlatmaz; "kaydedilebilir ama çalıştırılamaz" ayrımı korunur.
func TestService_StartRejectsNotRunnable(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	draft := mustCreate(t, s, "u1", draftWorkflow())
	_, err := s.Start(ctx, "u1", draft.ID, StartInput{Input: "x"})
	var ve *ValidationError
	if !errors.As(err, &ve) || !ve.NotRunnable {
		t.Fatalf("NotRunnable bekleniyordu: %v", err)
	}
}

// Start, bağlı olmayan yetenek gerektiren akışı *CapabilityError ile reddeder.
func TestService_StartRejectsMissingCapability(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())
	_, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	var ce *CapabilityError
	if !errors.As(err, &ce) || len(ce.Types) != 1 || ce.Types[0] != NodeLLM {
		t.Fatalf("CapabilityError bekleniyordu: %v", err)
	}
}

// Start girdi sınırını aşan isteği reddeder (kayıt şişmesi engellenir).
func TestService_StartRejectsOversizedInput(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())
	_, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: strings.Repeat("x", MaxInputRunes+1)})
	var ve *ValidationError
	if !errors.As(err, &ve) || ve.NotRunnable {
		t.Fatalf("girdi sınırı hatası bekleniyordu: %v", err)
	}
	if len(ve.Errors) == 0 || ve.Errors[0].Field != "input" {
		t.Errorf("input alanı işaretlenmeliydi: %+v", ve.Errors)
	}
}

// Start yalnızca sahibin akışını çalıştırır.
func TestService_StartScopedToOwner(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())
	if _, err := s.Start(ctx, "u2", wf.ID, StartInput{Input: "x"}); !errors.Is(err, ErrNotFound) {
		t.Errorf("başkasının akışı başlatıldı: %v", err)
	}
}

// Cancel: süren yürütme iptal edilir, kayıt "cancelled" olarak kapanır.
func TestService_CancelRunningExecution(t *testing.T) {
	started := make(chan struct{})
	llm := &fakeLLM{fn: func(ctx context.Context, _ LLMRequest) (LLMResult, error) {
		close(started)
		<-ctx.Done() // iptal bekleniyor
		return LLMResult{}, ctx.Err()
	}}
	s := newTestService(t, Deps{LLM: llm}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())

	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	<-started
	if _, err := s.Cancel(ctx, "u1", ex.ID); err != nil {
		t.Fatal(err)
	}
	final := waitTerminal(t, s, "u1", ex.ID)
	if final.Status != RunCancelled {
		t.Fatalf("durum: %s (cancelled bekleniyordu), hata: %s", final.Status, final.Error)
	}
	if !strings.Contains(final.Error, "iptal") {
		t.Errorf("iptal nedeni kaydedilmedi: %q", final.Error)
	}
	for _, st := range final.Steps {
		if st.Status == StepRunning {
			t.Error(`"çalışıyor" görünen adım kalmamalı`)
		}
	}
	// Bitmiş yürütme tekrar iptal EDİLEMEZ.
	if _, err := s.Cancel(ctx, "u1", ex.ID); !errors.Is(err, ErrAlreadyFinished) {
		t.Errorf("bitmiş yürütmenin iptali: %v, ErrAlreadyFinished bekleniyordu", err)
	}
}

// Cancel: başkasının yürütmesi görünmez, olmayan da ErrNotFound döner.
func TestService_CancelScopedToOwner(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())
	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	waitTerminal(t, s, "u1", ex.ID)
	if _, err := s.Cancel(ctx, "u2", ex.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("başkasının yürütmesi: %v", err)
	}
	if _, err := s.Cancel(ctx, "u1", "yok"); !errors.Is(err, ErrNotFound) {
		t.Errorf("olmayan yürütme: %v", err)
	}
}

// Cancel: bu süreçte çalışmayan "running" kayıt YETİM kabul edilir ve kayıtta kapatılır
// (arayüzde sonsuza dek "çalışıyor" görünmesin).
func TestService_CancelOrphanRecord(t *testing.T) {
	store := NewMemoryRepo()
	s := NewService(store, Deps{}, Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.Close(ctx)
	})
	orphan := &Execution{
		ID: "exec_orphan", UserID: "u1", WorkflowID: "wf", WorkflowName: "Akış",
		Status: RunRunning, StartedAt: time.Now().UTC().Add(-time.Minute), Steps: []Step{},
	}
	if err := store.CreateExecution(context.Background(), orphan); err != nil {
		t.Fatal(err)
	}
	got, err := s.Cancel(context.Background(), "u1", orphan.ID)
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != RunCancelled || !strings.Contains(got.Error, "yetim") {
		t.Errorf("yetim kayıt kapatılmadı: %+v", got)
	}
	saved, err := s.GetExecution(context.Background(), "u1", orphan.ID)
	if err != nil || saved.Status != RunCancelled || saved.FinishedAt == nil {
		t.Errorf("yetim kayıt kaydedilmedi: %+v %v", saved, err)
	}
}

// Eşzamanlılık sınırı: sınır doluyken Start ErrBusy döner, yürütme bitince yer açılır.
func TestService_MaxConcurrentBusy(t *testing.T) {
	release := make(chan struct{})
	started := make(chan struct{})
	llm := &fakeLLM{fn: func(context.Context, LLMRequest) (LLMResult, error) {
		started <- struct{}{}
		<-release
		return LLMResult{Text: "tamam"}, nil
	}}
	s := newTestService(t, Deps{LLM: llm}, Options{MaxConcurrent: 1})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())

	first, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	<-started
	if _, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "y"}); !errors.Is(err, ErrBusy) {
		t.Errorf("sınırdayken Start: %v, ErrBusy bekleniyordu", err)
	}
	close(release)
	waitTerminal(t, s, "u1", first.ID)

	// Yer açıldı: yeni yürütme başlamalı (release kapalı olduğu için hemen biter).
	if _, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "z"}); err != nil {
		t.Errorf("sınır boşaldıktan sonra Start reddedildi: %v", err)
	}
}

// Zaman aşımı: RunTimeout dolunca yürütme "failed" olur ve neden yazılır.
func TestService_RunTimeout(t *testing.T) {
	llm := &fakeLLM{fn: func(ctx context.Context, _ LLMRequest) (LLMResult, error) {
		<-ctx.Done()
		return LLMResult{}, ctx.Err()
	}}
	s := newTestService(t, Deps{LLM: llm}, Options{RunTimeout: 20 * time.Millisecond})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())

	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	final := waitTerminal(t, s, "u1", ex.ID)
	if final.Status != RunFailed {
		t.Fatalf("durum: %s", final.Status)
	}
	if !strings.Contains(final.Error, "zaman aşımı") {
		t.Errorf("zaman aşımı nedeni yok: %q", final.Error)
	}
}

// Panik kurtarma: bir node'un beklenmeyen paniği sunucuyu düşürmez, yürütme başarısız olur.
func TestService_PanicInNodeBecomesFailure(t *testing.T) {
	llm := &fakeLLM{fn: func(context.Context, LLMRequest) (LLMResult, error) {
		panic("beklenmeyen")
	}}
	s := newTestService(t, Deps{LLM: llm}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())

	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	final := waitTerminal(t, s, "u1", ex.ID)
	if final.Status != RunFailed || !strings.Contains(final.Error, "panic") {
		t.Errorf("panik yürütmeye sızmamalı: %+v", final)
	}
	for _, st := range final.Steps {
		if st.Status == StepRunning {
			t.Error(`panik sonrası "çalışıyor" adım kalmamalı`)
		}
	}
}

// Close: süren yürütmeler iptal edilir, yeni yürütme reddedilir (ErrClosed).
func TestService_CloseCancelsAndRejectsNew(t *testing.T) {
	started := make(chan struct{})
	llm := &fakeLLM{fn: func(ctx context.Context, _ LLMRequest) (LLMResult, error) {
		close(started)
		<-ctx.Done()
		return LLMResult{}, ctx.Err()
	}}
	s := NewService(NewMemoryRepo(), Deps{LLM: llm}, Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())

	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	<-started

	closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.Close(closeCtx); err != nil {
		t.Fatalf("kapanış: %v", err)
	}
	saved, err := s.GetExecution(ctx, "u1", ex.ID)
	if err != nil {
		t.Fatal(err)
	}
	if saved.Status != RunCancelled {
		t.Errorf("kapanışta süren yürütme iptal edilmeli: %s", saved.Status)
	}
	if _, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "y"}); !errors.Is(err, ErrClosed) {
		t.Errorf("kapanıştan sonra Start: %v, ErrClosed bekleniyordu", err)
	}
}

// Recover: açılışta "running" kalmış yürütmeler kapatılır (sunucu çökmüş olabilir).
func TestService_RecoverClosesOrphans(t *testing.T) {
	store := NewMemoryRepo()
	s := NewService(store, Deps{}, Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.Close(ctx)
	})
	ctx := context.Background()
	orphan := &Execution{
		ID: "exec_orphan", UserID: "u1", WorkflowID: "wf",
		Status: RunRunning, StartedAt: time.Now().UTC().Add(-time.Hour),
		Steps: []Step{{Index: 1, NodeID: "in", Status: StepRunning, StartedAt: time.Now().UTC().Add(-time.Hour)}},
	}
	if err := store.CreateExecution(ctx, orphan); err != nil {
		t.Fatal(err)
	}
	n, err := s.Recover(ctx)
	if err != nil || n != 1 {
		t.Fatalf("Recover: %d, %v (1 bekleniyordu)", n, err)
	}
	got, err := s.GetExecution(ctx, "u1", orphan.ID)
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != RunFailed || !strings.Contains(got.Error, "yeniden başlatıldı") {
		t.Errorf("yetim yürütme kapanmadı: %+v", got)
	}
	for _, st := range got.Steps {
		if st.Status == StepRunning {
			t.Error("yarım adım kalmamalı")
		}
	}
	if n, err := s.Recover(ctx); err != nil || n != 0 {
		t.Errorf("ikinci Recover: %d, %v", n, err)
	}
}

// OnFinish her biten yürütme için çağrılır (denetim kaydı).
func TestService_OnFinishCalled(t *testing.T) {
	var mu sync.Mutex
	var finished []Execution
	s := newTestService(t, Deps{}, Options{OnFinish: func(e Execution) {
		mu.Lock()
		defer mu.Unlock()
		finished = append(finished, e)
	}})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())
	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	waitTerminal(t, s, "u1", ex.ID)

	mu.Lock()
	defer mu.Unlock()
	if len(finished) != 1 || finished[0].ID != ex.ID || finished[0].Status != RunSucceeded {
		t.Errorf("OnFinish: %+v", finished)
	}
}

// Delete: akış silinince yürütme geçmişi de silinir.
func TestService_DeleteRemovesExecutions(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", runnableWorkflow())
	ex, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"})
	if err != nil {
		t.Fatal(err)
	}
	waitTerminal(t, s, "u1", ex.ID)

	if err := s.Delete(ctx, "u1", wf.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := s.GetExecution(ctx, "u1", ex.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("yürütme geçmişi silinmedi: %v", err)
	}
	list, err := s.ListExecutions(ctx, "u1", ExecutionFilter{}, 0, 0)
	if err != nil || len(list) != 0 {
		t.Errorf("liste: %v %v", list, err)
	}
}

// Delete: süren yürütmeyi iptal eder (akış ortadan kalkınca yürütme havada kalmamalı).
func TestService_DeleteCancelsRunning(t *testing.T) {
	started := make(chan struct{})
	llm := &fakeLLM{fn: func(ctx context.Context, _ LLMRequest) (LLMResult, error) {
		close(started)
		<-ctx.Done()
		return LLMResult{}, ctx.Err()
	}}
	s := newTestService(t, Deps{LLM: llm}, Options{})
	ctx := context.Background()
	wf := mustCreate(t, s, "u1", llmWorkflow())
	if _, err := s.Start(ctx, "u1", wf.ID, StartInput{Input: "x"}); err != nil {
		t.Fatal(err)
	}
	<-started
	if err := s.Delete(ctx, "u1", wf.ID); err != nil {
		t.Fatal(err)
	}
	closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.Close(closeCtx); err != nil {
		t.Errorf("süren yürütme kapanışta bitmedi: %v", err)
	}
}

// Inspect: geçersizlik HATA değil SONUÇTUR (arayüz canlı doğrulama yapar).
func TestService_Inspect(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	t.Run("geçerli akış çalıştırılabilir", func(t *testing.T) {
		got := s.Inspect(runnableWorkflow())
		if !got.Runnable || len(got.Errors) > 0 || len(got.RunErrors) > 0 {
			t.Errorf("rapor: %+v", got)
		}
	})
	t.Run("taslak kaydedilebilir ama çalıştırılamaz", func(t *testing.T) {
		got := s.Inspect(draftWorkflow())
		if len(got.Errors) != 0 {
			t.Errorf("taslak yapısal olarak geçerli olmalı: %+v", got.Errors)
		}
		if got.Runnable || len(got.RunErrors) == 0 {
			t.Errorf("output'suz akış çalıştırılamaz olmalı: %+v", got)
		}
	})
	t.Run("yapısal hata kaydı da engeller", func(t *testing.T) {
		w := runnableWorkflow()
		w.Name = ""
		got := s.Inspect(w)
		if len(got.Errors) == 0 || got.Runnable {
			t.Errorf("boş ad raporlanmalıydı: %+v", got)
		}
	})
	t.Run("eksik yetenek çalıştırmayı engeller", func(t *testing.T) {
		got := s.Inspect(llmWorkflow())
		if got.Runnable || len(got.RunErrors) == 0 {
			t.Errorf("LLM bağlı değilken çalıştırılabilir görünmemeli: %+v", got)
		}
		if !strings.Contains(got.RunErrors[len(got.RunErrors)-1].Message, "llm") {
			t.Errorf("eksik tür belirtilmeli: %+v", got.RunErrors)
		}
	})
}

// Meta: sunucu yeteneklerini ve sınırlarını arayüze bildirir.
func TestService_Meta(t *testing.T) {
	s := newTestService(t, Deps{LLM: &fakeLLM{}}, Options{MaxConcurrent: 7, RunTimeout: 90 * time.Second})
	meta := s.Meta()
	if !meta.Available[NodeLLM] {
		t.Error("bağlı LLM yeteneği bildirilmeli")
	}
	for _, nt := range []NodeType{NodeTool, NodeRAG, NodeMemory, NodeSkill} {
		if meta.Available[nt] {
			t.Errorf("%s bağlı değilken çalıştırılabilir görünmemeli", nt)
		}
	}
	// Yetenek gerektirmeyen türler her zaman çalıştırılabilir.
	for _, nt := range []NodeType{NodeInput, NodeCondition, NodeLoop, NodeTransform, NodeOutput} {
		if !meta.Available[nt] {
			t.Errorf("%s her zaman çalıştırılabilir olmalı", nt)
		}
	}
	if len(meta.Ops) == 0 || len(meta.Reserved) == 0 {
		t.Errorf("operatör/ayrılmış liste boş: %+v", meta)
	}
	if meta.Limits.MaxNodes != MaxNodes || meta.Limits.MaxSteps != MaxSteps ||
		meta.Limits.MaxInputRunes != MaxInputRunes || meta.Limits.MaxLoopIterations != MaxLoopIterations {
		t.Errorf("sınırlar yanlış: %+v", meta.Limits)
	}
	if meta.Limits.RunTimeoutSeconds != 90 || meta.Limits.MaxConcurrent != 7 {
		t.Errorf("servo ayarları yansıtılmadı: %+v", meta.Limits)
	}
}

// List: sorgu ve sayfalama servisten de geçer.
func TestService_ListFiltersAndPages(t *testing.T) {
	s := newTestService(t, Deps{}, Options{})
	ctx := context.Background()
	for _, name := range []string{"Rapor yazarı", "Özet çıkarır", "PLANE açar"} {
		w := runnableWorkflow()
		w.Name = name
		mustCreate(t, s, "u1", w)
	}
	names := func(f ListFilter, limit, offset int) []string {
		got, err := s.List(ctx, "u1", f, limit, offset)
		if err != nil {
			t.Fatal(err)
		}
		out := make([]string, 0, len(got))
		for _, w := range got {
			out = append(out, w.Name)
		}
		return out
	}
	if got := names(ListFilter{Query: "plane"}, 0, 0); len(got) != 1 || got[0] != "PLANE açar" {
		t.Errorf("sorgu: %v", got)
	}
	if got := names(ListFilter{}, 1, 1); len(got) != 1 {
		t.Errorf("sayfalama: %v", got)
	}
}
