package configrefresh_test

import (
	"context"
	"os"
	"path/filepath"
	"testing"

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

func writeYAML(t *testing.T, dir, name, content string) string {
	t.Helper()
	path := filepath.Join(dir, name)
	if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
		t.Fatalf("write %s: %v", name, err)
	}
	return path
}

// buildNormalizedCurrent creates a current config that mimics what a running
// serve process holds after loadRuntimeConfig normalization. Empty logging
// paths are resolved, explicit logging paths are preserved, and bootstrap
// artifact_dir is resolved to the effective runtime path.
func buildNormalizedCurrent(t *testing.T, path string) *config.EdgeConfig {
	t.Helper()
	cfg, err := config.LoadEdge(path)
	if err != nil {
		t.Fatalf("load current: %v", err)
	}
	// Apply the same normalization that loadRuntimeConfig performs.
	// NOTE: explicit relative logging.path is preserved exactly to match
	// edgecmd.ResolveEdgeLogPath – only empty paths are resolved.
	if cfg.Logging.Path == "" {
		cfg.Logging.Path = _normalizedLogPath()
	}
	// Explicit absolute / relative paths are intentionally left untouched.
	cfg.Bootstrap.ArtifactDir = _normalizeArtifactDir(cfg.Bootstrap.ArtifactDir)
	return cfg
}

func _normalizedLogPath() string {
	if exe, err := os.Executable(); err == nil {
		return filepath.Join(filepath.Dir(exe), "logs", "edge.log")
	}
	if wd, err := os.Getwd(); err == nil {
		return filepath.Join(wd, "logs", "edge.log")
	}
	return "logs/edge.log"
}

func _normalizeArtifactDir(dir string) string {
	if dir == "" {
		dir = "artifacts"
	}
	if filepath.IsAbs(dir) {
		return dir
	}
	return filepath.Join(binaryDirForTesting(), dir)
}

func binaryDirForTesting() string {
	if exe, err := os.Executable(); err == nil {
		return filepath.Dir(exe)
	}
	if wd, err := os.Getwd(); err == nil {
		return wd
	}
	return "."
}

const baseEdgeYAML = `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
`

// TestClassifyProviderCapacityApplied verifies S07: provider capacity change is classified as applied.
func TestClassifyProviderCapacityApplied(t *testing.T) {
	dir := t.TempDir()

	currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 8
        max_queue: 4
        queue_timeout_ms: 5000
 `
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)

	ctx := context.Background()
	result, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-s07",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusApplied {
		t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusApplied, result.Status, result.Summary)
	}
	if len(result.Changes) == 0 {
		t.Fatalf("expected at least one change")
	}
	found := false
	for _, c := range result.Changes {
		if c.Class == configrefresh.StatusApplied {
			found = true
		}
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change: %s", c.Path)
		}
	}
	if !found {
		t.Errorf("no applied change found in: %+v", result.Changes)
	}
}

// TestClassifyListenPortRestartRequired verifies S08: listen port change is classified as restart_required.
func TestClassifyListenPortRestartRequired(t *testing.T) {
	dir := t.TempDir()

	currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
	candidateYAML := `
server:
  listen: "0.0.0.0:19090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
 `
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)

	ctx := context.Background()
	result, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-s08",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusRestartRequired {
		t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusRestartRequired, result.Status, result.Summary)
	}
	found := false
	for _, c := range result.Changes {
		if c.Path == "server.listen" && c.Class == configrefresh.StatusRestartRequired {
			found = true
		}
	}
	if !found {
		t.Errorf("server.listen restart_required change not found in: %+v", result.Changes)
	}
}

// TestRefreshInvalidYAMLRejected verifies S09: invalid YAML is classified as rejected.
func TestRefreshInvalidYAMLRejected(t *testing.T) {
	dir := t.TempDir()
	currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
	invalidPath := writeYAML(t, dir, "invalid.yaml", "nodes: [\n  - bad: [unterminated")

	current := buildNormalizedCurrent(t, currentPath)

	ctx := context.Background()
	result, candidate, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: invalidPath,
		RequestID:  "test-s09-yaml",
	})
	if err != nil {
		t.Fatalf("Evaluate should not return error for rejected config: %v", err)
	}
	if result.Status != configrefresh.StatusRejected {
		t.Errorf("expected status=%q, got %q", configrefresh.StatusRejected, result.Status)
	}
	if candidate != nil {
		t.Errorf("expected nil candidate for rejected result")
	}
}

func TestClassifyModelCatalogProviderMappingApplied(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
      - id: "prov-b"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
models:
  - id: "qwen3.6:35b"
    display_name: "Qwen"
    providers:
      prov-a: "llama3.1"
 `
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
      - id: "prov-b"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
models:
  - id: "qwen3.6:35b"
    display_name: "Qwen"
    providers:
      prov-b: "llama3.1"
 `
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)

	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-model-catalog",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=%q, got %q changes=%+v", configrefresh.StatusApplied, result.Status, result.Changes)
	}
	found := false
	for _, change := range result.Changes {
		if change.Path == `models["qwen3.6:35b"].providers` && change.Class == configrefresh.StatusApplied {
			found = true
		}
	}
	if !found {
		t.Fatalf("model catalog providers change not found in %+v", result.Changes)
	}
}

func TestClassifyProviderStructuralChangeRestartRequired(t *testing.T) {
	dir := t.TempDir()
	currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "vllm"
        category: "api"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
 `
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)

	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-provider-structural",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusRestartRequired {
		t.Fatalf("expected status=%q, got %q changes=%+v", configrefresh.StatusRestartRequired, result.Status, result.Changes)
	}
	found := false
	for _, change := range result.Changes {
		if change.Path == `nodes[].providers["prov-a"].type` && change.Class == configrefresh.StatusRestartRequired {
			found = true
		}
	}
	if !found {
		t.Fatalf("provider structural type change not found in %+v", result.Changes)
	}
}

// TestRefreshInvalidProviderRejected verifies S09: invalid provider config is classified as rejected.
func TestRefreshInvalidProviderRejected(t *testing.T) {
	dir := t.TempDir()
	currentPath := writeYAML(t, dir, "current.yaml", baseEdgeYAML)
	// Invalid: shared validator rejects enabled openai_compat without endpoint.
	invalidProviderYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      openai_compat:
        enabled: true
        endpoint: ""
 `
	invalidPath := writeYAML(t, dir, "invalid_provider.yaml", invalidProviderYAML)

	current := buildNormalizedCurrent(t, currentPath)

	ctx := context.Background()
	result, candidate, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: invalidPath,
		RequestID:  "test-s09-provider",
	})
	if err != nil {
		t.Fatalf("Evaluate should not return error for rejected config: %v", err)
	}
	if result.Status != configrefresh.StatusRejected {
		t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusRejected, result.Status, result.Summary)
	}
	if candidate != nil {
		t.Errorf("expected nil candidate for rejected result")
	}
}

// TestClassifyServeNormalizedRefreshApplied verifies the archived bug:
// when current config is serve-normalized (logging.path absolute,
// bootstrap.artifact_dir absolute) and candidate uses source-level defaults
// (empty logging.path, relative bootstrap.artifact_dir) with only provider
// capacity changes, the result must be applied, NOT restart_required.
func TestClassifyServeNormalizedRefreshApplied(t *testing.T) {
	dir := t.TempDir()

	// Source-level YAML with empty logging.path and relative artifact_dir.
	// Only capacity differs (2 → 8).
	sourceYAML := `
server:
  listen: "0.0.0.0:9090"
bootstrap:
  artifact_dir: artifacts
logging:
  level: info
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
 `
	// Candidate: only capacity changes to 8; logging/artifact_dir are same source-level defaults.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
bootstrap:
  artifact_dir: artifacts
logging:
  level: info
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 8
        max_queue: 4
        queue_timeout_ms: 5000
 `

	currentPath := writeYAML(t, dir, "current.yaml", sourceYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	// Build current as serve-normalized: explicit absolute paths.
	current := buildNormalizedCurrent(t, currentPath)

	// Verify that the current config actually has normalized paths.
	if current.Logging.Path == "" {
		t.Fatalf("expected normalized logging.path, got empty")
	}
	if current.Bootstrap.ArtifactDir == "" {
		t.Fatalf("expected normalized bootstrap.artifact_dir, got empty")
	}

	// Evaluate the candidate via LoadCandidate (which now applies normalization).
	ctx := context.Background()
	result, candidate, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-serve-normalized",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}

	// Key assertion: capacity-only refresh must NOT be restart_required.
	if result.Status != configrefresh.StatusApplied {
		t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusApplied, result.Status, result.Summary)
	}

	// Ensure no restart_required changes for logging or bootstrap.artifact_dir.
	for _, c := range result.Changes {
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change for capacity-only refresh: %s (prev=%s, next=%s)", c.Path, c.Previous, c.Next)
		}
	}

	// Verify the capacity change is captured as applied.
	foundCapacity := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-a"].capacity` && c.Class == configrefresh.StatusApplied {
			foundCapacity = true
			if c.Previous != "2" || c.Next != "8" {
				t.Errorf("expected capacity change 2→8, got %s→%s", c.Previous, c.Next)
			}
		}
	}
	if !foundCapacity {
		t.Fatalf("capacity change not found in applied changes: %+v", result.Changes)
	}

	// Verify the candidate is also normalized.
	if candidate == nil {
		t.Fatalf("expected non-nil candidate")
	}
	if candidate.Logging.Path == "" {
		t.Fatalf("expected normalized candidate logging.path, got empty")
	}
}

// TestClassifyExplicitRelativeLoggingPathNoRestartRequired verifies that when
// both current and candidate use the same explicit relative logging.path
// (e.g. "./logs/edge.log") and only provider capacity differs, the refresh
// is classified as applied – NOT restart_required.
//
// This is the regression test for the archived bug where applyRuntimeNormalization
// called filepath.Clean on explicit relative paths, producing a false diff
// against the serve-side value that preserves "./logs/edge.log" unchanged.
func TestClassifyExplicitRelativeLoggingPathNoRestartRequired(t *testing.T) {
	dir := t.TempDir()

	// Current config with explicit relative logging.path – mimics what serve
	// holds in memory after loading YAML with logging.path: "./logs/edge.log".
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
logging:
  path: "./logs/edge.log"
  level: info
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
 `

	// Candidate: same explicit relative logging.path, only capacity changes.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
logging:
  path: "./logs/edge.log"
  level: info
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 8
        max_queue: 4
        queue_timeout_ms: 5000
 `

	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	// buildNormalizedCurrent preserves the explicit relative logging.path.
	current := buildNormalizedCurrent(t, currentPath)

	// Verify the current config still has the explicit relative path.
	if current.Logging.Path != "./logs/edge.log" {
		t.Fatalf("expected current logging.path to remain './logs/edge.log', got %q", current.Logging.Path)
	}

	ctx := context.Background()
	result, candidate, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-explicit-relative-logpath",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}

	// Key assertion: capacity-only refresh with same explicit relative path must be applied.
	if result.Status != configrefresh.StatusApplied {
		t.Errorf("expected status=%q, got %q (summary: %s)", configrefresh.StatusApplied, result.Status, result.Summary)
	}

	// Ensure no restart_required changes at all.
	for _, c := range result.Changes {
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change for capacity-only refresh: %s (prev=%s, next=%s)", c.Path, c.Previous, c.Next)
		}
	}

	// Verify the capacity change is captured as applied.
	foundCapacity := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-a"].capacity` && c.Class == configrefresh.StatusApplied {
			foundCapacity = true
			if c.Previous != "2" || c.Next != "8" {
				t.Errorf("expected capacity change 2→8, got %s→%s", c.Previous, c.Next)
			}
		}
	}
	if !foundCapacity {
		t.Fatalf("capacity change not found in applied changes: %+v", result.Changes)
	}

	// Candidate should also preserve the explicit relative path.
	if candidate == nil {
		t.Fatalf("expected non-nil candidate")
	}
	if candidate.Logging.Path != "./logs/edge.log" {
		t.Errorf("expected candidate logging.path to remain './logs/edge.log', got %q", candidate.Logging.Path)
	}
}

// TestClassifyDryRunTwiceNoDiff verifies that after apply, re-running dry-run
// on the same candidate produces no diff.
func TestClassifyDryRunTwiceNoDiff(t *testing.T) {
	dir := t.TempDir()

	baseYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
 `
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 4
 `

	currentPath := writeYAML(t, dir, "current.yaml", baseYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)
	current := buildNormalizedCurrent(t, currentPath)

	ctx := context.Background()

	// First dry-run: should detect capacity change.
	result1, cand1, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-first-dryrun",
	})
	if err != nil {
		t.Fatalf("first Evaluate: %v", err)
	}
	if result1.Status != configrefresh.StatusApplied {
		t.Fatalf("first dry-run: expected status=%q, got %q", configrefresh.StatusApplied, result1.Status)
	}

	// Second dry-run with the same candidate loaded again.
	// The candidate path still exists, so LoadCandidate re-reads the same YAML.
	result2, cand2, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-second-dryrun",
	})
	if err != nil {
		t.Fatalf("second Evaluate: %v", err)
	}

	// Both results should be identical (same changes).
	if len(result1.Changes) != len(result2.Changes) {
		t.Fatalf("expected same number of changes, got %d vs %d: result1=%+v result2=%+v",
			len(result1.Changes), len(result2.Changes), result1.Changes, result2.Changes)
	}

	// Both candidates should be normalized identically.
	if cand1 == nil || cand2 == nil {
		t.Fatalf("expected non-nil candidates")
	}
	if cand1.Logging.Path != cand2.Logging.Path {
		t.Fatalf("candidate logging.path mismatch: %s vs %s", cand1.Logging.Path, cand2.Logging.Path)
	}
}

// TestClassifyNodeRuntimeConcurrencyApplied verifies S15: node runtime
// concurrency is applied live.
func TestClassifyNodeRuntimeConcurrencyApplied(t *testing.T) {
	dir := t.TempDir()

	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    runtime:
      concurrency: 2
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)

	concurrencyOnlyYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    runtime:
      concurrency: 4
`
	concurrencyOnlyPath := writeYAML(t, dir, "concurrency.yaml", concurrencyOnlyYAML)

	current := buildNormalizedCurrent(t, currentPath)
	ctx := context.Background()

	// concurrency-only change → applied.
	result, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: concurrencyOnlyPath,
		RequestID:  "test-s15-conc",
	})
	if err != nil {
		t.Fatalf("Evaluate concurrency: %v", err)
	}
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected concurrency change status=applied, got %q (summary: %s)", result.Status, result.Summary)
	}
	foundConc := false
	for _, c := range result.Changes {
		if c.Path == `nodes["node-1"].runtime.concurrency` {
			foundConc = true
			if c.Class != configrefresh.StatusApplied {
				t.Errorf("expected concurrency class=applied, got %q", c.Class)
			}
		}
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change for concurrency-only diff: %s", c.Path)
		}
	}
	if !foundConc {
		t.Errorf("concurrency change path not found in: %+v", result.Changes)
	}

}

// TestClassifyProviderEnabledLiveApply verifies that toggling providers[].enabled
// is classified as StatusApplied (live apply, no restart required).
func TestClassifyProviderEnabledLiveApply(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	// Candidate disables prov-a.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        enabled: false
`
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)
	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-enabled-liveapply",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=applied for enabled toggle, got %q", result.Status)
	}
	foundEnabled := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-a"].enabled` {
			foundEnabled = true
			if c.Class != configrefresh.StatusApplied {
				t.Errorf("providers[].enabled class: got %q, want applied", c.Class)
			}
			if c.Previous != "true" || c.Next != "false" {
				t.Errorf("providers[].enabled change: prev=%q next=%q", c.Previous, c.Next)
			}
		}
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change for enabled-only diff: %s", c.Path)
		}
	}
	if !foundEnabled {
		t.Errorf("providers[].enabled change path not found in: %+v", result.Changes)
	}
}

// TestClassifyProviderFirstEndpointOnlyRestartRequired verifies that changing
// only the provider endpoint (type/category unchanged) is classified as
// restart_required and surfaces the correct path.
func TestClassifyProviderFirstEndpointOnlyRestartRequired(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    providers:
      - id: "prov-oc"
        type: "openai_compat"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        models: ["model-a"]
        capacity: 2
`
	// Only endpoint changes — type, category, models, capacity are identical.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    providers:
      - id: "prov-oc"
        type: "openai_compat"
        category: "api"
        endpoint: "http://127.0.0.1:9000/v1"
        models: ["model-a"]
        capacity: 2
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)
	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-endpoint-only",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusRestartRequired {
		t.Fatalf("expected status=restart_required for endpoint-only change, got %q; changes=%+v", result.Status, result.Changes)
	}
	found := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-oc"].endpoint` && c.Class == configrefresh.StatusRestartRequired {
			found = true
		}
	}
	if !found {
		t.Errorf("expected restart_required change for providers[].endpoint; changes=%+v", result.Changes)
	}
}

// TestClassifyProviderFirstHeadersChangeRestartRequired verifies that changing
// a provider headers map (type/category unchanged) is classified as
// restart_required — covers map deep-diff path.
func TestClassifyProviderFirstHeadersChangeRestartRequired(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    providers:
      - id: "prov-oc"
        type: "openai_compat"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        headers:
          Authorization: "Bearer old-token"
        models: ["model-a"]
        capacity: 2
`
	// Only headers change.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    providers:
      - id: "prov-oc"
        type: "openai_compat"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        headers:
          Authorization: "Bearer new-token"
        models: ["model-a"]
        capacity: 2
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)
	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-headers-only",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusRestartRequired {
		t.Fatalf("expected status=restart_required for headers change, got %q; changes=%+v", result.Status, result.Changes)
	}
	found := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-oc"].headers` && c.Class == configrefresh.StatusRestartRequired {
			found = true
		}
	}
	if !found {
		t.Errorf("expected restart_required change for providers[].headers; changes=%+v", result.Changes)
	}
}

func TestClassifyProviderFirstExecutionFieldRestartRequired(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      vllm:
        enabled: true
        endpoint: "http://127.0.0.1:8000/v1"
    providers:
      - id: "prov-vllm"
        type: "vllm"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        models: ["model-a"]
        capacity: 2
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	// Candidate changes endpoint (provider-first execution field) → restart required.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      vllm:
        enabled: true
        endpoint: "http://127.0.0.1:8080/v1"
    providers:
      - id: "prov-vllm"
        type: "openai_compat"
        category: "api"
        endpoint: "http://127.0.0.1:8080/v1"
        models: ["model-a"]
        capacity: 2
`
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)
	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-provfirst-restart",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusRestartRequired {
		t.Fatalf("expected status=restart_required for provider type change, got %q", result.Status)
	}
	hasTypeRestart := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-vllm"].type` && c.Class == configrefresh.StatusRestartRequired {
			hasTypeRestart = true
		}
	}
	if !hasTypeRestart {
		t.Errorf("expected restart_required for providers[].type change; changes: %+v", result.Changes)
	}
}

// TestClassifyProviderPriorityApplied verifies that provider priority change
// is classified as StatusApplied (live apply, no restart required).
func TestClassifyProviderPriorityApplied(t *testing.T) {
	dir := t.TempDir()
	currentYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        priority: 5
`
	currentPath := writeYAML(t, dir, "current.yaml", currentYAML)
	// Candidate only changes priority.
	candidateYAML := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
      cli:
        enabled: true
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        adapter: "cli"
        models: ["llama3.1"]
        capacity: 2
        priority: 1
`
	candidatePath := writeYAML(t, dir, "candidate.yaml", candidateYAML)

	current := buildNormalizedCurrent(t, currentPath)
	result, _, err := configrefresh.Evaluate(context.Background(), current, configrefresh.Request{
		Mode:       configrefresh.ModeDryRun,
		ConfigPath: candidatePath,
		RequestID:  "test-priority-applied",
	})
	if err != nil {
		t.Fatalf("Evaluate: %v", err)
	}
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=applied for priority-only change, got %q (summary: %s)", result.Status, result.Summary)
	}
	foundPriority := false
	for _, c := range result.Changes {
		if c.Path == `nodes[].providers["prov-a"].priority` {
			foundPriority = true
			if c.Class != configrefresh.StatusApplied {
				t.Errorf("providers[].priority class: got %q, want applied", c.Class)
			}
			if c.Previous != "5" || c.Next != "1" {
				t.Errorf("providers[].priority change: prev=%q next=%q", c.Previous, c.Next)
			}
		}
		if c.Class == configrefresh.StatusRestartRequired {
			t.Errorf("unexpected restart_required change for priority-only diff: %s", c.Path)
		}
	}
	if !foundPriority {
		t.Errorf("providers[].priority change path not found in: %+v", result.Changes)
	}
}
