package main

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

	"iop/packages/go/config"
)

func TestBootstrapPackCreatesNodeArtifacts(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yamlStr := `server:
  listen: "127.0.0.1:39090"
  advertise_host: "edge.example.test"
bootstrap:
  listen: "127.0.0.1:38080"
  artifact_dir: "artifacts"
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	nodeBinary := filepath.Join(dir, "iop-node")
	if err := os.WriteFile(nodeBinary, []byte("fake-node-binary"), 0o755); err != nil {
		t.Fatalf("write node binary: %v", err)
	}
	outDir := filepath.Join(dir, "out")

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{
		"--config", cfgPath,
		"bootstrap", "pack",
		"--node-binary", nodeBinary,
		"--output", outDir,
		"--target", "linux-arm64",
	})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute pack: %v\n%s", err, out.String())
	}

	for _, path := range []string{
		filepath.Join(outDir, "linux-arm64", "iop-node"),
		filepath.Join(outDir, "linux-arm64", "SHA256SUMS"),
		filepath.Join(outDir, "bootstrap", "node.sh"),
		filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"),
	} {
		if _, err := os.Stat(path); err != nil {
			t.Fatalf("expected artifact %s: %v", path, err)
		}
	}
	script, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"))
	if err != nil {
		t.Fatalf("read script: %v", err)
	}
	if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.example.test:38080"`) {
		t.Fatalf("script missing derived artifact URL:\n%s", script)
	}
	if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.example.test:39090"`) {
		t.Fatalf("script missing derived edge addr:\n%s", script)
	}
	universalScript, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node.sh"))
	if err != nil {
		t.Fatalf("read universal script: %v", err)
	}
	if !strings.Contains(string(universalScript), `DEFAULT_TARGET=""`) {
		t.Fatalf("universal script should detect target dynamically:\n%s", universalScript)
	}
	if !strings.Contains(string(universalScript), `detect_target()`) {
		t.Fatalf("universal script missing target detection:\n%s", universalScript)
	}
}

func TestBootstrapPackCreatesWindowsPowerShellArtifact(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yamlStr := `server:
  listen: "127.0.0.1:39090"
  advertise_host: "edge.example.test"
bootstrap:
  listen: "127.0.0.1:38080"
  artifact_dir: "artifacts"
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	nodeBinary := filepath.Join(dir, "iop-node")
	if err := os.WriteFile(nodeBinary, []byte("fake-node-binary"), 0o755); err != nil {
		t.Fatalf("write node binary: %v", err)
	}
	outDir := filepath.Join(dir, "out")

	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{
		"--config", cfgPath,
		"bootstrap", "pack",
		"--node-binary", nodeBinary,
		"--output", outDir,
		"--target", "windows-amd64",
	})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute pack: %v\n%s", err, out.String())
	}

	psScript, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node-windows-amd64.ps1"))
	if err != nil {
		t.Fatalf("read powershell script: %v", err)
	}
	got := string(psScript)
	for _, want := range []string{
		"function Start-IopNode",
		"$Target = 'windows-amd64'",
		"$ArtifactBaseURL = 'http://edge.example.test:38080'",
		"$EdgeAddr = 'edge.example.test:39090'",
		"Start-Process -FilePath $BinFile",
		"port: 19104",
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("powershell script missing %q:\n%s", want, got)
		}
	}
}

func TestNodeRegisterRefreshesExistingBootstrapArtifact(t *testing.T) {
	dir := t.TempDir()
	artifactDir := filepath.Join(dir, "artifacts")
	target := "darwin-arm64"
	targetDir := filepath.Join(artifactDir, target)
	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		t.Fatalf("mkdir target dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(targetDir, "iop-node"), []byte("fake-node-binary"), 0o755); err != nil {
		t.Fatalf("write node artifact: %v", err)
	}

	cfgPath := filepath.Join(dir, "edge.yaml")
	yamlStr := `server:
  listen: "127.0.0.1:39090"
  advertise_host: "edge.deploy.test"
bootstrap:
  listen: "127.0.0.1:38080"
  artifact_dir: "` + artifactDir + `"
nodes: []
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}

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

	script, err := os.ReadFile(filepath.Join(artifactDir, "bootstrap", "node-"+target+".sh"))
	if err != nil {
		t.Fatalf("read refreshed script: %v", err)
	}
	universalScript, err := os.ReadFile(filepath.Join(artifactDir, "bootstrap", "node.sh"))
	if err != nil {
		t.Fatalf("read refreshed universal script: %v", err)
	}
	if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
		t.Fatalf("script missing refreshed artifact URL:\n%s", script)
	}
	if !strings.Contains(string(universalScript), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
		t.Fatalf("universal script missing refreshed artifact URL:\n%s", universalScript)
	}
	if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
		t.Fatalf("script missing refreshed edge addr:\n%s", script)
	}
	if !strings.Contains(string(universalScript), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
		t.Fatalf("universal script missing refreshed edge addr:\n%s", universalScript)
	}
	if !strings.Contains(out.String(), `curl -fsSL http://edge.deploy.test:38080/bootstrap/node.sh | bash -s test-token`) {
		t.Fatalf("unexpected register output:\n%s", out.String())
	}
}

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

	yamlStr := strings.Replace(`server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-a"
    alias: "a"
    token: "token-a"
    adapters:
      {legacy}:
        enabled: true
        profiles:
          default:
            command: "removed-provider"
`, "{legacy}", "cl"+"i", 1)
	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", cfgPath, "config", "check"})
	if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "legacy provider configuration") {
		t.Fatalf("config check error = %v, output=%s", err, out.String())
	}
}

func TestNodeRegisterRejectsDuplicateAliasAndDuplicateToken(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: "a"
    token: "token-a"
    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)
	}

	originalBytes, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read original: %v", err)
	}

	// 1. Try to register node-b with DUPLICATE alias "a"
	root := rootCmd()
	var out bytes.Buffer
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "a", "--token", "token-b"})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected error due to duplicate alias, got nil")
	}

	output := out.String()
	if strings.Contains(output, "Registered node") || strings.Contains(output, "Updated node") || strings.Contains(output, "curl -fsSL") {
		t.Errorf("stdout should not contain success clues on failure, got:\n%s", output)
	}

	// Assert YAML file is byte-for-byte unchanged
	currentBytes, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read current: %v", err)
	}
	if !bytes.Equal(originalBytes, currentBytes) {
		t.Fatal("expected edge.yaml to remain unchanged on validation failure")
	}

	// 2. Try to register node-b with DUPLICATE token "token-a"
	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "b", "--token", "token-a"})
	if err := root.Execute(); err == nil {
		t.Fatalf("expected error due to duplicate token, got nil")
	}

	currentBytes2, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read current2: %v", err)
	}
	if !bytes.Equal(originalBytes, currentBytes2) {
		t.Fatal("expected edge.yaml to remain unchanged on validation failure")
	}

	// 3. Confirm config check still passes on original file
	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("config check failed on original valid file: %v", err)
	}
}

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

	// Create minimal config but with configured artifact base url
	yamlStr := `server:
  listen: "0.0.0.0:9090"
bootstrap:
  artifact_base_url: "http://configured-artifact-server:8080"
`
	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", cfgPath, "node", "register", "node-test", "--target", "darwin-arm64", "--token", "test-token"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute register: %v\n%s", err, out.String())
	}

	output := out.String()
	expectedCmd := "curl -fsSL http://configured-artifact-server:8080/bootstrap/node.sh | bash -s test-token"
	if !strings.Contains(output, expectedCmd) {
		t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
	}

	// Now override artifact base URL with flag
	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "linux-amd64", "--artifact-base-url", "http://flag-artifact-server:9000", "--token", "test-token-2"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute register override: %v\n%s", err, out.String())
	}

	output = out.String()
	expectedCmdOverride := "curl -fsSL http://flag-artifact-server:9000/bootstrap/node.sh | bash -s test-token-2"
	if !strings.Contains(output, expectedCmdOverride) {
		t.Errorf("expected bootstrap command %q, got %q", expectedCmdOverride, output)
	}
}

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

	yamlStr := `server:
  listen: "0.0.0.0:9090"
  advertise_host: "edge.example.test"
bootstrap:
  artifact_base_url: ""
`
	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", cfgPath, "node", "register", "node-test", "--target", "linux-amd64", "--token", "test-token"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute register: %v\n%s", err, out.String())
	}

	expectedCmd := "curl -fsSL http://edge.example.test:18080/bootstrap/node.sh | bash -s test-token"
	if !strings.Contains(out.String(), expectedCmd) {
		t.Errorf("expected bootstrap command %q, got %q", expectedCmd, out.String())
	}
}

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

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

	// 2. node register
	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"node", "register", "node-silicon-ollama", "--adapter", "ollama", "--ollama-base-url", "http://127.0.0.1:11434", "--target", "darwin-arm64", "--artifact-base-url", "http://toki-labs.com:18080", "--token", "test-token"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute register: %v\n%s", err, out.String())
	}

	output := out.String()
	expectedCmd := "curl -fsSL http://toki-labs.com:18080/bootstrap/node.sh | bash -s test-token"
	if !strings.Contains(output, expectedCmd) {
		t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
	}

	// 3. config check
	root = rootCmd()
	out.Reset()
	root.SetOut(&out)
	root.SetErr(&out)
	root.SetArgs([]string{"config", "check"})
	if err := root.Execute(); err != nil {
		t.Fatalf("execute check: %v\n%s", err, out.String())
	}

	if !strings.Contains(out.String(), "OK edge.yaml") {
		t.Fatalf("expected check OK, got %q", out.String())
	}
}

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

	// Write minimal config with server.listen and openai.enabled, omitting other defaults
	yamlStr := `server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true
  provider_id: "test-provider"
`
	if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

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

	// 1. Assert original YAML structure still omits the defaulted fields (preserves raw layout)
	rawBytes, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("read raw config: %v", err)
	}
	rawStr := string(rawBytes)
	badSubstrings := []string{
		"openai:\n  listen:",
		"openai:\n    listen:",
		"openai:\n  adapter:",
		"openai:\n    adapter:",
		"openai:\n  session_id:",
		"openai:\n    session_id:",
		"openai:\n  timeout_sec:",
		"openai:\n    timeout_sec:",
	}
	for _, sub := range badSubstrings {
		// Replace standard newlines with system newlines just in case, but standard is fine.
		if strings.Contains(rawStr, sub) || strings.Contains(rawStr, strings.ReplaceAll(sub, "\n", "\r\n")) {
			t.Errorf("expected raw edge.yaml to still omit default field, but found explicit %q in YAML:\n%s", sub, rawStr)
		}
	}

	// 2. Assert effective OpenAI defaults are resolved correctly (not zeroed out)
	cfgPtr, err := config.LoadEdge(cfgPath)
	if err != nil {
		t.Fatalf("load effective config: %v", err)
	}
	cfg := *cfgPtr

	if cfg.OpenAI.Listen != "0.0.0.0:18081" {
		t.Errorf("expected effective openai.listen to remain 0.0.0.0:18081, got %q", cfg.OpenAI.Listen)
	}
	if cfg.OpenAI.Adapter != "ollama" {
		t.Errorf("expected effective openai.adapter to remain ollama, got %q", cfg.OpenAI.Adapter)
	}
	if cfg.OpenAI.SessionID != "openai" {
		t.Errorf("expected effective openai.session_id to remain openai, got %q", cfg.OpenAI.SessionID)
	}
	if cfg.OpenAI.TimeoutSec != 120 {
		t.Errorf("expected effective openai.timeout_sec to remain 120, got %d", cfg.OpenAI.TimeoutSec)
	}
	if !cfg.OpenAI.StrictOutput {
		t.Errorf("expected effective openai.strict_output to remain true")
	}

	// 3. Assert nodes[] has the new node and config check passes
	if len(cfg.Nodes) != 1 {
		t.Fatalf("expected exactly 1 node record, got %d", len(cfg.Nodes))
	}
	if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Token != "token-a" {
		t.Fatalf("unexpected registered node: %+v", cfg.Nodes[0])
	}

	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("config check failed: %v\n%s", err, out.String())
	}
}

func TestNodesListCommand(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "edge.yaml")
	yamlStr := `server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-x"
    alias: "alias-x"
    token: "token-x"
    adapters:
      ollama:
        enabled: true
        base_url: "http://127.0.0.1:11434"
  - id: "node-y"
    alias: "alias-y"
    token: "token-y"
    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 temp edge.yaml: %v", err)
	}

	root := rootCmd()
	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("nodes list execute failed: %v\n%s", err, out.String())
	}
	got := out.String()

	if !strings.Contains(got, "node-x") || !strings.Contains(got, "node-y") {
		t.Errorf("nodes list output missing configured nodes: %s", got)
	}
	if !strings.Contains(got, "offline (configured)") {
		t.Errorf("nodes list output should explicitly mark nodes as offline, got: %s", got)
	}
}
