package workflow

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
)

// fixtureEcho returns the input map verbatim. Useful for testing
// runner mechanics without external dependencies.
func fixtureEcho(_ Context, input map[string]any) (map[string]any, error) {
	out := make(map[string]any, len(input))
	for k, v := range input {
		out[k] = v
	}
	return out, nil
}

// fixtureFail always errors. Useful for testing skip-on-failed-dep.
func fixtureFail(_ Context, _ map[string]any) (map[string]any, error) {
	return nil, fmt.Errorf("fixture: intentional failure")
}

// fixtureUpper returns {"upper": strings.ToUpper(input["prompt"])}.
func fixtureUpper(_ Context, input map[string]any) (map[string]any, error) {
	prompt, _ := input["prompt"].(string)
	return map[string]any{"upper": strings.ToUpper(prompt)}, nil
}

func newTestRunner() *Runner {
	r := NewRunner()
	r.RegisterMode("fixture.echo", fixtureEcho)
	r.RegisterMode("fixture.fail", fixtureFail)
	r.RegisterMode("fixture.upper", fixtureUpper)
	return r
}

func TestValidate_RequiresName(t *testing.T) {
	w := Workflow{Name: "", Nodes: []Node{{ID: "a", Mode: "fixture.echo"}}}
	if err := w.Validate(); err == nil {
		t.Error("empty name should fail validation")
	}
}

func TestValidate_RequiresNodes(t *testing.T) {
	w := Workflow{Name: "x"}
	if err := w.Validate(); err == nil {
		t.Error("empty nodes should fail validation")
	}
}

func TestValidate_DuplicateNodeID(t *testing.T) {
	w := Workflow{Name: "x", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo"},
		{ID: "a", Mode: "fixture.echo"},
	}}
	if err := w.Validate(); !errors.Is(err, ErrDuplicateNodeID) {
		t.Errorf("want ErrDuplicateNodeID, got %v", err)
	}
}

func TestValidate_MissingDep(t *testing.T) {
	w := Workflow{Name: "x", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo", DependsOn: []string{"ghost"}},
	}}
	if err := w.Validate(); !errors.Is(err, ErrMissingDep) {
		t.Errorf("want ErrMissingDep, got %v", err)
	}
}

func TestValidate_DetectsCycle(t *testing.T) {
	w := Workflow{Name: "x", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo", DependsOn: []string{"b"}},
		{ID: "b", Mode: "fixture.echo", DependsOn: []string{"a"}},
	}}
	if err := w.Validate(); !errors.Is(err, ErrCycle) {
		t.Errorf("want ErrCycle, got %v", err)
	}
}

func TestRun_SingleNode(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "single", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo", Prompt: "hello"},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatal(err)
	}
	if res.Status != StatusSucceeded {
		t.Errorf("status: want succeeded, got %q", res.Status)
	}
	if len(res.Nodes) != 1 {
		t.Fatalf("nodes: want 1, got %d", len(res.Nodes))
	}
	if res.Nodes[0].Output["prompt"] != "hello" {
		t.Errorf("echo round-trip: %+v", res.Nodes[0].Output)
	}
}

func TestRun_DAG_RefSubstitution(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "chain", Nodes: []Node{
		{ID: "shape", Mode: "fixture.upper", Prompt: "hello world"},
		{ID: "weakness", Mode: "fixture.echo",
			Prompt: "Given $shape.output.upper find issue",
			DependsOn: []string{"shape"}},
		{ID: "improvement", Mode: "fixture.echo",
			Prompt: "Based on $weakness.output.prompt do better",
			DependsOn: []string{"weakness"}},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if res.Status != StatusSucceeded {
		t.Errorf("status: %q", res.Status)
	}
	// Order check: shape → weakness → improvement
	wantOrder := []string{"shape", "weakness", "improvement"}
	for i, want := range wantOrder {
		if res.Nodes[i].NodeID != want {
			t.Errorf("execution order %d: want %q, got %q", i, want, res.Nodes[i].NodeID)
		}
	}
	// shape uppercases "hello world" → "HELLO WORLD"
	if up := res.Nodes[0].Output["upper"]; up != "HELLO WORLD" {
		t.Errorf("shape.upper: %q", up)
	}
	// weakness sees "Given HELLO WORLD find issue" in its prompt
	wp, _ := res.Nodes[1].Output["prompt"].(string)
	if !strings.Contains(wp, "HELLO WORLD") {
		t.Errorf("weakness ref-substitution failed: %q", wp)
	}
	// improvement sees the SUBSTITUTED weakness prompt
	ip, _ := res.Nodes[2].Output["prompt"].(string)
	if !strings.Contains(ip, "HELLO WORLD") {
		t.Errorf("improvement chain-substitution failed: %q", ip)
	}
}

func TestRun_FailedNodeSkipsDownstream(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "skipchain", Nodes: []Node{
		{ID: "a", Mode: "fixture.fail"},
		{ID: "b", Mode: "fixture.echo", DependsOn: []string{"a"}},
		{ID: "c", Mode: "fixture.echo"}, // independent of a — should still run
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatal(err)
	}
	if res.Status != StatusPartial {
		t.Errorf("status: want partial, got %q", res.Status)
	}
	byID := make(map[string]NodeResult)
	for _, n := range res.Nodes {
		byID[n.NodeID] = n
	}
	if byID["a"].Error == "" {
		t.Error("a should have errored")
	}
	if byID["b"].Error == "" || !strings.Contains(byID["b"].Error, "upstream") {
		t.Errorf("b should be skipped with upstream-failure reason; got %q", byID["b"].Error)
	}
	if byID["c"].Error != "" {
		t.Errorf("c is independent; should run successfully; got error: %q", byID["c"].Error)
	}
}

func TestRun_UnknownModeAborts(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "bad", Nodes: []Node{
		{ID: "a", Mode: "fixture.does_not_exist"},
	}}
	res, err := r.Run(context.Background(), w)
	if !errors.Is(err, ErrUnknownMode) {
		t.Errorf("want ErrUnknownMode, got %v", err)
	}
	if res.Status != StatusAborted {
		t.Errorf("status: want aborted, got %q", res.Status)
	}
}

func TestRun_UnresolvedReferenceErrors(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "badref", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo",
			Prompt: "references $ghost.output but ghost doesn't exist"},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if res.Nodes[0].Error == "" {
		t.Error("unresolved $ghost should error the node")
	}
	if !strings.Contains(res.Nodes[0].Error, "no such node") {
		t.Errorf("error should explain no-such-node; got %q", res.Nodes[0].Error)
	}
}

func TestRun_ImplicitLLMChatFallback(t *testing.T) {
	r := NewRunner()
	r.RegisterMode("llm.chat", fixtureEcho) // pretend llm.chat exists
	w := Workflow{Name: "implicit", Nodes: []Node{
		{ID: "a", Prompt: "no Mode field — should default to llm.chat"},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatal(err)
	}
	if res.Status != StatusSucceeded {
		t.Errorf("implicit llm.chat: status %q", res.Status)
	}
	if res.Nodes[0].Mode != "llm.chat" {
		t.Errorf("effective mode: want llm.chat, got %q", res.Nodes[0].Mode)
	}
}

func TestRun_ProvenanceRecording(t *testing.T) {
	r := newTestRunner()
	w := Workflow{Name: "trace", Nodes: []Node{
		{ID: "x", Mode: "fixture.echo", Prompt: "trace me"},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatal(err)
	}
	n := res.Nodes[0]
	if n.NodeID != "x" || n.Mode != "fixture.echo" {
		t.Errorf("provenance: node=%q mode=%q", n.NodeID, n.Mode)
	}
	if n.StartedAt.IsZero() {
		t.Error("started_at should be set")
	}
	if n.DurationMs < 0 {
		t.Errorf("duration_ms: %d", n.DurationMs)
	}
}

func TestRun_InputsResolveRefs(t *testing.T) {
	// Verify that node.Inputs (not just Prompt) honors $-substitution.
	r := newTestRunner()
	w := Workflow{Name: "inputs", Nodes: []Node{
		{ID: "a", Mode: "fixture.echo", Prompt: "first"},
		{ID: "b", Mode: "fixture.echo",
			Inputs: map[string]any{
				"copied": "$a.output.prompt",
				"static": "literal",
			},
			DependsOn: []string{"a"}},
	}}
	res, err := r.Run(context.Background(), w)
	if err != nil {
		t.Fatal(err)
	}
	bOut := res.Nodes[1].Output
	if bOut["copied"] != "first" {
		t.Errorf("inputs ref: want 'first', got %q", bOut["copied"])
	}
	if bOut["static"] != "literal" {
		t.Errorf("inputs static: want 'literal', got %q", bOut["static"])
	}
}

func TestTopoSort_Stable(t *testing.T) {
	// Independent nodes preserve their declaration order.
	nodes := []Node{
		{ID: "z"}, {ID: "y"}, {ID: "x"},
	}
	got, err := topoSort(nodes)
	if err != nil {
		t.Fatal(err)
	}
	want := []string{"z", "y", "x"}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("position %d: want %q, got %q", i, want[i], got[i])
		}
	}
}
