package edgecmd

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

	"gopkg.in/yaml.v3"

	"iop/apps/edge/internal/configrefresh"
	"iop/packages/go/config"
)

const minimalEdgeYAML = "server:\n  listen: \"0.0.0.0:9090\"\n"

func writeMinimalEdgeYAML(t *testing.T, path string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(minimalEdgeYAML), 0o600); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

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

	root := NewRoot(Options{})
	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()
	if !strings.Contains(got, "advertise_host: ") || !strings.Contains(got, " (auto)") {
		t.Errorf("expected advertise_host with (auto) source, got:\n%s", got)
	}
	expectedArtifact := "artifact_base_url: " + defaultArtifactBaseURL(resolveAdvertiseHost("").Host) + " (derived)"
	if !strings.Contains(got, expectedArtifact) {
		t.Errorf("expected derived artifact_base_url %q, got:\n%s", expectedArtifact, got)
	}
	expectedBootstrap := "node_bootstrap: " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host))
	if !strings.Contains(got, expectedBootstrap) {
		t.Errorf("expected node_bootstrap %q, got:\n%s", expectedBootstrap, got)
	}
	if !strings.Contains(got, "openai_url: (disabled)") {
		t.Errorf("expected openai_url (disabled), got:\n%s", got)
	}
	if !strings.Contains(got, "configured_nodes: 0 (none)") {
		t.Errorf("expected configured_nodes 0 (none), got:\n%s", got)
	}
	if !strings.Contains(got, "connected_nodes: offline") {
		t.Errorf("expected connected_nodes offline placeholder, got:\n%s", got)
	}
}

func TestResolveEdgeLogPathFallsBackToBinaryDirLogsEdgeLog(t *testing.T) {
	got, err := ResolveEdgeLogPath("")
	if err != nil {
		t.Fatalf("ResolveEdgeLogPath: %v", err)
	}
	want := filepath.Join(binaryDir(), "logs", "edge.log")
	if got != want {
		t.Fatalf("ResolveEdgeLogPath empty: got %q, want %q", got, want)
	}
}

func TestResolveEdgeLogPathRespectsExplicitPath(t *testing.T) {
	explicit := filepath.Join(t.TempDir(), "custom.log")
	got, err := ResolveEdgeLogPath(explicit)
	if err != nil {
		t.Fatalf("ResolveEdgeLogPath: %v", err)
	}
	if got != explicit {
		t.Fatalf("ResolveEdgeLogPath explicit: got %q, want %q", got, explicit)
	}
}

func TestResolveAdvertiseHostPrefersExplicit(t *testing.T) {
	got := resolveAdvertiseHost("explicit.example.test")
	if got.Host != "explicit.example.test" || got.Source != "explicit" {
		t.Fatalf("explicit advertise: got %+v", got)
	}
}

func TestResolveAdvertiseHostFallsBackToAuto(t *testing.T) {
	got := resolveAdvertiseHost("")
	if got.Host == "" {
		t.Fatal("auto advertise host empty")
	}
	if got.Source != "auto" {
		t.Fatalf("expected auto source, got %q", got.Source)
	}
}

func TestAddressForCombinesAdvertiseHostWithListenPort(t *testing.T) {
	if got := addressFor("10.0.0.1", "0.0.0.0:9090"); got != "10.0.0.1:9090" {
		t.Errorf("addressFor: got %q", got)
	}
	if got := addressFor("host", ""); got != "" {
		t.Errorf("addressFor empty listen: got %q", got)
	}
}

func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
	origGen := tokenGenerator
	defer func() { tokenGenerator = origGen }()
	tokenGenerator = func() string {
		return "mocked-token-1234"
	}

	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	writeMinimalEdgeYAML(t, cfgPath)

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

	output := out.String()
	expectedCmd := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s mocked-token-1234"
	if strings.TrimSpace(output) != expectedCmd {
		t.Errorf("expected only bootstrap command %q, got %q", expectedCmd, output)
	}

	data, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read persisted config: %v", err)
	}
	var cfg config.EdgeConfig
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		t.Fatalf("unmarshal config: %v", err)
	}

	if len(cfg.Nodes) != 1 {
		t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
	}
	if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Alias != "alias-a" || cfg.Nodes[0].Token != "mocked-token-1234" {
		t.Fatalf("unexpected node state in YAML: %+v", cfg.Nodes[0])
	}

	root = NewRoot(Options{})
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--token", "explicit-token-abc", "--adapter", "ollama", "--ollama-base-url", "http://ollama-host:11434"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute update node-a: %v\n%s", err, out.String())
	}

	output = out.String()
	expectedCmd2 := "curl -fsSL " + universalBootstrapScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host)) + " | bash -s explicit-token-abc"
	if strings.TrimSpace(output) != expectedCmd2 {
		t.Errorf("expected only bootstrap command %q, got %q", expectedCmd2, output)
	}

	data, err = os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read persisted config: %v", err)
	}
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		t.Fatalf("unmarshal config: %v", err)
	}

	if len(cfg.Nodes) != 1 {
		t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
	}
	if cfg.Nodes[0].Token != "explicit-token-abc" || !cfg.Nodes[0].Adapters.Ollama.Enabled || cfg.Nodes[0].Adapters.Ollama.BaseURL != "http://ollama-host:11434" {
		t.Fatalf("unexpected node state after explicit token update: %+v", cfg.Nodes[0])
	}
}

func TestNodeRegisterOutputsPowerShellBootstrapForWindowsTarget(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	artifactDir := filepath.Join(dir, "artifacts")
	yamlStr := "server:\n  listen: \"0.0.0.0:9090\"\nbootstrap:\n  artifact_dir: \"" + strings.ReplaceAll(artifactDir, "\\", "\\\\") + "\"\n"
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}

	root := NewRoot(Options{})
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-win", "--target", "windows-amd64", "--token", "win-token-123"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute register node-win: %v\n%s", err, out.String())
	}

	expectedCmd := "Invoke-RestMethod " + bootstrapPowerShellScriptURL(defaultArtifactBaseURL(resolveAdvertiseHost("").Host), "windows-amd64") + " | Invoke-Expression; Start-IopNode 'win-token-123'"
	if strings.TrimSpace(out.String()) != expectedCmd {
		t.Errorf("expected only bootstrap command %q, got %q", expectedCmd, out.String())
	}
	if _, err := os.Stat(filepath.Join(artifactDir, "bootstrap", "node-windows-amd64.ps1")); err != nil {
		t.Fatalf("expected powershell bootstrap script to be refreshed: %v", err)
	}
}

func TestNodesListPrintsConfiguredProviders(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-gpu-01"
    alias: "gpu-node"
    token: "token-1"
    adapters:
      vllm_instances:
        - name: "vllm-gpu"
          enabled: true
          endpoint: "http://127.0.0.1:8000/v1"
          capacity: 4
    providers:
      - id: "vllm-gpu"
        type: "vllm"
        category: "api"
        adapter: "vllm-gpu"
        models:
          - "nvidia/Qwen3.6-35B-A3B-NVFP4"
        capacity: 4
        lifecycle_capabilities:
          - "list_models"
          - "load_model"
`
	if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil {
		t.Fatalf("write %s: %v", cfgPath, err)
	}

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

	got := out.String()
	for _, want := range []string{
		"node-gpu-01",
		"providers: vllm-gpu",
		"type=vllm/api",
		"models=nvidia/Qwen3.6-35B-A3B-NVFP4",
		"capacity=4",
		"lifecycle=list_models,load_model",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("nodes list output missing %q:\n%s", want, got)
		}
	}
}

func TestPatchYAMLPreservesArtifactBootstrapField(t *testing.T) {
	origYAML := `server:
  listen: "0.0.0.0:9090"
bootstrap:
  artifact_base_url: "http://old.example.com/artifacts"
`
	cfg := &config.EdgeConfig{
		Bootstrap: config.EdgeBootstrapConf{
			ArtifactBaseURL: "http://example.com/artifacts",
		},
	}

	patched, err := patchYAML([]byte(origYAML), cfg)
	if err != nil {
		t.Fatalf("patchYAML: %v", err)
	}

	var doc struct {
		Server struct {
			Listen string `yaml:"listen"`
		} `yaml:"server"`
		Bootstrap struct {
			ArtifactBaseURL string `yaml:"artifact_base_url"`
		} `yaml:"bootstrap"`
	}

	if err := yaml.Unmarshal(patched, &doc); err != nil {
		t.Fatalf("unmarshal patched: %v", err)
	}

	if doc.Server.Listen != "0.0.0.0:9090" {
		t.Errorf("server.listen was modified: %q", doc.Server.Listen)
	}
	if doc.Bootstrap.ArtifactBaseURL != "http://example.com/artifacts" {
		t.Errorf("artifact_base_url = %q", doc.Bootstrap.ArtifactBaseURL)
	}
}

func TestValidateEdgeConfig_VllmLegacyEmptyEndpointRejected(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					Vllm: config.VllmConf{Enabled: true, Endpoint: ""},
				},
			},
		},
	}
	err := validateEdgeConfig(cfg)
	if err == nil {
		t.Fatal("expected error for enabled vllm with empty endpoint")
	}
}

func TestValidateEdgeConfig_VllmInstanceEmptyEndpointRejected(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					VllmInstances: []config.VllmInstanceConf{
						{Name: "a100", Enabled: true, Endpoint: ""},
					},
				},
			},
		},
	}
	err := validateEdgeConfig(cfg)
	if err == nil {
		t.Fatal("expected error for enabled vllm instance with empty endpoint")
	}
}

func TestValidateEdgeConfig_AllAdaptersDisabledRejected(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					Ollama: config.OllamaConf{Enabled: false},
					Vllm:   config.VllmConf{Enabled: false},
					OllamaInstances: []config.OllamaInstanceConf{
						{Name: "off", Enabled: false, BaseURL: "http://127.0.0.1:11434"},
					},
					VllmInstances: []config.VllmInstanceConf{
						{Name: "off", Enabled: false, Endpoint: "http://10.0.0.5:8000"},
					},
				},
			},
		},
	}
	err := validateEdgeConfig(cfg)
	if err == nil {
		t.Fatal("expected error when all adapters are disabled")
	}
}

func TestValidateEdgeConfig_OllamaInstanceEnabledAccepted(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					OllamaInstances: []config.OllamaInstanceConf{
						{Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
					},
				},
			},
		},
	}
	if err := validateEdgeConfig(cfg); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestValidateEdgeConfig_OpenAICompatInstanceEnabled(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					OpenAICompatInstances: []config.OpenAICompatInstanceConf{
						{Name: "lemonade", Enabled: true, Endpoint: "http://127.0.0.1:13305"},
					},
				},
			},
		},
	}
	if err := validateEdgeConfig(cfg); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestValidateEdgeConfig_OpenAICompatInstanceEmptyEndpointRejected(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					OpenAICompatInstances: []config.OpenAICompatInstanceConf{
						{Name: "lemonade", Enabled: true, Endpoint: ""},
					},
				},
			},
		},
	}
	err := validateEdgeConfig(cfg)
	if err == nil {
		t.Fatal("expected error for enabled openai_compat instance with empty endpoint")
	}
}

// TestConfigRefreshCommandPostsModeAndConfigPath verifies that the CLI refresh
// command sends a valid JSON request to the admin API and prints the JSON result.
func TestConfigRefreshCommandPostsModeAndConfigPath(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	writeMinimalEdgeYAML(t, cfgPath)

	expectedResult := configrefresh.Result{
		Status:  configrefresh.StatusApplied,
		Mode:    configrefresh.ModeDryRun,
		Summary: "no changes detected",
		Changes: []configrefresh.Change{},
	}

	var capturedReq configrefresh.Request
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := json.NewDecoder(r.Body).Decode(&capturedReq); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(expectedResult)
	}))
	defer ts.Close()

	// Strip "http://" prefix for --addr flag.
	addr := strings.TrimPrefix(ts.URL, "http://")

	root := NewRoot(Options{})
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "config", "refresh",
		"--mode", "dry-run",
		"--addr", addr,
		"--config-path", cfgPath,
	})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}

	if capturedReq.Mode != configrefresh.ModeDryRun {
		t.Errorf("expected mode=%q, got %q", configrefresh.ModeDryRun, capturedReq.Mode)
	}
	if capturedReq.ConfigPath != cfgPath {
		t.Errorf("expected config_path=%q, got %q", cfgPath, capturedReq.ConfigPath)
	}
	if !strings.Contains(out.String(), string(configrefresh.StatusApplied)) {
		t.Errorf("expected status in output; got:\n%s", out.String())
	}
}

// TestConfigRefreshCommandPrintsChangedItems verifies SDD S11 on the CLI
// surface: the refresh command prints the changed node/provider/model report
// fields returned by the admin API so an operator can confirm what changed.
func TestConfigRefreshCommandPrintsChangedItems(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	writeMinimalEdgeYAML(t, cfgPath)

	result := configrefresh.Result{
		Status:               configrefresh.StatusApplied,
		Mode:                 configrefresh.ModeDryRun,
		Summary:              "all changes can be applied without restart",
		Changes:              []configrefresh.Change{},
		ChangedNodes:         []string{},
		ChangedProviders:     []string{"prov-a"},
		ChangedModels:        []string{"qwen3.6:35b"},
		RestartRequiredPaths: []string{},
	}

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(result)
	}))
	defer ts.Close()
	addr := strings.TrimPrefix(ts.URL, "http://")

	root := NewRoot(Options{})
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "config", "refresh",
		"--mode", "dry-run",
		"--addr", addr,
		"--config-path", cfgPath,
	})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute: %v\n%s", err, out.String())
	}

	// Decode the printed JSON back so the assertion does not depend on key order.
	var printed configrefresh.Result
	if err := json.Unmarshal(out.Bytes(), &printed); err != nil {
		t.Fatalf("decode printed output: %v\n%s", err, out.String())
	}
	if len(printed.ChangedProviders) != 1 || printed.ChangedProviders[0] != "prov-a" {
		t.Errorf("expected changed_providers=[prov-a] in CLI output, got %v", printed.ChangedProviders)
	}
	if len(printed.ChangedModels) != 1 || printed.ChangedModels[0] != "qwen3.6:35b" {
		t.Errorf("expected changed_models=[qwen3.6:35b] in CLI output, got %v", printed.ChangedModels)
	}
}

// TestConfigRefreshCommandRejectsInvalidMode verifies that an invalid --mode
// flag returns an error before sending any HTTP request.
func TestConfigRefreshCommandRejectsInvalidMode(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	writeMinimalEdgeYAML(t, cfgPath)

	root := NewRoot(Options{})
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "config", "refresh",
		"--mode", "invalid-mode",
		"--addr", "127.0.0.1:0",
		"--config-path", cfgPath,
	})
	err := root.Execute()
	if err == nil {
		t.Fatalf("expected error for invalid mode, got nil\n%s", out.String())
	}
	if !strings.Contains(err.Error(), "invalid --mode") {
		t.Errorf("expected 'invalid --mode' in error, got: %v", err)
	}
}

func TestValidateEdgeConfig_OpenAICompatLegacyEmptyEndpointRejected(t *testing.T) {
	cfg := &config.EdgeConfig{
		Nodes: []config.NodeDefinition{
			{
				ID:    "node-1",
				Alias: "test",
				Token: "tok",
				Adapters: config.AdaptersConf{
					OpenAICompat: config.OpenAICompatConf{Enabled: true, Endpoint: ""},
				},
			},
		},
	}
	err := validateEdgeConfig(cfg)
	if err == nil {
		t.Fatal("expected error for enabled legacy openai_compat with empty endpoint")
	}
}
