package workflow_test

import (
	"archive/zip"
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"workflow"
)

// ── helpers ──────────────────────────────────────────────────────────────────

// makeZip builds an in-memory zip archive from a map of filename→content.
func makeZip(files map[string][]byte) []byte {
	var buf bytes.Buffer
	w := zip.NewWriter(&buf)
	for name, data := range files {
		f, _ := w.Create(name)
		f.Write(data)
	}
	w.Close()
	return buf.Bytes()
}

// v314Registry returns a standard registry allowing .tmp/**, artifacts/**,
// ExtComponents/**, Services/**, and Sections/** writes.
func v314Registry() workflow.Registry {
	return workflow.Registry{
		Services:   []string{},
		Components: []string{},
		Vars:       []string{},
		Files: workflow.FilesRegistry{
			Artifacts: []string{
				".tmp/**",
				"artifacts/**",
				"ExtComponents/**",
				"Services/**",
				"Sections/**",
			},
		},
	}
}

// v314Adapters wraps a FileAdapter in the full Adapters struct.
func v314Adapters(fa workflow.FileAdapter) *workflow.Adapters {
	return &workflow.Adapters{
		Service:   workflow.NewDefaultServiceAdapter(),
		Component: workflow.NewDefaultComponentAdapter(),
		LLM:       workflow.NewDefaultLLMAdapter(),
		File:      fa,
	}
}

func mustEngineV314(t *testing.T, wf *workflow.Workflow) *workflow.Engine {
	t.Helper()
	eng, err := workflow.NewEngine(wf)
	if err != nil {
		t.Fatalf("NewEngine: %v", err)
	}
	return eng
}

// mustExecuteV314 starts execution and blocks until the workflow finishes.
func mustExecuteV314(t *testing.T, eng *workflow.Engine, adapters *workflow.Adapters) *workflow.ExecutionResult {
	t.Helper()
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, adapters)
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(result.RunEventStream) // wait for goroutine to finish
	return result
}

// ── Write_* prepend (v3.14) ───────────────────────────────────────────────

func TestV314_WritePrepend(t *testing.T) {
	fa := workflow.NewDefaultFileAdapter()
	fa.SetFile("artifacts/file.txt", []byte("world"))

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Prepend Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Write_Prepend",
				Target: "artifacts/file.txt",
				Value:  "hello ",
				Mode:   "prepend",
				Next:   "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))

	got := string(fa.GetFile("artifacts/file.txt"))
	if want := "hello world"; got != want {
		t.Errorf("prepend: got %q, want %q", got, want)
	}
}

// ── .tmp/ path isolation (v3.14) ─────────────────────────────────────────

func TestV314_TmpPathIsolation(t *testing.T) {
	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Tmp Path Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Write_Tmp",
				Target: ".tmp/bundle.zip",
				Value:  "data",
				Next:   "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(result.RunEventStream) // wait for workflow to finish

	runID := result.Context.WorkflowID
	expectedPath := ".tmp/" + runID + "/bundle.zip"
	if fa.GetFile(expectedPath) == nil {
		t.Errorf(".tmp/ not isolated: expected file at %s", expectedPath)
	}
	// Bare unresolved path must NOT exist
	if fa.GetFile(".tmp/bundle.zip") != nil {
		t.Errorf(".tmp/ isolation: bare path should not be written")
	}
}

// ── _iterDir injection in parallel loop (v3.14) ──────────────────────────

func TestV314_IterDirParallel(t *testing.T) {
	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version: "3.14",
		Name:    "IterDir Parallel Test",
		Registry: workflow.Registry{
			Services:   []string{},
			Components: []string{},
			Vars:       []string{"$items([STRING])"},
			Files:      workflow.FilesRegistry{Artifacts: []string{".tmp/**"}},
		},
		Steps: []workflow.Step{
			{ID: "Set_Items", Target: "$items", Value: `=["a","b","c"]`, Next: "Loop_Process"},
			{
				ID:       "Loop_Process",
				Source:   "=$items",
				Mode:     "parallel",
				Children: []string{"Write_TmpFile"},
				Next:     "Stop_Done",
			},
			{ID: "Write_TmpFile", Target: ".tmp/{_iterDir}/item.txt", Value: "=_item", Next: "RETURN"},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(result.RunEventStream) // wait for all parallel goroutines to finish

	runID := result.Context.WorkflowID
	for i, want := range []string{"a", "b", "c"} {
		iterDir := fmt.Sprintf("Loop_Process_%d", i)
		path := ".tmp/" + runID + "/" + iterDir + "/item.txt"
		got := fa.GetFile(path)
		if got == nil {
			t.Errorf("missing file for iteration %d at %s", i, path)
			continue
		}
		if string(got) != want {
			t.Errorf("iteration %d: got %q, want %q", i, string(got), want)
		}
	}
}

// ── _iterDir injection in serial loop (v3.14) ────────────────────────────

func TestV314_IterDirSerial(t *testing.T) {
	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version: "3.14",
		Name:    "IterDir Serial Test",
		Registry: workflow.Registry{
			Services:   []string{},
			Components: []string{},
			Vars:       []string{"$items([STRING])"},
			Files:      workflow.FilesRegistry{Artifacts: []string{".tmp/**"}},
		},
		Steps: []workflow.Step{
			{ID: "Set_Items", Target: "$items", Value: `=["x","y"]`, Next: "Loop_Serial"},
			{
				ID:       "Loop_Serial",
				Source:   "=$items",
				Mode:     "serial",
				Children: []string{"Write_TmpFile"},
				Next:     "Stop_Done",
			},
			{ID: "Write_TmpFile", Target: ".tmp/{_iterDir}/item.txt", Value: "=_item", Next: "RETURN"},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(result.RunEventStream) // wait for workflow to finish

	runID := result.Context.WorkflowID
	for i, want := range []string{"x", "y"} {
		iterDir := fmt.Sprintf("Loop_Serial_%d", i)
		path := ".tmp/" + runID + "/" + iterDir + "/item.txt"
		got := fa.GetFile(path)
		if got == nil {
			t.Errorf("missing file for serial iteration %d at %s", i, path)
			continue
		}
		if string(got) != want {
			t.Errorf("serial iteration %d: got %q, want %q", i, string(got), want)
		}
	}
}

// ── Download_* with explicit target (v3.14) ──────────────────────────────

func TestV314_DownloadTarget(t *testing.T) {
	content := []byte("file-content-abc")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write(content)
	}))
	defer srv.Close()

	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Download Target Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Download_File",
				Source: fmt.Sprintf("%s/file.cp", srv.URL),
				Target: "artifacts/downloaded.cp",
				Next:   "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))

	got := fa.GetFile("artifacts/downloaded.cp")
	if !bytes.Equal(got, content) {
		t.Errorf("download content mismatch: got %q, want %q", got, content)
	}
}

// ── Download_* with routeByExt (v3.14) ───────────────────────────────────

func TestV314_DownloadRouteByExt(t *testing.T) {
	content := []byte("component-data")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write(content)
	}))
	defer srv.Close()

	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Download RouteByExt Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Download_Cp",
				Source: fmt.Sprintf("%s/MyWidget.cp", srv.URL),
				RouteByExt: map[string]string{
					".cp": "ExtComponents/",
					".vs": "Services/",
				},
				DefaultDir: "artifacts/misc/",
				Next:       "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))

	got := fa.GetFile("ExtComponents/MyWidget.cp")
	if !bytes.Equal(got, content) {
		t.Errorf("routeByExt content mismatch: got %q, want %q", got, content)
	}
}

// ── Unzip_* with routeByExt (v3.14) ──────────────────────────────────────

func TestV314_UnzipRouteByExt(t *testing.T) {
	zipData := makeZip(map[string][]byte{
		"Widget.cp":  []byte("cp-content"),
		"Service.vs": []byte("vs-content"),
		"Readme.txt": []byte("readme"),
	})

	fa := workflow.NewDefaultFileAdapter()
	fa.SetFile("artifacts/bundle.zip", zipData)

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Unzip RouteByExt Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Unzip_Bundle",
				Source: "artifacts/bundle.zip",
				RouteByExt: map[string]string{
					".cp": "ExtComponents/",
					".vs": "Services/",
				},
				DefaultDir: "artifacts/misc/",
				Next:       "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	mustExecuteV314(t, mustEngineV314(t, wf), v314Adapters(fa))

	cases := []struct{ path, want string }{
		{"ExtComponents/Widget.cp", "cp-content"},
		{"Services/Service.vs", "vs-content"},
		{"artifacts/misc/Readme.txt", "readme"},
	}
	for _, c := range cases {
		got := string(fa.GetFile(c.path))
		if got != c.want {
			t.Errorf("%s: got %q, want %q", c.path, got, c.want)
		}
	}
}

// ── Unzip_* zip-slip protection (v3.14) ──────────────────────────────────

func TestV314_UnzipZipSlipRejected(t *testing.T) {
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	f, _ := zw.Create("../../etc/passwd")
	f.Write([]byte("root:x:0:0"))
	zw.Close()

	fa := workflow.NewDefaultFileAdapter()
	fa.SetFile("artifacts/evil.zip", buf.Bytes())

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "ZipSlip Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:         "Unzip_Evil",
				Source:     "artifacts/evil.zip",
				RouteByExt: map[string]string{},
				DefaultDir: "artifacts/out/",
				Next:       "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute (start): %v", err)
	}
	drainEvents(result.RunEventStream) // wait for workflow to finish

	if result.Context.Status != workflow.StatusFailed {
		t.Errorf("expected workflow to fail for zip-slip entry, got status %q", result.Context.Status)
	}
}

// ── Unzip_* overwrite=false (v3.14) ──────────────────────────────────────

func TestV314_UnzipNoOverwrite(t *testing.T) {
	zipData := makeZip(map[string][]byte{
		"file.cp": []byte("new-content"),
	})

	fa := workflow.NewDefaultFileAdapter()
	fa.SetFile("artifacts/bundle.zip", zipData)
	fa.SetFile("ExtComponents/file.cp", []byte("original"))

	overwriteFalse := false
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Unzip No Overwrite Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:         "Unzip_Bundle",
				Source:     "artifacts/bundle.zip",
				RouteByExt: map[string]string{".cp": "ExtComponents/"},
				Overwrite:  &overwriteFalse,
				Next:       "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute (start): %v", err)
	}
	drainEvents(result.RunEventStream) // wait for workflow to finish

	if result.Context.Status != workflow.StatusFailed {
		t.Errorf("expected workflow to fail when overwrite=false on existing file, got status %q", result.Context.Status)
	}
	if got := string(fa.GetFile("ExtComponents/file.cp")); got != "original" {
		t.Errorf("original file overwritten despite overwrite=false, got %q", got)
	}
}

// ── Download_* → Unzip_* full pipeline (v3.14) ───────────────────────────

func TestV314_DownloadThenUnzip(t *testing.T) {
	zipData := makeZip(map[string][]byte{
		"Alpha.cp": []byte("alpha"),
		"Beta.vs":  []byte("beta"),
	})

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write(zipData)
	}))
	defer srv.Close()

	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Download+Unzip Pipeline Test",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Download_Bundle",
				Source: fmt.Sprintf("%s/bundle.zip", srv.URL),
				Target: ".tmp/bundle.zip",
				Next:   "Unzip_Bundle",
			},
			{
				ID:     "Unzip_Bundle",
				Source: ".tmp/bundle.zip",
				RouteByExt: map[string]string{
					".cp": "ExtComponents/",
					".vs": "Services/",
				},
				Next: "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	result, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(result.RunEventStream) // wait for workflow to finish

	runID := result.Context.WorkflowID
	tmpZip := ".tmp/" + runID + "/bundle.zip"
	if fa.GetFile(tmpZip) == nil {
		t.Errorf("expected zip at isolated path %s", tmpZip)
	}
	if got := fa.GetFile("ExtComponents/Alpha.cp"); !bytes.Equal(got, []byte("alpha")) {
		t.Errorf("Alpha.cp: got %q, want %q", got, "alpha")
	}
	if got := fa.GetFile("Services/Beta.vs"); !bytes.Equal(got, []byte("beta")) {
		t.Errorf("Beta.vs: got %q, want %q", got, "beta")
	}
}

// ── Version 3.14 accepted by validator ───────────────────────────────────

func TestV314_VersionAccepted(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Version Test",
		Registry: v314Registry(),
		Steps:    []workflow.Step{{ID: "Stop_Done"}},
	}
	if err := wf.Validate(); err != nil {
		t.Errorf("version 3.14 should be accepted: %v", err)
	}
}

// ── Validation: Download_* missing source ────────────────────────────────

func TestV314_Validate_DownloadMissingSource(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Validate Download No Source",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{ID: "Download_File", Target: "artifacts/file.txt", Next: "Stop_Done"}, // no Source
			{ID: "Stop_Done"},
		},
	}
	if err := wf.Validate(); err == nil {
		t.Error("expected validation error for Download_* without source, got nil")
	}
}

// ── Validation: Download_* missing target and routeByExt ─────────────────

func TestV314_Validate_DownloadMissingRoute(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Validate Download No Route",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			// Has source but neither target nor routeByExt/defaultDir
			{ID: "Download_File", Source: "http://example.com/file.zip", Next: "Stop_Done"},
			{ID: "Stop_Done"},
		},
	}
	if err := wf.Validate(); err == nil {
		t.Error("expected validation error for Download_* without target/routeByExt, got nil")
	}
}

// ── Validation: Download_* both target and routeByExt (mutually exclusive) ─

func TestV314_Validate_DownloadBothTargetAndRoute(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Validate Download Both Target and Route",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:         "Download_File",
				Source:     "http://example.com/file.zip",
				Target:     "artifacts/file.zip",
				RouteByExt: map[string]string{".zip": "artifacts/"},
				Next:       "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}
	if err := wf.Validate(); err == nil {
		t.Error("expected validation error when both target and routeByExt are set, got nil")
	}
}

// ── Validation: Unzip_* missing source ───────────────────────────────────

func TestV314_Validate_UnzipMissingSource(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Validate Unzip No Source",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:         "Unzip_Bundle",
				RouteByExt: map[string]string{".cp": "ExtComponents/"},
				Next:       "Stop_Done",
			}, // no Source
			{ID: "Stop_Done"},
		},
	}
	if err := wf.Validate(); err == nil {
		t.Error("expected validation error for Unzip_* without source, got nil")
	}
}

// ── Validation: Unzip_* missing routeByExt ───────────────────────────────

func TestV314_Validate_UnzipMissingRouteByExt(t *testing.T) {
	wf := &workflow.Workflow{
		Version:  "3.14",
		Name:     "Validate Unzip No RouteByExt",
		Registry: v314Registry(),
		Steps: []workflow.Step{
			{
				ID:     "Unzip_Bundle",
				Source: "artifacts/bundle.zip",
				// RouteByExt not set (nil)
				Next: "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}
	if err := wf.Validate(); err == nil {
		t.Error("expected validation error for Unzip_* without routeByExt, got nil")
	}
}

// ── Unzip_* out mapping captures extraction summary ──────────────────────

func TestV314_UnzipOutMapping(t *testing.T) {
	zipData := makeZip(map[string][]byte{
		"A.cp": []byte("a-content"),
		"B.cp": []byte("b-content"),
	})

	fa := workflow.NewDefaultFileAdapter()
	fa.SetFile("artifacts/pkg.zip", zipData)

	wf := &workflow.Workflow{
		Version: "3.14",
		Name:    "Unzip Out Mapping Test",
		Registry: workflow.Registry{
			Services:   []string{},
			Components: []string{},
			Vars:       []string{"$unzipCount(NUMBER)", "$unzipFiles([STRING])"},
			Files: workflow.FilesRegistry{
				Artifacts: []string{"artifacts/**", "ExtComponents/**"},
			},
		},
		Steps: []workflow.Step{
			{
				ID:     "Unzip_Bundle",
				Source: "artifacts/pkg.zip",
				RouteByExt: map[string]string{
					".cp": "ExtComponents/",
				},
				Out: workflow.StepOutput{
					"$unzipCount": "=_result.count",
					"$unzipFiles": "=_result.files",
				},
				Next: "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	res, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(res.RunEventStream)

	// Verify count was mapped (extractedPaths has 2 entries)
	count := res.Context.Variables["$unzipCount"]
	if count == nil {
		t.Error("$unzipCount should be set after out mapping")
	}
	// Verify files list was mapped (non-nil, and the correct count)
	files := res.Context.Variables["$unzipFiles"]
	if files == nil {
		t.Error("$unzipFiles should be set after out mapping")
	}
}

// ── Download_* out mapping captures written path ──────────────────────────

func TestV314_DownloadOutMapping(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("download-content"))
	}))
	defer srv.Close()

	fa := workflow.NewDefaultFileAdapter()

	wf := &workflow.Workflow{
		Version: "3.14",
		Name:    "Download Out Mapping Test",
		Registry: workflow.Registry{
			Services:   []string{},
			Components: []string{},
			Vars:       []string{"$downloadedPath(STRING)"},
			Files: workflow.FilesRegistry{
				Artifacts: []string{"artifacts/**"},
			},
		},
		Steps: []workflow.Step{
			{
				ID:     "Download_File",
				Source: fmt.Sprintf("%s/widget.cp", srv.URL),
				Target: "artifacts/widget.cp",
				Out: workflow.StepOutput{
					"$downloadedPath": "=_result.path",
				},
				Next: "Stop_Done",
			},
			{ID: "Stop_Done"},
		},
	}

	eng := mustEngineV314(t, wf)
	res, err := eng.Execute(context.Background(), map[string]interface{}{}, v314Adapters(fa))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	drainEvents(res.RunEventStream)

	got, ok := res.Context.Variables["$downloadedPath"].(string)
	if !ok || got == "" {
		t.Errorf("$downloadedPath: got %v (%T), want non-empty string",
			res.Context.Variables["$downloadedPath"], res.Context.Variables["$downloadedPath"])
	}
	if got != "artifacts/widget.cp" {
		t.Errorf("$downloadedPath: got %q, want %q", got, "artifacts/widget.cp")
	}
}
