package main

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

	"iop/packages/go/version"
)

func TestRootCmdIncludesOperationalCommands(t *testing.T) {
	root := rootCmd()
	want := map[string]bool{
		"serve":     false,
		"console":   false,
		"version":   false,
		"config":    false,
		"env":       false,
		"setup":     false,
		"bootstrap": false,
		"node":      false,
		"nodes":     false,
		"smoke":     false,
	}
	for _, c := range root.Commands() {
		if _, ok := want[c.Name()]; ok {
			want[c.Name()] = true
		}
	}
	for name, ok := range want {
		if !ok {
			t.Errorf("root command missing %q", name)
		}
	}

	cfg, _, err := root.Find([]string{"config"})
	if err != nil {
		t.Fatalf("find config: %v", err)
	}
	subs := map[string]bool{"print": false, "check": false}
	subs["init"] = false
	for _, c := range cfg.Commands() {
		if _, ok := subs[c.Name()]; ok {
			subs[c.Name()] = true
		}
	}
	for name, ok := range subs {
		if !ok {
			t.Errorf("config subcommand missing %q", name)
		}
	}
}

func TestVersionCmdPrintsVersion(t *testing.T) {
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"version"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v", err)
	}
	got := strings.TrimSpace(out.String())
	if got != version.Version {
		t.Fatalf("version output = %q, want %q", got, version.Version)
	}
}

func TestConfigCheckCmdLoadsEdgeConfig(t *testing.T) {
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", "../../../../configs/edge.yaml"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), "OK") {
		t.Fatalf("expected OK in output, got %q", out.String())
	}
}

func TestConfigInitWritesTemplateThatChecks(t *testing.T) {
	dir := t.TempDir()
	outPath := filepath.Join(dir, "edge.yaml")

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "init", "--output", outPath})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute init: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), "wrote "+outPath) {
		t.Fatalf("expected written path in output, got %q", out.String())
	}

	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", outPath})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute check: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), "OK "+outPath) {
		t.Fatalf("expected config check OK for generated file, got %q", out.String())
	}
}

func TestConfigInitPrintsTemplateToStdout(t *testing.T) {
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "init", "--stdout"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	for _, want := range []string{"edge:", "server:", "nodes: []"} {
		if !strings.Contains(out.String(), want) {
			t.Fatalf("template output missing %q:\n%s", want, out.String())
		}
	}
}

func TestConfigInitPrintsBundleTemplateWithEmptyLogPath(t *testing.T) {
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "init", "--stdout"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	got := out.String()
	for _, want := range []string{
		"advertise_host:",
		"bootstrap:",
		"artifact_base_url:",
		"path: \"\"",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("bundle init output missing %q\n---\n%s", want, got)
		}
	}
	if strings.Contains(got, "/var/lib/iop/edge/logs/edge.log") {
		t.Errorf("bundle init leaked service log path:\n%s", got)
	}
}

func TestConfigInitRefusesOverwriteWithoutForce(t *testing.T) {
	outPath := filepath.Join(t.TempDir(), "edge.yaml")
	if err := os.WriteFile(outPath, []byte("preserved: true\n"), 0o600); err != nil {
		t.Fatalf("write existing: %v", err)
	}

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "init", "--output", outPath})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected overwrite refusal, output:\n%s", out.String())
	}
	got, err := os.ReadFile(outPath)
	if err != nil {
		t.Fatalf("read existing: %v", err)
	}
	if string(got) != "preserved: true\n" {
		t.Fatalf("existing file was overwritten: %q", got)
	}
}

func TestSetupDryRunUsesEdgeDefaults(t *testing.T) {
	cfgFile = "configs/edge.yaml" // reset to root default
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"setup", "--dry-run", "--binary", "/usr/local/bin/iop-edge"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	s := out.String()
	for _, want := range []string{
		"/etc/iop/edge.yaml",
		"/var/lib/iop/edge",
		"/etc/systemd/system/iop-edge.service",
		"/usr/local/bin/iop-edge serve --config /etc/iop/edge.yaml",
		"--- unit preview ---",
		"--- config preview ---",
		"path: \"/var/lib/iop/edge/logs/edge.log\"",
	} {
		if !strings.Contains(s, want) {
			t.Errorf("dry-run output missing %q\n---\n%s", want, s)
		}
	}
	if strings.Contains(s, "configs/edge.yaml") {
		t.Errorf("setup default leaked root dev config path: %s", s)
	}
}

func TestSetupDryRunAcceptsExplicitConfig(t *testing.T) {
	cfgFile = "configs/edge.yaml" // reset
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	custom := filepath.Join(t.TempDir(), "edge.yaml")
	root.SetArgs([]string{"--config", custom, "setup", "--dry-run", "--binary", "/usr/local/bin/iop-edge"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), custom) {
		t.Errorf("explicit config not used; output:\n%s", out.String())
	}
}

func TestConfigCheckUsesBundleEdgeYAMLByDefault(t *testing.T) {
	dir := t.TempDir()
	writeMinimalEdgeYAML(t, filepath.Join(dir, "edge.yaml"))
	t.Chdir(dir)

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), "OK edge.yaml") {
		t.Fatalf("expected OK edge.yaml (cwd bundle), got %q", out.String())
	}
}

func TestExplicitConfigWinsOverBundleDefault(t *testing.T) {
	dir := t.TempDir()
	writeMinimalEdgeYAML(t, filepath.Join(dir, "edge.yaml"))
	t.Chdir(dir)

	otherDir := t.TempDir()
	other := filepath.Join(otherDir, "edge.yaml")
	writeMinimalEdgeYAML(t, other)

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", other})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}
	if !strings.Contains(out.String(), "OK "+other) {
		t.Fatalf("expected OK %s, got %q", other, out.String())
	}
}

func TestConfigDiscoveryReportsMissingWhenNoBundleFound(t *testing.T) {
	dir := t.TempDir()
	t.Chdir(dir)

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check"})
	err := root.Execute()
	if err == nil {
		t.Fatalf("expected error when no edge.yaml found, got output: %s", out.String())
	}
	if !strings.Contains(err.Error(), "no edge.yaml found") {
		t.Fatalf("expected discovery error, got %v", err)
	}
}

func TestEnvCmdPrintsResolvedReportWithExplicitAdvertiseHost(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yaml := `server:
  listen: "0.0.0.0:9090"
  advertise_host: "edge.example.test"
bootstrap:
  artifact_base_url: "http://edge.example.test:18080"
openai:
  enabled: true
  listen: "0.0.0.0:8080"
  provider_id: "test-provider"
nodes:
  - id: "node-a"
    alias: "a"
    token: "t-a"
  - id: "node-b"
    alias: "b"
    token: "t-b"
`
	if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"env", "--config", cfgPath})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}

	got := out.String()
	wants := []string{
		"config: " + cfgPath,
		"binary_dir: ",
		"log_path: ",
		"advertise_host: edge.example.test (explicit)",
		"node_transport: edge.example.test:9090",
		"artifact_base_url: http://edge.example.test:18080",
		"openai_url: http://edge.example.test:8080/v1",
		"configured_nodes: 2 (node-a, node-b)",
		"connected_nodes: offline",
	}
	for _, w := range wants {
		if !strings.Contains(got, w) {
			t.Errorf("env output missing %q\n---\n%s", w, got)
		}
	}
}

func TestConfigCheckRejectsDuplicateNodeTokenAndInvalidConfigs(t *testing.T) {
	// Duplicate token test
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yamlStr := `server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-a"
    alias: "a"
    token: "same-token"
    adapters:
      ollama:
        enabled: true
        base_url: "http://127.0.0.1:11434"
  - id: "node-b"
    alias: "b"
    token: "same-token"
    adapters:
      ollama:
        enabled: true
        base_url: "http://127.0.0.1:11434"
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", cfgPath})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected error due to duplicate token, got nil")
	}

	// Invalid runtime concurrency test
	yamlStr2 := `server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-a"
    alias: "a"
    token: "token-a"
    adapters:
      ollama:
        enabled: true
        base_url: "http://127.0.0.1:11434"
    runtime:
      concurrency: -1
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr2), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", cfgPath})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected error due to negative concurrency, got nil")
	}

	// Invalid Ollama BaseURL test
	yamlStr3 := `server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-a"
    alias: "a"
    token: "token-a"
    adapters:
      ollama:
        enabled: true
        base_url: ""
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr3), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", cfgPath})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected error due to empty ollama base_url, got nil")
	}
}

func TestConfigCheckRejectsAdapterlessNode(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")

	yamlStr := `server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-a"
    alias: "node-a"
    token: "token-a"
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check", "--config", cfgPath})
	err := root.Execute()
	if err == nil {
		t.Fatalf("expected adapterless node config to fail, got OK output %q", out.String())
	}
	if !strings.Contains(err.Error(), "at least one adapter must be enabled") {
		t.Fatalf("expected adapterless error, got %v\n%s", err, out.String())
	}
}

func TestHelpDiscoversEdgeLocalDevFlow(t *testing.T) {
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--help"})
	if err := root.Execute(); err != nil {
		t.Fatalf("help execute failed: %v", err)
	}
	got := out.String()

	// 1. Assert official flow-level help
	wants := []string{
		"1. config init",
		"2. env",
		"3. node register",
		"4. config check",
		"5. serve",
		"6. nodes list",
		"7. smoke openai",
	}
	for _, w := range wants {
		if !strings.Contains(got, w) {
			t.Errorf("root help output missing official flow step: %q", w)
		}
	}

	// 2. Assert absence of non-official/legacy paths (verify they aren't recommended)
	for _, unwanted := range []string{"scripts/dev/edge.sh", "scripts/dev/node.sh", "dev-deploy", "agent register"} {
		if strings.Contains(got, unwanted) {
			t.Errorf("root help contains legacy path reference: %q", unwanted)
		}
	}

	helpCases := []struct {
		name string
		args []string
		want []string
	}{
		{
			name: "config",
			args: []string{"config", "--help"},
			want: []string{"config init", "iop-edge env", "node register", "config check", "serve"},
		},
		{
			name: "node",
			args: []string{"node", "--help"},
			want: []string{"node register", "edge.yaml", "one-line bootstrap command", "config check"},
		},
		{
			name: "node register",
			args: []string{"node", "register", "--help"},
			want: []string{"one-line bootstrap command", "node.yaml", "iop-node serve", "--adapter", "--target"},
		},
		{
			name: "smoke",
			args: []string{"smoke", "--help"},
			want: []string{"smoke openai", "config init", "node register", "serve"},
		},
	}
	for _, tc := range helpCases {
		root := rootCmd()
		out.Reset()
		root.SetOut(&out)
		root.SetErr(&out)
		root.SetArgs(tc.args)
		if err := root.Execute(); err != nil {
			t.Fatalf("%s help execute failed: %v", tc.name, err)
		}
		got := out.String()
		for _, w := range tc.want {
			if !strings.Contains(got, w) {
				t.Errorf("%s help output missing %q\n---\n%s", tc.name, w, got)
			}
		}
	}
}
