package api

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"testing"
	"time"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/workflow"
)

// decodeInto ham gövdeyi verilen yapıya çözer ( anonim JSON şeması için).
func decodeInto(t *testing.T, raw []byte, v any) {
	t.Helper()
	if err := json.Unmarshal(raw, v); err != nil {
		t.Fatalf("JSON çözümlenemedi: %v (gövde: %s)", err, raw)
	}
}

// workflowsServer yalnızca akış servisini bağlı bir API sunucusu kurar.
// LLM BAĞLI DEĞİLDİR: yeteneği olmayan akış "çalıştırılamaz" olarak bildirilir
// (HTTP 501) — sunucunun eksik alt sistemle de ayakta kaldığını gösterir.
func workflowsServer(t *testing.T) (*Server, *workflow.Service) {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	bus := event.NewMemoryBus(nil, 16)
	t.Cleanup(bus.Close)
	wfSvc := workflow.NewService(workflow.NewMemoryRepo(), workflow.Deps{}, workflow.Options{
		Logger: log, MaxConcurrent: 2, RunTimeout: 2 * time.Second,
	})
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = wfSvc.Close(ctx)
	})
	s := New(context.Background(), Config{
		Env: "test", Version: "test", DefaultUserID: "local", DefaultPageSize: 50, MaxPageSize: 200,
	}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
		Bus:           bus,
		Workflows:     wfSvc,
	}, log)
	return s, wfSvc
}

// runnableWorkflowBody yetenek gerektirmeyen doğrusal bir akıştır (input → transform → output).
func runnableWorkflowBody(name string) map[string]any {
	return map[string]any{
		"name": name,
		"nodes": []map[string]any{
			{"id": "in", "type": "input"},
			{"id": "t1", "type": "transform", "config": map[string]any{"template": "Merhaba: {{input}}"}},
			{"id": "out", "type": "output"},
		},
		"edges": []map[string]any{
			{"from": "in", "to": "t1"},
			{"from": "t1", "to": "out"},
		},
	}
}

func mustCreateWorkflow(t *testing.T, s *Server, body map[string]any) workflow.Workflow {
	t.Helper()
	resp, raw := do(t, s, http.MethodPost, "/api/workflows", body, nil)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /api/workflows = %d, beklenen 201 (gövde %s)", resp.StatusCode, raw)
	}
	return decode[workflowResponse](t, raw).Workflow
}

// Akış CRUD tam turu: oluştur → oku → güncelle → kopyala → sil → 404.
func TestWorkflowCRUDFlow(t *testing.T) {
	s, _ := workflowsServer(t)

	w := mustCreateWorkflow(t, s, runnableWorkflowBody("Özetleyici"))
	if w.ID == "" || w.UserID != "local" || w.CreatedAt.IsZero() {
		t.Fatalf("oluşturulan akış eksik: %+v", w)
	}
	if len(w.Nodes) != 3 || w.Edges[0].ID == "" {
		t.Errorf("node/kenarlar kaydedilmemiş: %+v", w)
	}

	resp, raw := do(t, s, http.MethodGet, "/api/workflows/"+w.ID, nil, nil)
	if resp.StatusCode != http.StatusOK || decode[workflowResponse](t, raw).Workflow.Name != "Özetleyici" {
		t.Fatalf("GET = %d %s", resp.StatusCode, raw)
	}

	upd := runnableWorkflowBody("Özetleyici v2")
	upd["description"] = "güncel"
	resp, raw = do(t, s, http.MethodPut, "/api/workflows/"+w.ID, upd, nil)
	got := decode[workflowResponse](t, raw).Workflow
	if resp.StatusCode != http.StatusOK || got.Name != "Özetleyici v2" || got.ID != w.ID ||
		!got.CreatedAt.Equal(w.CreatedAt) || got.Description != "güncel" {
		t.Fatalf("PUT = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/workflows/"+w.ID+"/duplicate", nil, nil)
	cp := decode[workflowResponse](t, raw).Workflow
	if resp.StatusCode != http.StatusCreated || cp.Name != "Özetleyici v2 (kopya)" || cp.ID == w.ID {
		t.Fatalf("duplicate = %d %s", resp.StatusCode, raw)
	}

	resp, _ = do(t, s, http.MethodDelete, "/api/workflows/"+w.ID, nil, nil)
	if resp.StatusCode != http.StatusNoContent {
		t.Errorf("DELETE = %d, beklenen 204", resp.StatusCode)
	}
	if resp, raw := do(t, s, http.MethodGet, "/api/workflows/"+w.ID, nil, nil); resp.StatusCode != http.StatusNotFound {
		t.Errorf("silinen akış = %d, beklenen 404 (gövde %s)", resp.StatusCode, raw)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/workflows/"+cp.ID, nil, nil); resp.StatusCode != http.StatusOK {
		t.Errorf("kopya da silinmiş: %d", resp.StatusCode)
	}
}

// Kaydetme esnek: yapısal olarak geçerli TASLAK (output'suz) kaydedilebilmeli.
func TestWorkflowDraftIsSavable(t *testing.T) {
	s, _ := workflowsServer(t)
	w := mustCreateWorkflow(t, s, map[string]any{
		"name":  "Taslak",
		"nodes": []map[string]any{{"id": "in", "type": "input"}},
	})
	if len(w.Nodes) != 1 {
		t.Errorf("taslak düğümleri kayboldu: %+v", w)
	}
	// Ama çalıştırılamaz: 409 + "kaydedilebilir ama çalıştırılamaz" ayrımı.
	resp, raw := do(t, s, http.MethodPost, "/api/workflows/"+w.ID+"/run", map[string]any{"input": "x"}, nil)
	if resp.StatusCode != http.StatusConflict {
		t.Fatalf("run = %d, beklenen 409 (gövde %s)", resp.StatusCode, raw)
	}
	if e := errBody(t, raw); e.Code != protocol.ErrCodeValidation || !strings.Contains(e.Message, "çalıştırılamaz") {
		t.Errorf("hata gövdesi ayrımı taşımalı: %+v", e)
	}
}

// Yapısal hata 400 + alan bazlı hatalar döner.
func TestWorkflowValidationErrorHasFields(t *testing.T) {
	s, _ := workflowsServer(t)
	resp, raw := do(t, s, http.MethodPost, "/api/workflows", runnableWorkflowBody(""), nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("POST = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}
	if e := errBody(t, raw); len(e.Fields) == 0 || e.Fields[0].Field != "name" {
		t.Errorf("alan hatası bekleniyordu: %+v", e)
	}
}

// LLM bağlı değilken llm node'lu akış 501 döner: sunucu hatası DEĞİL, yetenek eksikliği.
func TestWorkflowMissingCapabilityIs501(t *testing.T) {
	s, _ := workflowsServer(t)
	w := mustCreateWorkflow(t, s, map[string]any{
		"name": "LLM akışı",
		"nodes": []map[string]any{
			{"id": "in", "type": "input"},
			{"id": "llm1", "type": "llm", "config": map[string]any{"prompt": "yaz: {{input}}"}},
			{"id": "out", "type": "output"},
		},
		"edges": []map[string]any{{"from": "in", "to": "llm1"}, {"from": "llm1", "to": "out"}},
	})
	// Kayıt ESNEKTİR: yetenek eksikliği kaydı engellemez.
	resp, raw := do(t, s, http.MethodPost, "/api/workflows/"+w.ID+"/run", map[string]any{"input": "x"}, nil)
	if resp.StatusCode != http.StatusNotImplemented {
		t.Fatalf("run = %d, beklenen 501 (gövde %s)", resp.StatusCode, raw)
	}
	if e := errBody(t, raw); !strings.Contains(e.Message, "llm") {
		t.Errorf("eksik tür mesajda belirtilmeli: %+v", e)
	}
}

// Yürütme: 202 + "running" kaydı, ardından GET ile kalıcı sonuç izlenir.
func TestWorkflowRunLifecycle(t *testing.T) {
	s, _ := workflowsServer(t)
	w := mustCreateWorkflow(t, s, runnableWorkflowBody("Çalıştır"))

	resp, raw := do(t, s, http.MethodPost, "/api/workflows/"+w.ID+"/run",
		map[string]any{"input": "merhaba", "conversationId": "conv_1"}, nil)
	if resp.StatusCode != http.StatusAccepted {
		t.Fatalf("run = %d, beklenen 202 (gövde %s)", resp.StatusCode, raw)
	}
	started := decode[executionResponse](t, raw).Execution
	if started.ID == "" || started.Status != workflow.RunRunning {
		t.Fatalf("dönen kayıt running olmalı: %+v", started)
	}
	if started.WorkflowID != w.ID || started.ConversationID != "conv_1" {
		t.Errorf("yürütme başlığı eksik: %+v", started)
	}

	// Yürütme arka planda bitene kadar bekle (yoksa test titremeli).
	var final workflow.Execution
	deadline := time.Now().Add(5 * time.Second)
	for {
		resp, raw = do(t, s, http.MethodGet, "/api/workflows/runs/"+started.ID, nil, nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("GET run = %d %s", resp.StatusCode, raw)
		}
		final = decode[executionResponse](t, raw).Execution
		if final.Status.Terminal() {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("yürütme bitmedi: %+v", final)
		}
		time.Sleep(5 * time.Millisecond)
	}
	if final.Status != workflow.RunSucceeded || final.Output != "Merhaba: merhaba" {
		t.Fatalf("sonuç: %s / %q (hata: %s)", final.Status, final.Output, final.Error)
	}
	if len(final.Steps) != 3 {
		t.Errorf("adım sayısı: %d", len(final.Steps))
	}

	// Geçmiş listesi akışa göre daraltılabilir.
	resp, raw = do(t, s, http.MethodGet, "/api/workflows/runs?workflowId="+w.ID, nil, nil)
	list := decode[executionsResponse](t, raw)
	if resp.StatusCode != http.StatusOK || list.Count != 1 || list.Items[0].ID != started.ID {
		t.Fatalf("runs listesi = %d %s", resp.StatusCode, raw)
	}

	// Bitti: iptal 409.
	resp, raw = do(t, s, http.MethodPost, "/api/workflows/runs/"+started.ID+"/cancel", nil, nil)
	if resp.StatusCode != http.StatusConflict {
		t.Errorf("bitmiş yürütmeyi iptal = %d, beklenen 409 (gövde %s)", resp.StatusCode, raw)
	}
}

// Sahiplik: akışlar KULLANICIYA AITTİR. X-Fiko-User başlığıyla ayrı kullanıcı
// başkasının akışını GÖREMEZ (404) ve kendi listesinde göremez — varlık bile sızmaz.
func TestWorkflowOwnerScoping(t *testing.T) {
	s, _ := workflowsServer(t)
	mine := mustCreateWorkflow(t, s, runnableWorkflowBody("Benim akışım"))
	other := map[string]string{UserHeader: "başka-kullanıcı"}

	resp, raw := do(t, s, http.MethodGet, "/api/workflows/"+mine.ID, nil, other)
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("başkasının akışı = %d, beklenen 404 (gövde %s)", resp.StatusCode, raw)
	}
	if resp, raw := do(t, s, http.MethodPut, "/api/workflows/"+mine.ID, runnableWorkflowBody("Çalınma"), other); resp.StatusCode != http.StatusNotFound {
		t.Errorf("başkasının akışını güncelleme = %d (gövde %s)", resp.StatusCode, raw)
	}
	if resp, raw := do(t, s, http.MethodDelete, "/api/workflows/"+mine.ID, nil, other); resp.StatusCode != http.StatusNotFound {
		t.Errorf("başkasının akışını silme = %d (gövde %s)", resp.StatusCode, raw)
	}
	resp, raw = do(t, s, http.MethodGet, "/api/workflows", nil, other)
	if list := decode[workflowsResponse](t, raw); resp.StatusCode != http.StatusOK || list.Count != 0 {
		t.Errorf("yabancının listesi boş olmalı: %d %s", resp.StatusCode, raw)
	}

	// Sahibi hâlâ görebilmeli (reddedilen işlemler bir şeyi bozmadı).
	if resp, _ := do(t, s, http.MethodGet, "/api/workflows/"+mine.ID, nil, nil); resp.StatusCode != http.StatusOK {
		t.Errorf("sahibi akışını göremiyor: %d", resp.StatusCode)
	}
	// Sahiplik başlığı olmadan varsayılan kullanıcı ("local") devreye girer.
	if resp, _ := do(t, s, http.MethodGet, "/api/workflows", nil, nil); resp.StatusCode != http.StatusOK {
		t.Errorf("listeleme = %d", resp.StatusCode)
	}
}

// validate: geçersizlik HATA değil SONUÇTUR (her zaman 200).
func TestWorkflowValidateReturnsReport(t *testing.T) {
	s, _ := workflowsServer(t)

	resp, raw := do(t, s, http.MethodPost, "/api/workflows/validate", runnableWorkflowBody("Geçerli"), nil)
	got := decode[workflowValidateResponse](t, raw)
	if resp.StatusCode != http.StatusOK || !got.Valid || got.Errors == nil {
		t.Fatalf("geçerli akış = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/workflows/validate", map[string]any{"name": ""}, nil)
	got = decode[workflowValidateResponse](t, raw)
	if resp.StatusCode != http.StatusOK || got.Valid || len(got.Errors) == 0 {
		t.Errorf("boş ad geçersiz bildirilmeli: %d %s", resp.StatusCode, raw)
	}

	// Taslak: kaydedilebilir (valid) ama çalıştırılamaz (runnable=false).
	resp, raw = do(t, s, http.MethodPost, "/api/workflows/validate", map[string]any{
		"name":  "Taslak",
		"nodes": []map[string]any{{"id": "in", "type": "input"}},
	}, nil)
	got = decode[workflowValidateResponse](t, raw)
	if resp.StatusCode != http.StatusOK || !got.Valid || got.Runnable || len(got.RunErrors) == 0 {
		t.Errorf("taslak raporu = %d %s", resp.StatusCode, raw)
	}
}

// meta: palet, operatörler ve sunucu sınırları (yetenekler bağlı değil).
func TestWorkflowMeta(t *testing.T) {
	s, _ := workflowsServer(t)
	resp, raw := do(t, s, http.MethodGet, "/api/workflows/meta", nil, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("meta = %d %s", resp.StatusCode, raw)
	}
	var meta struct {
		Available map[string]bool `json:"available"`
		Ops       []string        `json:"ops"`
		Reserved  []string        `json:"reserved"`
		Limits    struct {
			MaxNodes int `json:"maxNodes"`
			MaxSteps int `json:"maxSteps"`
		} `json:"limits"`
	}
	decodeInto(t, raw, &meta)
	// Yeteneği gerekmeyen türler çalıştırılabilir; LLM bağlı olmadığı için llm=false.
	if !meta.Available["input"] || !meta.Available["transform"] || !meta.Available["output"] {
		t.Errorf("temel türler çalıştırılabilir olmalı: %+v", meta.Available)
	}
	if meta.Available["llm"] {
		t.Error("LLM bağlı değilken çalıştırılabilir görünmemeli")
	}
	if len(meta.Ops) == 0 || len(meta.Reserved) == 0 {
		t.Errorf("operatör/ayrılmış liste boş: %+v", meta)
	}
	if meta.Limits.MaxNodes == 0 || meta.Limits.MaxSteps == 0 {
		t.Errorf("sınırlar bildirilmeli: %+v", meta.Limits)
	}
}

// Servis bağlı değilse tüm uçlar 503 döner (yapılandırılmamış sistem sessizce 404 olmamalı).
func TestWorkflowsUnavailableWhenNotWired(t *testing.T) {
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	s := New(context.Background(), Config{Env: "test", Version: "test"},
		Deps{Conversations: conversation.NewService(conversation.NewMemoryRepo())}, log)
	for _, tc := range []struct{ method, path string }{
		{http.MethodGet, "/api/workflows"},
		{http.MethodPost, "/api/workflows"},
		{http.MethodGet, "/api/workflows/meta"},
		{http.MethodGet, "/api/workflows/runs"},
	} {
		resp, raw := do(t, s, tc.method, tc.path, map[string]any{"name": "x"}, nil)
		if resp.StatusCode != http.StatusServiceUnavailable {
			t.Errorf("%s %s = %d, beklenen 503 (gövde %s)", tc.method, tc.path, resp.StatusCode, raw)
		}
	}
}
