package config_test

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

	"iop/packages/go/config"
)

// TestLoadEdgeModelExecutionPresetOneOf covers the one-of admission rule for
// ModelCatalogEntry: exactly one of providers or execution_preset must be set,
// preset ids must resolve to an execution_presets[] entry, and existing
// provider-only fixtures must keep working unchanged.
func TestLoadEdgeModelExecutionPresetOneOf(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")

	// ---- Happy path: provider-only (existing behavior) ----
	t.Run("provider-only entry loads unchanged", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "qwen3.6:35b"
    providers:
      vllm-gpu: "nvidia/Qwen3.6-35B"
nodes:
  - id: "node-gpu-01"
    providers:
      - id: "vllm-gpu"
        type: "vllm"
        category: "api"
        models:
          - "nvidia/Qwen3.6-35B"
        capacity: 4
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		cfg, err := config.LoadEdge(f)
		if err != nil {
			t.Fatalf("load: %v", err)
		}
		if len(cfg.Models) != 1 {
			t.Fatalf("expected 1 model, got %d", len(cfg.Models))
		}
		if cfg.Models[0].ExecutionPreset != "" {
			t.Errorf("provider-only entry should not have execution_preset set, got %q", cfg.Models[0].ExecutionPreset)
		}
		if len(cfg.Models[0].Providers) != 1 {
			t.Errorf("expected 1 provider, got %d", len(cfg.Models[0].Providers))
		}
	})

	// ---- Happy path: preset-only (virtual model) ----
	t.Run("preset-only entry loads as virtual model", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "virtual-model"
    execution_preset: "fast-path"
execution_presets:
  - id: "fast-path"
    selector:
      model: "virtual-model"
    allowed_modes:
      - "direct"
    routes:
      direct:
        stages: []
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		cfg, err := config.LoadEdge(f)
		if err != nil {
			t.Fatalf("load: %v", err)
		}
		if len(cfg.Models) != 1 {
			t.Fatalf("expected 1 model, got %d", len(cfg.Models))
		}
		m := cfg.Models[0]
		if m.ID != "virtual-model" {
			t.Errorf("model id = %q, want virtual-model", m.ID)
		}
		if m.ExecutionPreset != "fast-path" {
			t.Errorf("execution_preset = %q, want fast-path", m.ExecutionPreset)
		}
		if len(m.Providers) != 0 {
			t.Errorf("virtual model should have empty providers, got %v", m.Providers)
		}
	})

	// ---- Error: both providers and execution_preset set ----
	t.Run("both providers and execution_preset rejected", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "confused-model"
    execution_preset: "fast-path"
    providers:
      prov-a: "model-a"
execution_presets:
  - id: "fast-path"
    selector:
      model: "model-a"
    allowed_modes:
      - "direct"
    routes:
      direct:
        stages: []
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		_, err := config.LoadEdge(f)
		if err == nil {
			t.Fatal("expected error for both providers and execution_preset set")
		}
		if !strings.Contains(err.Error(), "exactly one of providers or execution_preset must be set") {
			t.Fatalf("expected one-of error, got %v", err)
		}
	})

	// ---- Error: neither providers nor execution_preset ----
	t.Run("neither providers nor execution_preset rejected", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "empty-model"
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		_, err := config.LoadEdge(f)
		if err == nil {
			t.Fatal("expected error for neither providers nor execution_preset")
		}
		if !strings.Contains(err.Error(), "providers must not be empty") {
			t.Fatalf("expected providers must not be empty error, got %v", err)
		}
	})

	// ---- Error: dangling execution_preset id ----
	t.Run("dangling execution_preset id rejected", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "dangling-model"
    execution_preset: "non-existent-preset"
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		_, err := config.LoadEdge(f)
		if err == nil {
			t.Fatal("expected error for dangling execution_preset id")
		}
		if !strings.Contains(err.Error(), "execution_preset") && !strings.Contains(err.Error(), "does not match any execution_presets") {
			t.Fatalf("expected dangling preset error, got %v", err)
		}
	})

	// ---- Compatibility: mixed catalog with both provider-only and preset-only ----
	t.Run("mixed catalog with provider-only and preset-only entries", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "qwen3.6:35b"
    providers:
      vllm-gpu: "nvidia/Qwen3.6-35B"
  - id: "virtual-light"
    execution_preset: "review-path"
execution_presets:
  - id: "review-path"
    selector:
      model: "qwen3.6:35b"
    allowed_modes:
      - "light"
    routes:
      light:
        stages:
          - role: "local"
            model: "qwen3.6:35b"
          - role: "review"
            model: "qwen3.6:35b"
    workspace_tools:
      - name: "ws1"
        operations:
          read:
            tool_name: "cat"
            schema_matcher:
              type: "object"
            argument_map:
              path: "path"
            result_matcher:
              status: "ok"
          write:
            tool_name: "tee"
            creates_parents: true
            schema_matcher:
              type: "object"
            argument_map:
              path: "path"
            result_matcher:
              status: "ok"
          delete:
            tool_name: "rm"
            schema_matcher:
              type: "object"
            argument_map:
              path: "path"
            result_matcher:
              status: "ok"
nodes:
  - id: "node-gpu-01"
    providers:
      - id: "vllm-gpu"
        type: "vllm"
        category: "api"
        models:
          - "nvidia/Qwen3.6-35B"
        capacity: 4
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		cfg, err := config.LoadEdge(f)
		if err != nil {
			t.Fatalf("load: %v", err)
		}
		if len(cfg.Models) != 2 {
			t.Fatalf("expected 2 models, got %d", len(cfg.Models))
		}
		byID := map[string]config.ModelCatalogEntry{}
		for _, m := range cfg.Models {
			byID[m.ID] = m
		}
		// Provider-only entry should be unchanged.
		provModel := byID["qwen3.6:35b"]
		if len(provModel.Providers) != 1 {
			t.Errorf("provider-only model should have 1 provider, got %d", len(provModel.Providers))
		}
		if provModel.ExecutionPreset != "" {
			t.Errorf("provider-only model should not have execution_preset, got %q", provModel.ExecutionPreset)
		}
		// Virtual entry should reference the preset.
		virtualModel := byID["virtual-light"]
		if virtualModel.ExecutionPreset != "review-path" {
			t.Errorf("virtual model execution_preset = %q, want review-path", virtualModel.ExecutionPreset)
		}
		if len(virtualModel.Providers) != 0 {
			t.Errorf("virtual model should have empty providers, got %v", virtualModel.Providers)
		}
	})

	// ---- Compatibility: provider-only fixture with budget must still validate ----
	t.Run("provider-only entry with insufficient budget still rejected", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "qwen3.6:35b"
    context_window_tokens: 262144
    providers:
      vllm-gpu: "nvidia/Qwen3.6-35B"
nodes:
  - id: "node-gpu-01"
    providers:
      - id: "vllm-gpu"
        type: "vllm"
        category: "api"
        models:
          - "nvidia/Qwen3.6-35B"
        capacity: 4
        total_context_tokens: 262144
        long_context_capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		_, err := config.LoadEdge(f)
		if err == nil {
			t.Fatal("expected error for insufficient long-context budget on provider-only entry")
		}
		if !strings.Contains(err.Error(), "total_context_tokens") {
			t.Fatalf("expected budget error, got %v", err)
		}
	})

	// ---- Edge: whitespace-only execution_preset treated as unset ----
	t.Run("whitespace-only execution_preset treated as unset", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "whitespace-model"
    execution_preset: "   "
    providers:
      prov-a: "model-a"
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		cfg, err := config.LoadEdge(f)
		if err != nil {
			t.Fatalf("load: %v", err)
		}
		if len(cfg.Models) != 1 {
			t.Fatalf("expected 1 model, got %d", len(cfg.Models))
		}
		// Whitespace-only preset should be treated as unset, so provider-only
		// path should apply.
		if cfg.Models[0].ExecutionPreset != "" {
			t.Errorf("whitespace preset should be treated as unset, got %q", cfg.Models[0].ExecutionPreset)
		}
	})

	// ---- Normalization: non-empty execution_preset is stored canonically ----
	t.Run("non-empty execution_preset is normalized", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "qwen3.6:35b"
    providers:
      vllm-gpu: "nvidia/Qwen3.6-35B"
  - id: "virtual-fast"
    execution_preset: "  fast-path  "
execution_presets:
  - id: "fast-path"
    selector:
      model: "qwen3.6:35b"
    allowed_modes:
      - "direct"
    routes:
      direct:
        stages: []
nodes:
  - id: "node-gpu-01"
    providers:
      - id: "vllm-gpu"
        type: "vllm"
        category: "api"
        models:
          - "nvidia/Qwen3.6-35B"
        capacity: 4
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		cfg, err := config.LoadEdge(f)
		if err != nil {
			t.Fatalf("load: %v", err)
		}
		byID := map[string]config.ModelCatalogEntry{}
		for _, m := range cfg.Models {
			byID[m.ID] = m
		}
		// The padded valid preset id must be persisted in canonical (trimmed)
		// form so exact downstream lookups match the admitted value.
		virtual := byID["virtual-fast"]
		if virtual.ExecutionPreset != "fast-path" {
			t.Errorf("execution_preset = %q, want canonical %q", virtual.ExecutionPreset, "fast-path")
		}
		if len(virtual.Providers) != 0 {
			t.Errorf("virtual model should have empty providers, got %v", virtual.Providers)
		}
		// Provider-only entry stays unchanged.
		prov := byID["qwen3.6:35b"]
		if prov.ExecutionPreset != "" {
			t.Errorf("provider-only entry should not have execution_preset set, got %q", prov.ExecutionPreset)
		}
		if len(prov.Providers) != 1 {
			t.Errorf("provider-only entry should have 1 provider, got %d", len(prov.Providers))
		}
	})

	// ---- Error: empty execution_preset string with no providers ----
	t.Run("explicit empty execution_preset with no providers rejected", func(t *testing.T) {
		yaml := `
server:
  listen: "0.0.0.0:9090"
models:
  - id: "empty-preset-model"
    execution_preset: ""
nodes:
  - id: "node-01"
    providers:
      - id: "prov-a"
        type: "ollama"
        category: "local_inference"
        models: ["model-a"]
        capacity: 2
`
		if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
			t.Fatalf("write yaml: %v", err)
		}
		_, err := config.LoadEdge(f)
		if err == nil {
			t.Fatal("expected error for empty execution_preset with no providers")
		}
		if !strings.Contains(err.Error(), "providers must not be empty") {
			t.Fatalf("expected providers must not be empty error, got %v", err)
		}
	})
}

// TestModelCatalogEntry_ValidateVirtualEntryUnit covers unit-level Validate
// behavior for the one-of rule without going through LoadEdge.
func TestModelCatalogEntry_ValidateVirtualEntryUnit(t *testing.T) {
	providerIDs := map[string]struct{}{
		"vllm-gpu": {},
	}
	serveModels := map[string]map[string]struct{}{
		"vllm-gpu": {"model-a": {}},
	}

	t.Run("provider-only validates", func(t *testing.T) {
		e := config.ModelCatalogEntry{
			ID:        "model-a",
			Providers: map[string]string{"vllm-gpu": "model-a"},
		}
		if err := e.Validate(providerIDs, serveModels); err != nil {
			t.Fatalf("expected no error, got: %v", err)
		}
	})

	t.Run("preset-only validates (returns nil, preset resolved later)", func(t *testing.T) {
		e := config.ModelCatalogEntry{
			ID:              "virtual-model",
			ExecutionPreset: "fast-path",
		}
		if err := e.Validate(providerIDs, serveModels); err != nil {
			t.Fatalf("expected no error for preset-only, got: %v", err)
		}
	})

	t.Run("both providers and execution_preset rejected", func(t *testing.T) {
		e := config.ModelCatalogEntry{
			ID:              "bad-model",
			Providers:       map[string]string{"vllm-gpu": "model-a"},
			ExecutionPreset: "fast-path",
		}
		if err := e.Validate(providerIDs, serveModels); err == nil {
			t.Fatal("expected error for both set")
		}
	})

	t.Run("neither providers nor execution_preset rejected", func(t *testing.T) {
		e := config.ModelCatalogEntry{
			ID: "empty-model",
		}
		if err := e.Validate(providerIDs, serveModels); err == nil {
			t.Fatal("expected error for neither set")
		}
	})

	t.Run("whitespace execution_preset treated as unset", func(t *testing.T) {
		e := config.ModelCatalogEntry{
			ID:              "ws-model",
			Providers:       map[string]string{"vllm-gpu": "model-a"},
			ExecutionPreset: "   ",
		}
		if err := e.Validate(providerIDs, serveModels); err != nil {
			t.Fatalf("expected no error (whitespace preset treated as unset), got: %v", err)
		}
	})
}
