package workflow

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"testing"
	"time"
)

// StoreContract her Store uygulamasının sağlaması gereken davranışı sınar.
// Üretim dosyasında durur (skill.RepositoryContract gibi): _test.go içindeyken dış paketler
// (mongostore) çağıramaz. newStore her alt test için TEMİZ bir depo vermelidir.
func StoreContract(t *testing.T, newStore func() Store) {
	t.Helper()
	ctx := context.Background()
	base := time.Now().UTC().Truncate(time.Millisecond)

	t.Run("workflow_roundtrip", func(t *testing.T) {
		s := newStore()
		want := contractWorkflow("wf_1", "u1", "Özetleyici", base)
		if err := s.Create(ctx, want); err != nil {
			t.Fatal(err)
		}
		got, err := s.Get(ctx, "u1", "wf_1")
		if err != nil {
			t.Fatal(err)
		}
		sameJSON(t, want, got)
		if err := s.Create(ctx, contractWorkflow("wf_1", "u1", "Başka", base)); err == nil {
			t.Error("aynı kimlikle ikinci kayıt eklenmemeliydi")
		}
	})

	t.Run("workflow_user_scoping", func(t *testing.T) {
		s := newStore()
		if err := s.Create(ctx, contractWorkflow("wf_1", "u1", "Benim", base)); err != nil {
			t.Fatal(err)
		}
		if _, err := s.Get(ctx, "u2", "wf_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının akışı Get: %v, ErrNotFound bekleniyordu", err)
		}
		if err := s.Update(ctx, contractWorkflow("wf_1", "u2", "Ele geçir", base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının akışı Update: %v, ErrNotFound bekleniyordu", err)
		}
		if err := s.Delete(ctx, "u2", "wf_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının akışı Delete: %v, ErrNotFound bekleniyordu", err)
		}
		if got, err := s.List(ctx, "u2", ListFilter{}, 0, 0); err != nil || len(got) != 0 {
			t.Errorf("başkasının listesi: %v %v", got, err)
		}
		// Sahibi hâlâ okuyabilmeli (reddedilen işlemler hiçbir şeyi bozmadı).
		got, err := s.Get(ctx, "u1", "wf_1")
		if err != nil || got.Name != "Benim" {
			t.Errorf("sahibin akışı bozulmuş: %v %v", got, err)
		}
	})

	t.Run("workflow_update_delete", func(t *testing.T) {
		s := newStore()
		if err := s.Create(ctx, contractWorkflow("wf_1", "u1", "Eski", base)); err != nil {
			t.Fatal(err)
		}
		upd := contractWorkflow("wf_1", "u1", "Yeni", base.Add(time.Minute))
		upd.Description = "güncel"
		upd.Nodes = upd.Nodes[:1]
		upd.Edges = []Edge{}
		if err := s.Update(ctx, upd); err != nil {
			t.Fatal(err)
		}
		got, err := s.Get(ctx, "u1", "wf_1")
		if err != nil {
			t.Fatal(err)
		}
		sameJSON(t, upd, got)

		if err := s.Update(ctx, contractWorkflow("yok", "u1", "x", base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan Update: %v", err)
		}
		if err := s.Delete(ctx, "u1", "wf_1"); err != nil {
			t.Fatal(err)
		}
		if _, err := s.Get(ctx, "u1", "wf_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("silinen akış okunuyor: %v", err)
		}
		if err := s.Delete(ctx, "u1", "wf_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("ikinci Delete: %v", err)
		}
	})

	t.Run("workflow_list_sorted_filtered_paged", func(t *testing.T) {
		s := newStore()
		if got, err := s.List(ctx, "u1", ListFilter{}, 0, 0); err != nil || got == nil || len(got) != 0 {
			t.Fatalf("boş depoda List: %v, %v (nil olmayan boş dilim bekleniyordu)", got, err)
		}
		mk := func(id, name, desc string, at time.Time) {
			w := contractWorkflow(id, "u1", name, at)
			w.Description = desc
			if err := s.Create(ctx, w); err != nil {
				t.Fatal(err)
			}
		}
		mk("1", "Eski", "rapor yazar", base)
		mk("2", "Orta", "PLANE açar", base.Add(time.Minute))
		mk("3", "Yeni", "özet çıkarır", base.Add(2*time.Minute))

		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
		}
		check := func(label string, got, want []string) {
			t.Helper()
			if !slices.Equal(got, want) {
				t.Errorf("%s: %v, beklenen %v", label, got, want)
			}
		}
		check("en son güncellenen önce", names(ListFilter{}, 0, 0), []string{"Yeni", "Orta", "Eski"})
		check("sorgu açıklamada, büyük/küçük harf duyarsız", names(ListFilter{Query: "plane"}, 0, 0), []string{"Orta"})
		check("sorgu adda", names(ListFilter{Query: "YEN"}, 0, 0), []string{"Yeni"})
		check("sayfalama", names(ListFilter{}, 1, 1), []string{"Orta"})
		check("offset sonu aşıyor", names(ListFilter{}, 5, 10), []string{})
		check("regex özel karakterleri düz metindir", names(ListFilter{Query: "a.c"}, 0, 0), []string{})
	})

	t.Run("workflow_no_aliasing", func(t *testing.T) {
		s := newStore()
		w := contractWorkflow("wf_1", "u1", "Alias", base)
		if err := s.Create(ctx, w); err != nil {
			t.Fatal(err)
		}
		w.Nodes[1].Config["prompt"] = "BOZUK"
		w.Nodes[1].Config["nested"].(map[string]any)["k"] = "BOZUK"
		got, err := s.Get(ctx, "u1", "wf_1")
		if err != nil {
			t.Fatal(err)
		}
		if got.Nodes[1].Config["prompt"] == "BOZUK" || got.Nodes[1].Config["nested"].(map[string]any)["k"] == "BOZUK" {
			t.Error("depo, çağıranın verisiyle bellek paylaşıyor")
		}
	})

	t.Run("execution_roundtrip_update_scoping", func(t *testing.T) {
		s := newStore()
		want := contractExecution("ex_1", "u1", "wf_1", RunRunning, base)
		if err := s.CreateExecution(ctx, want); err != nil {
			t.Fatal(err)
		}
		got, err := s.GetExecution(ctx, "u1", "ex_1")
		if err != nil {
			t.Fatal(err)
		}
		sameJSON(t, want, got)
		if err := s.CreateExecution(ctx, contractExecution("ex_1", "u1", "wf_1", RunRunning, base)); err == nil {
			t.Error("aynı yürütme kimliği ikinci kez eklenmemeliydi")
		}

		if _, err := s.GetExecution(ctx, "u2", "ex_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının yürütmesi: %v", err)
		}
		if err := s.UpdateExecution(ctx, contractExecution("ex_1", "u2", "wf_1", RunFailed, base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının yürütmesi Update: %v", err)
		}
		if err := s.UpdateExecution(ctx, contractExecution("yok", "u1", "wf_1", RunFailed, base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan yürütme Update: %v", err)
		}

		done := contractExecution("ex_1", "u1", "wf_1", RunSucceeded, base)
		fin := base.Add(time.Second)
		done.FinishedAt, done.LatencyMs, done.Output = &fin, 1000, "sonuç"
		if err := s.UpdateExecution(ctx, done); err != nil {
			t.Fatal(err)
		}
		got, err = s.GetExecution(ctx, "u1", "ex_1")
		if err != nil {
			t.Fatal(err)
		}
		sameJSON(t, done, got)
	})

	t.Run("execution_list_filter_paging", func(t *testing.T) {
		s := newStore()
		if got, err := s.ListExecutions(ctx, "u1", ExecutionFilter{}, 0, 0); err != nil || got == nil || len(got) != 0 {
			t.Fatalf("boş depoda ListExecutions: %v, %v", got, err)
		}
		add := func(id, wf string, st RunStatus, at time.Time) {
			if err := s.CreateExecution(ctx, contractExecution(id, "u1", wf, st, at)); err != nil {
				t.Fatal(err)
			}
		}
		add("e1", "wfA", RunSucceeded, base)
		add("e2", "wfA", RunFailed, base.Add(time.Minute))
		add("e3", "wfB", RunSucceeded, base.Add(2*time.Minute))
		if err := s.CreateExecution(ctx, contractExecution("e4", "u2", "wfA", RunSucceeded, base)); err != nil {
			t.Fatal(err)
		}

		ids := func(f ExecutionFilter, limit, offset int) []string {
			got, err := s.ListExecutions(ctx, "u1", f, limit, offset)
			if err != nil {
				t.Fatal(err)
			}
			out := make([]string, 0, len(got))
			for _, e := range got {
				out = append(out, e.ID)
			}
			return out
		}
		check := func(label string, got, want []string) {
			t.Helper()
			if !slices.Equal(got, want) {
				t.Errorf("%s: %v, beklenen %v", label, got, want)
			}
		}
		check("en yeni önce, yalnızca kendi kayıtları", ids(ExecutionFilter{}, 0, 0), []string{"e3", "e2", "e1"})
		check("akışa göre", ids(ExecutionFilter{WorkflowID: "wfA"}, 0, 0), []string{"e2", "e1"})
		check("duruma göre", ids(ExecutionFilter{Status: RunSucceeded}, 0, 0), []string{"e3", "e1"})
		check("sayfalama", ids(ExecutionFilter{}, 1, 1), []string{"e2"})
	})

	t.Run("execution_delete_by_workflow", func(t *testing.T) {
		s := newStore()
		for i, wf := range []string{"wfA", "wfA", "wfB"} {
			if err := s.CreateExecution(ctx, contractExecution(fmt.Sprintf("e%d", i), "u1", wf, RunSucceeded, base)); err != nil {
				t.Fatal(err)
			}
		}
		if err := s.CreateExecution(ctx, contractExecution("other", "u2", "wfA", RunSucceeded, base)); err != nil {
			t.Fatal(err)
		}
		n, err := s.DeleteExecutionsByWorkflow(ctx, "u1", "wfA")
		if err != nil || n != 2 {
			t.Fatalf("silinen: %d, %v (2 bekleniyordu)", n, err)
		}
		if _, err := s.GetExecution(ctx, "u1", "e2"); err != nil {
			t.Errorf("başka akışın yürütmesi silinmiş: %v", err)
		}
		if _, err := s.GetExecution(ctx, "u2", "other"); err != nil {
			t.Errorf("başka kullanıcının yürütmesi silinmiş: %v", err)
		}
	})

	t.Run("execution_fail_running", func(t *testing.T) {
		s := newStore()
		run := contractExecution("run", "u1", "wf", RunRunning, base)
		run.Steps = append(run.Steps, Step{Index: 2, NodeID: "llm", NodeType: NodeLLM, Status: StepRunning, StartedAt: base})
		if err := s.CreateExecution(ctx, run); err != nil {
			t.Fatal(err)
		}
		ok := contractExecution("ok", "u1", "wf", RunSucceeded, base)
		if err := s.CreateExecution(ctx, ok); err != nil {
			t.Fatal(err)
		}

		at := base.Add(time.Minute)
		n, err := s.FailRunning(ctx, "çöktü", at)
		if err != nil || n != 1 {
			t.Fatalf("FailRunning: %d, %v (1 bekleniyordu)", n, err)
		}
		got, err := s.GetExecution(ctx, "u1", "run")
		if err != nil {
			t.Fatal(err)
		}
		if got.Status != RunFailed || got.Error != "çöktü" || got.FinishedAt == nil || !got.FinishedAt.Equal(at) {
			t.Errorf("yetim yürütme kapanmamış: %+v", got)
		}
		for _, st := range got.Steps {
			if st.Status == StepRunning {
				t.Errorf("yarım adım kapanmamış: %+v", st)
			}
		}
		if got, _ := s.GetExecution(ctx, "u1", "ok"); got == nil || got.Status != RunSucceeded {
			t.Errorf("biten yürütme değişmemeliydi: %+v", got)
		}
		if n, err := s.FailRunning(ctx, "tekrar", at); err != nil || n != 0 {
			t.Errorf("ikinci FailRunning: %d, %v (0 bekleniyordu)", n, err)
		}
	})
}

// contractWorkflow input → llm → output zinciri olan dolu bir akış üretir. Sayılar float64'tür
// (BSON/JSON gidiş-dönüşünde tip kaybı olmasın); zaman damgaları milisaniyeye kırpılı UTC'dir.
func contractWorkflow(id, userID, name string, at time.Time) *Workflow {
	return &Workflow{
		ID: id, UserID: userID, Name: name, Description: "sözleşme testi",
		Nodes: []Node{
			{ID: "in", Type: NodeInput, Label: "Girdi", Position: Position{X: 10, Y: 20}},
			{ID: "llm1", Type: NodeLLM, Label: "Özetle", Position: Position{X: 200, Y: 20.5}, Config: map[string]any{
				"prompt":      "Özetle: {{input}}",
				"temperature": 0.25,
				"nested":      map[string]any{"k": "v", "list": []any{"a", 1.0}},
			}},
			{ID: "out", Type: NodeOutput, Position: Position{X: 400, Y: 20}},
		},
		Edges:     []Edge{{ID: "e1", From: "in", To: "llm1"}, {ID: "e2", From: "llm1", To: "out"}},
		CreatedAt: at, UpdatedAt: at,
	}
}

func contractExecution(id, userID, wfID string, st RunStatus, at time.Time) *Execution {
	fin := at.Add(time.Millisecond)
	return &Execution{
		ID: id, UserID: userID, WorkflowID: wfID, WorkflowName: "Akış", ConversationID: "conv_1",
		Input: "merhaba", Status: st, ToolCalls: 1, PromptTokens: 5, CompletionTokens: 7,
		Steps: []Step{{
			Index: 1, NodeID: "in", NodeType: NodeInput, Status: StepOK, Output: "merhaba",
			StartedAt: at, FinishedAt: &fin, DurationMs: 1,
		}},
		StartedAt: at,
	}
}

// sameJSON iki değeri JSON üzerinden karşılaştırır: bellek içi ve Mongo'nun iç temsilleri
// (ör. []any / bson.A) farklı olsa da anlamsal eşitlik doğrulanır.
func sameJSON(t *testing.T, want, got any) {
	t.Helper()
	w, err1 := json.Marshal(want)
	g, err2 := json.Marshal(got)
	if err1 != nil || err2 != nil {
		t.Fatalf("JSON üretilemedi: %v %v", err1, err2)
	}
	if string(w) != string(g) {
		t.Errorf("kayıt farklı:\n beklenen: %s\n gelen:    %s", w, g)
	}
}
