package cli

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"git.cloudinit.dev/coreci/orca/internal/paths"
)

// resetNSFlags zeroes the ns subcommand flag-bound vars so tests don't
// leak state.
func resetNSFlags() {
	nsCreateParent = ""
	nsCreateInheritsEnv = true
	nsCreateInheritsSecret = true
	nsInheritParent = ""
}

func writeDefaultsNS(t *testing.T, root string) {
	t.Helper()
	nsDir := filepath.Join(root, "_defaults")
	if err := os.MkdirAll(nsDir, 0o755); err != nil {
		t.Fatalf("mkdir _defaults: %v", err)
	}
	body := "---\nkind: Namespace\nname: _defaults\ninherits_env: true\ninherits_secrets: true\n---\n# defaults\n"
	if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil {
		t.Fatalf("write _defaults ns.md: %v", err)
	}
}

func writeCustomNS(t *testing.T, root, name, parentsList string) {
	t.Helper()
	nsDir := filepath.Join(root, name)
	if err := os.MkdirAll(nsDir, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", name, err)
	}
	body := "---\nkind: Namespace\nname: " + name + "\n"
	if parentsList != "" {
		body += "parents: " + parentsList + "\n"
	}
	body += "inherits_env: true\ninherits_secrets: true\n---\n# " + name + "\n"
	if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil {
		t.Fatalf("write %s ns.md: %v", name, err)
	}
}

func TestNSListEmpty(t *testing.T) {
	t.Setenv("ORCA_HOME", t.TempDir())
	resetRootFlags(t)
	resetNSFlags()

	rootCmd.SetArgs([]string{"ns", "list"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns list: %v", err)
	}
}

func TestNSListWithNamespaces(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "list"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns list: %v", err)
	}
}

func TestNSCreateHappy(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()

	rootCmd.SetArgs([]string{"ns", "create", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns create: %v", err)
	}
	if _, err := os.Stat(filepath.Join(root, "prod", "ns.md")); err != nil {
		t.Fatalf("ns.md not created: %v", err)
	}
	for _, sub := range []string{"db", "jobs", "alloc"} {
		if _, err := os.Stat(filepath.Join(root, "prod", sub)); err != nil {
			t.Errorf("subdir %s not created: %v", sub, err)
		}
	}
}

func TestNSCreateWithParent(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "base", "")

	rootCmd.SetArgs([]string{"ns", "create", "child", "--parent", "base"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns create: %v", err)
	}
	data, err := os.ReadFile(filepath.Join(root, "child", "ns.md"))
	if err != nil {
		t.Fatalf("read ns.md: %v", err)
	}
	if !strings.Contains(string(data), "parents: [") || !strings.Contains(string(data), "\"base\"") {
		t.Errorf("ns.md missing parents: %s", string(data))
	}
}

func TestNSCreateExisting(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "create", "prod"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error creating existing namespace, got nil")
	}
	if !strings.Contains(err.Error(), "already exists") {
		t.Errorf("error = %q, want contains 'already exists'", err.Error())
	}
}

func TestNSCreateDefaultsRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()

	rootCmd.SetArgs([]string{"ns", "create", "_defaults"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error creating _defaults, got nil")
	}
}

func TestNSCreateClusterRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()

	rootCmd.SetArgs([]string{"ns", "create", "cluster"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error creating cluster, got nil")
	}
}

func TestNSDeleteHappy(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "delete", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns delete: %v", err)
	}
	if _, err := os.Stat(filepath.Join(root, "prod")); !os.IsNotExist(err) {
		t.Errorf("prod dir still exists after delete")
	}
}

func TestNSDeleteDefaultsRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)

	rootCmd.SetArgs([]string{"ns", "delete", "_defaults"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error deleting _defaults, got nil")
	}
}

func TestNSDeleteNonEmpty(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")
	// put a job in jobs/
	if err := os.MkdirAll(filepath.Join(root, "prod", "jobs"), 0o755); err != nil {
		t.Fatalf("mkdir jobs: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "prod", "jobs", "j1.md"), []byte("x"), 0o644); err != nil {
		t.Fatalf("write job: %v", err)
	}

	rootCmd.SetArgs([]string{"ns", "delete", "prod"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error deleting non-empty namespace, got nil")
	}
	if !strings.Contains(err.Error(), "refusing to delete") {
		t.Errorf("error = %q, want contains 'refusing to delete'", err.Error())
	}
}

func TestNSDeleteMissing(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)

	rootCmd.SetArgs([]string{"ns", "delete", "ghost"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error deleting missing namespace, got nil")
	}
}

func TestNSInspectHappy(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "inspect", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns inspect: %v", err)
	}
}

func TestNSInspectJSON(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetArgs([]string{"ns", "inspect", "prod", "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns inspect --json: %v", err)
	}
	var result map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
		t.Fatalf("unmarshal: %v\n%s", err, buf.String())
	}
	if result["name"] != "prod" {
		t.Errorf("name = %v, want prod", result["name"])
	}
	chain, _ := result["chain"].([]any)
	if len(chain) < 2 {
		t.Errorf("chain too short: %v", chain)
	}
}

func TestNSInspectMissingNamespace(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)

	rootCmd.SetArgs([]string{"ns", "inspect", "ghost"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error for missing namespace, got nil")
	}
}

func TestNSValidateHappy(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "validate", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns validate: %v", err)
	}
}

func TestNSValidateCycle(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	// a -> b, b -> a (cycle)
	writeCustomNS(t, root, "a", "[\"b\"]")
	writeCustomNS(t, root, "b", "[\"a\"]")

	rootCmd.SetArgs([]string{"ns", "validate", "a"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected cycle error, got nil")
	}
	if !strings.Contains(err.Error(), "cycle") {
		t.Errorf("error = %q, want contains 'cycle'", err.Error())
	}
}

func TestNSValidateMissingParent(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "x", "[\"ghost\"]")

	rootCmd.SetArgs([]string{"ns", "validate", "x"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected missing-parent error, got nil")
	}
	if !strings.Contains(err.Error(), "ghost") || !strings.Contains(err.Error(), "not found") {
		t.Errorf("error = %q, want contains ghost + not found", err.Error())
	}
}

func TestNSValidateMissingNamespace(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)

	rootCmd.SetArgs([]string{"ns", "validate", "ghost"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error for missing namespace, got nil")
	}
}

func TestRenderNSMd(t *testing.T) {
	body := renderNSMd("foo", []string{"_defaults"}, true, false)
	if !strings.Contains(body, "kind: Namespace") {
		t.Errorf("missing kind: %s", body)
	}
	if !strings.Contains(body, "name: foo") {
		t.Errorf("missing name: %s", body)
	}
	if !strings.Contains(body, "inherits_env: true") {
		t.Errorf("missing inherits_env true: %s", body)
	}
	if !strings.Contains(body, "inherits_secrets: false") {
		t.Errorf("missing inherits_secrets false: %s", body)
	}
}

func TestNSRootRegistered(t *testing.T) {
	found := false
	for _, c := range rootCmd.Commands() {
		if c.Use == "ns" {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("ns command not registered on root")
	}
	// ensure subcommands present
	sub := map[string]bool{}
	for _, c := range rootCmd.Commands() {
		if c.Use == "ns" {
			for _, sc := range c.Commands() {
				sub[sc.Use] = true
			}
		}
	}
	for _, want := range []string{"list", "create <name>", "delete <name>", "inspect <name>", "validate <name>", "inherit <name>", "set-constraint <name> <key>=<value>"} {
		if !sub[want] {
			t.Errorf("missing ns subcommand %q", want)
		}
	}
}

func TestNSListNoORCAHOME(t *testing.T) {
	// ORCA_HOME points at a nonexistent dir; list should error.
	t.Setenv("ORCA_HOME", filepath.Join(t.TempDir(), "nope"))
	resetRootFlags(t)
	resetNSFlags()

	rootCmd.SetArgs([]string{"ns", "list"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error for missing ORCA_HOME, got nil")
	}
}

var _ = paths.DefaultNamespace // keep paths import alive

func TestNSInheritSetsParent(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "_defaults"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns inherit: %v", err)
	}
	data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md"))
	if err != nil {
		t.Fatalf("read ns.md: %v", err)
	}
	if !strings.Contains(string(data), "parents:") || !strings.Contains(string(data), "_defaults") {
		t.Errorf("ns.md missing parents: %s", string(data))
	}
}

func TestNSInheritCycleRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	// a -> b already; now set a's parent to b, then try b -> a.
	writeCustomNS(t, root, "a", `["b"]`)
	writeCustomNS(t, root, "b", "")

	resetNSFlags()
	rootCmd.SetArgs([]string{"ns", "inherit", "b", "--parent", "a"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected cycle error, got nil")
	}
	if !strings.Contains(err.Error(), "cycle") {
		t.Errorf("error = %q, want contains 'cycle'", err.Error())
	}
}

func TestNSInheritSelfRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "prod"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected self-inherit error, got nil")
	}
}

func TestNSInheritDefaultsRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "inherit", "_defaults", "--parent", "prod"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error setting parent on _defaults, got nil")
	}
}

func TestNSSetConstraint(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns set-constraint: %v", err)
	}
	data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md"))
	if err != nil {
		t.Fatalf("read ns.md: %v", err)
	}
	if !strings.Contains(string(data), "constraints:") || !strings.Contains(string(data), "max-allocs=10") {
		t.Errorf("ns.md missing constraints: %s", string(data))
	}
}

func TestNSSetConstraintValidatePasses(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns set-constraint: %v", err)
	}
	resetNSFlags()
	rootCmd.SetArgs([]string{"ns", "validate", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns validate after set-constraint: %v", err)
	}
}

func TestNSSetConstraintInspectShowsConstraint(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns set-constraint: %v", err)
	}

	resetNSFlags()
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetArgs([]string{"ns", "inspect", "prod", "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns inspect --json: %v", err)
	}
	var result map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
		t.Fatalf("unmarshal: %v\n%s", err, buf.String())
	}
	cons, _ := result["constraints"].([]any)
	found := false
	for _, c := range cons {
		if c == "max-allocs=10" {
			found = true
		}
	}
	if !found {
		t.Errorf("constraint max-allocs=10 not in inspect output: %v", cons)
	}
}

func TestNSSetConstraintInvalidFormat(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "noequals"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error for malformed constraint, got nil")
	}
	if !strings.Contains(err.Error(), "<key>=<value>") {
		t.Errorf("error = %q, want contains '<key>=<value>'", err.Error())
	}
}

func TestNSSetConstraintDuplicate(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")

	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("ns set-constraint first: %v", err)
	}
	resetNSFlags()
	rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected duplicate error, got nil")
	}
	if !strings.Contains(err.Error(), "already set") {
		t.Errorf("error = %q, want contains 'already set'", err.Error())
	}
}

// --- REQ-120 / F4 path traversal regression tests ---

// TestNSCreateTraversalRefused verifies that ns create rejects names
// that would traverse outside ORCA_HOME via ".." or "/".
func TestNSCreateTraversalRefused(t *testing.T) {
	bad := []string{
		"..",
		"../etc",
		"foo/../bar",
		"/etc",
		"etc/",
		"foo/bar",
		"-x",
		"--flag",
		"with space",
		"tab\there",
		"newline\nname",
	}
	for _, name := range bad {
		t.Run(name, func(t *testing.T) {
			root := t.TempDir()
			t.Setenv("ORCA_HOME", root)
			resetRootFlags(t)
			resetNSFlags()
			rootCmd.SetArgs([]string{"ns", "create", name})
			err := rootCmd.Execute()
			if err == nil {
				t.Errorf("ns create %q should fail, got nil", name)
			}
			// Verify no directory was created outside ORCA_HOME.
			// For ".." and "../etc", the danger is a dir was created
			// outside root. Check root's parent has no new orca dirs.
			parent := filepath.Dir(root)
			entries, _ := os.ReadDir(parent)
			for _, e := range entries {
				// The temp dir itself is fine; anything else that looks
				// like an orca namespace (has ns.md) outside root is a
				// leak.
				if e.Name() == filepath.Base(root) {
					continue
				}
				if _, err := os.Stat(filepath.Join(parent, e.Name(), "ns.md")); err == nil {
					t.Errorf("namespace dir leaked outside ORCA_HOME: %s", filepath.Join(parent, e.Name()))
				}
			}
		})
	}
}

// TestNSInheritTraversalRefused verifies --parent rejects traversal.
func TestNSInheritTraversalRefused(t *testing.T) {
	root := t.TempDir()
	t.Setenv("ORCA_HOME", root)
	resetRootFlags(t)
	resetNSFlags()
	writeDefaultsNS(t, root)
	writeCustomNS(t, root, "prod", "")
	rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "../../etc"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("ns inherit with traversal --parent should fail")
	}
}
