package config_test

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

	"iop/packages/go/config"
)

func TestLoadEdge_EdgeIdentity(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := "server:\n  listen: \"0.0.0.0:9090\"\nedge:\n  id: \"edge-dgx-group\"\n  name: \"DGX Group\"\n"
	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 cfg.Edge.ID != "edge-dgx-group" {
		t.Fatalf("expected edge.id=%q, got %q", "edge-dgx-group", cfg.Edge.ID)
	}
	if cfg.Edge.Name != "DGX Group" {
		t.Fatalf("expected edge.name=%q, got %q", "DGX Group", cfg.Edge.Name)
	}
}

func TestLoadEdge_EdgeIdentityEmptyByDefault(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	if err := os.WriteFile(f, []byte("server:\n  listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}
	cfg, err := config.LoadEdge(f)
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if cfg.Edge.ID != "" || cfg.Edge.Name != "" {
		t.Fatalf("expected empty edge identity by default, got id=%q name=%q", cfg.Edge.ID, cfg.Edge.Name)
	}
}

func TestLoadEdge_OpenAIDefaults(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	if err := os.WriteFile(f, []byte("server:\n  listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}

	cfg, err := config.LoadEdge(f)
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if cfg.OpenAI.Enabled {
		t.Fatal("expected openai.enabled=false by default")
	}
	if cfg.OpenAI.Listen != "0.0.0.0:18081" {
		t.Fatalf("expected openai.listen default, got %q", cfg.OpenAI.Listen)
	}
	if cfg.OpenAI.BearerToken != "" {
		t.Fatalf("expected openai.bearer_token empty by default, got %q", cfg.OpenAI.BearerToken)
	}
	if cfg.OpenAI.ProviderAuth.Enabled {
		t.Fatal("expected openai.provider_auth.enabled=false by default")
	}
	if cfg.OpenAI.ProviderAuth.FromHeader != "" || cfg.OpenAI.ProviderAuth.TargetHeader != "" || cfg.OpenAI.ProviderAuth.Scheme != "" || cfg.OpenAI.ProviderAuth.Required {
		t.Fatalf("expected openai.provider_auth fields empty/false by default, got %+v", cfg.OpenAI.ProviderAuth)
	}
	if cfg.OpenAI.Adapter != "ollama" {
		t.Fatalf("expected openai.adapter=%q, got %q", "ollama", cfg.OpenAI.Adapter)
	}
	if cfg.OpenAI.SessionID != "openai" {
		t.Fatalf("expected openai.session_id=%q, got %q", "openai", cfg.OpenAI.SessionID)
	}
	if cfg.OpenAI.TimeoutSec != 120 {
		t.Fatalf("expected openai.timeout_sec=120, got %d", cfg.OpenAI.TimeoutSec)
	}
	if !cfg.OpenAI.StrictOutput {
		t.Fatal("expected openai.strict_output=true")
	}
	if cfg.OpenAI.StrictStreamBuffer {
		t.Fatal("expected openai.strict_stream_buffer=false")
	}
	if cfg.Metrics.Port != 19092 {
		t.Fatalf("expected metrics.port=19092, got %d", cfg.Metrics.Port)
	}
	if cfg.LongContextThresholdTokens != 100000 {
		t.Fatalf("expected long_context_threshold_tokens=100000 by default, got %d", cfg.LongContextThresholdTokens)
	}
}

func TestLoadEdge_LongContextThresholdOverride(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
long_context_threshold_tokens: 120000
`
	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 cfg.LongContextThresholdTokens != 120000 {
		t.Fatalf("expected long_context_threshold_tokens=120000, got %d", cfg.LongContextThresholdTokens)
	}
}

func TestLoadEdge_LongContextThresholdRejectsNonPositive(t *testing.T) {
	for _, tc := range []struct {
		name  string
		value int
	}{
		{name: "zero", value: 0},
		{name: "negative", value: -1},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "edge.yaml")
			yaml := fmt.Sprintf(`
server:
  listen: "0.0.0.0:9090"
long_context_threshold_tokens: %d
`, tc.value)
			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 non-positive long_context_threshold_tokens")
			}
			if !strings.Contains(err.Error(), "long_context_threshold_tokens must be positive") {
				t.Fatalf("expected error mentioning positive threshold, got %v", err)
			}
		})
	}
}

func TestLoadEdge_OpenAIOverride(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true
  provider_id: "test-provider"
  listen: "127.0.0.1:8088"
  bearer_token: "secret-token"
  node: "node0"
  adapter: "ollama"
  target: "llama-test"
  models:
    - "llama-test"
  session_id: "cline"
  timeout_sec: 45
  strict_output: false
  strict_stream_buffer: true
`
	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 !cfg.OpenAI.Enabled {
		t.Fatal("expected openai.enabled=true")
	}
	if cfg.OpenAI.Listen != "127.0.0.1:8088" || cfg.OpenAI.NodeRef != "node0" {
		t.Fatalf("unexpected openai routing config: %+v", cfg.OpenAI)
	}
	if cfg.OpenAI.BearerToken != "secret-token" {
		t.Fatalf("expected openai.bearer_token=%q, got %q", "secret-token", cfg.OpenAI.BearerToken)
	}
	if cfg.OpenAI.Target != "llama-test" || len(cfg.OpenAI.Models) != 1 || cfg.OpenAI.Models[0] != "llama-test" {
		t.Fatalf("unexpected openai model config: %+v", cfg.OpenAI)
	}
	if cfg.OpenAI.SessionID != "cline" || cfg.OpenAI.TimeoutSec != 45 {
		t.Fatalf("unexpected openai execution config: %+v", cfg.OpenAI)
	}
	if cfg.OpenAI.StrictOutput {
		t.Fatalf("unexpected openai strict output config: %+v", cfg.OpenAI)
	}
	if !cfg.OpenAI.StrictStreamBuffer {
		t.Fatalf("unexpected openai strict stream buffer config: %+v", cfg.OpenAI)
	}
}

func TestLoadEdge_OpenAIProviderAuthEnabledDefaults(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  provider_auth:
    enabled: true
`
	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)
	}
	auth := cfg.OpenAI.ProviderAuth
	if !auth.Enabled {
		t.Fatal("expected provider_auth.enabled=true")
	}
	if auth.FromHeader != "X-IOP-Provider-Authorization" {
		t.Fatalf("expected default from_header, got %q", auth.FromHeader)
	}
	if auth.TargetHeader != "Authorization" {
		t.Fatalf("expected default target_header, got %q", auth.TargetHeader)
	}
	if auth.Scheme != "Bearer" {
		t.Fatalf("expected default scheme Bearer, got %q", auth.Scheme)
	}
	if !auth.Required {
		t.Fatal("expected required=true by default when provider_auth is enabled")
	}
}

func TestLoadEdge_OpenAIProviderAuthOverride(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  provider_auth:
    enabled: true
    from_header: "X-Seulgivibe-Token"
    target_header: "X-Provider-Authorization"
    scheme: "Token"
    required: false
`
	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)
	}
	auth := cfg.OpenAI.ProviderAuth
	if !auth.Enabled {
		t.Fatal("expected provider_auth.enabled=true")
	}
	if auth.FromHeader != "X-Seulgivibe-Token" || auth.TargetHeader != "X-Provider-Authorization" || auth.Scheme != "Token" || auth.Required {
		t.Fatalf("unexpected provider_auth override: %+v", auth)
	}
}

func TestLoadEdge_OpenAIProviderAuthRejectsBlankHeaders(t *testing.T) {
	for _, tc := range []struct {
		name    string
		field   string
		wantMsg string
	}{
		{name: "from_header", field: "from_header", wantMsg: "from_header"},
		{name: "target_header", field: "target_header", wantMsg: "target_header"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "edge.yaml")
			yaml := fmt.Sprintf(`
server:
  listen: "0.0.0.0:9090"
openai:
  provider_auth:
    enabled: true
    %s: "  "
`, tc.field)
			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 blank provider_auth header error")
			}
			if !strings.Contains(err.Error(), tc.wantMsg) {
				t.Fatalf("expected error mentioning %q, got %v", tc.wantMsg, err)
			}
		})
	}
}

func TestLoadEdge_OpenAIProviderAuthRejectsInboundCallerAuthHeaders(t *testing.T) {
	for _, tc := range []struct {
		name   string
		header string
	}{
		{name: "Authorization exact", header: "Authorization"},
		{name: "authorization lowercase", header: "authorization"},
		{name: "AUTHORIZATION uppercase", header: "AUTHORIZATION"},
		{name: "Authorization with whitespace", header: "  Authorization  "},
		{name: "X-Api-Key exact", header: "X-Api-Key"},
		{name: "x-api-key lowercase", header: "x-api-key"},
		{name: "X-API-KEY uppercase", header: "X-API-KEY"},
		{name: "X-Api-Key with whitespace", header: "  X-Api-Key  "},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "edge.yaml")
			yaml := fmt.Sprintf(`
server:
  listen: "0.0.0.0:9090"
openai:
  provider_auth:
    enabled: true
    from_header: %q
`, tc.header)
			if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
				t.Fatalf("write yaml: %v", err)
			}
			_, err := config.LoadEdge(f)
			if err == nil {
				t.Fatalf("expected error for inbound caller auth header %q", tc.header)
			}
			if !strings.Contains(err.Error(), "must not reuse inbound caller authentication header") {
				t.Fatalf("expected error mentioning inbound caller authentication header, got %v", err)
			}
		})
	}
}

func TestNormalizeProviderTypeOpenAICompatibleAliases(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want string
	}{
		{name: "openai compat", in: "openai_compat", want: "openai_compat"},
		{name: "openai api", in: "openai_api", want: "openai_compat"},
		{name: "vllm", in: "vllm", want: "openai_compat"},
		{name: "vllm mlx", in: "vllm-mlx", want: "openai_compat"},
		{name: "lemonade", in: "lemonade", want: "openai_compat"},
		{name: "sglang", in: "sglang", want: "openai_compat"},
		{name: "seulgivibe claude", in: "seulgivibe_claude", want: "openai_compat"},
		{name: "seulgivibe openai", in: "seulgivibe_openai", want: "openai_compat"},
		{name: "normalized case and space", in: " VLLM-MLX ", want: "openai_compat"},
		{name: "ollama", in: "ollama", want: "ollama"},
		{name: "cli", in: "cli", want: "cli"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := config.NormalizeProviderType(tc.in); got != tc.want {
				t.Fatalf("NormalizeProviderType(%q) = %q, want %q", tc.in, got, tc.want)
			}
		})
	}
}

func TestLoadEdge_OpenAIPrincipalTokens(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true
  provider_id: "test-provider"
  principal_tokens:
    - token_ref: "iop-tok-alice"
      token_hash_sha256: "` + strings.Repeat("a1", 32) + `"
      principal_ref: "user:alice"
      principal_alias: "alice"
    - token_ref: "iop-tok-bob"
      token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
      principal_ref: "user:bob"
`
	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.OpenAI.PrincipalTokens) != 2 {
		t.Fatalf("expected 2 principal_tokens, got %d", len(cfg.OpenAI.PrincipalTokens))
	}
	first := cfg.OpenAI.PrincipalTokens[0]
	if first.TokenRef != "iop-tok-alice" || first.PrincipalRef != "user:alice" || first.PrincipalAlias != "alice" {
		t.Fatalf("unexpected first principal token: %+v", first)
	}
	if first.TokenHashSHA256 != strings.Repeat("a1", 32) {
		t.Fatalf("unexpected token hash: %q", first.TokenHashSHA256)
	}
	second := cfg.OpenAI.PrincipalTokens[1]
	if second.TokenRef != "iop-tok-bob" || second.PrincipalRef != "user:bob" || second.PrincipalAlias != "" {
		t.Fatalf("unexpected second principal token: %+v", second)
	}
}

func TestLoadEdge_OpenAIPrincipalTokensAllowMultipleTokensPerPrincipal(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true
  provider_id: "test-provider"
  principal_tokens:
    - token_ref: "alice-app-a"
      token_hash_sha256: "` + strings.Repeat("a1", 32) + `"
      principal_ref: "user:alice"
      principal_alias: "alice"
    - token_ref: "alice-app-b"
      token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
      principal_ref: "user:alice"
      principal_alias: "alice"
`
	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.OpenAI.PrincipalTokens) != 2 {
		t.Fatalf("expected 2 principal_tokens, got %d", len(cfg.OpenAI.PrincipalTokens))
	}
	if cfg.OpenAI.PrincipalTokens[0].PrincipalRef != cfg.OpenAI.PrincipalTokens[1].PrincipalRef {
		t.Fatalf("expected both tokens to map to the same principal: %+v", cfg.OpenAI.PrincipalTokens)
	}
	if cfg.OpenAI.PrincipalTokens[0].TokenRef == cfg.OpenAI.PrincipalTokens[1].TokenRef {
		t.Fatalf("token_ref must distinguish app/integration tokens: %+v", cfg.OpenAI.PrincipalTokens)
	}
}

func TestLoadEdge_OpenAIPrincipalTokensRejectInvalid(t *testing.T) {
	validHash := strings.Repeat("a1", 32)
	for _, tc := range []struct {
		name string
		yaml string
	}{
		{
			name: "duplicate token_ref",
			yaml: `
openai:
  principal_tokens:
    - token_ref: "dup"
      token_hash_sha256: "` + validHash + `"
      principal_ref: "user:alice"
    - token_ref: "dup"
      token_hash_sha256: "` + strings.Repeat("b2", 32) + `"
      principal_ref: "user:bob"
`,
		},
		{
			name: "duplicate token_hash_sha256",
			yaml: `
openai:
  principal_tokens:
    - token_ref: "one"
      token_hash_sha256: "` + validHash + `"
      principal_ref: "user:alice"
    - token_ref: "two"
      token_hash_sha256: "` + validHash + `"
      principal_ref: "user:bob"
`,
		},
		{
			name: "empty principal_ref",
			yaml: `
openai:
  principal_tokens:
    - token_ref: "one"
      token_hash_sha256: "` + validHash + `"
      principal_ref: ""
`,
		},
		{
			name: "non-hex hash",
			yaml: `
openai:
  principal_tokens:
    - token_ref: "one"
      token_hash_sha256: "` + strings.Repeat("z", 64) + `"
      principal_ref: "user:alice"
`,
		},
		{
			name: "non-64-length hash",
			yaml: `
openai:
  principal_tokens:
    - token_ref: "one"
      token_hash_sha256: "abcd"
      principal_ref: "user:alice"
`,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "edge.yaml")
			body := "server:\n  listen: \"0.0.0.0:9090\"\n" + tc.yaml
			if err := os.WriteFile(f, []byte(body), 0o600); err != nil {
				t.Fatalf("write yaml: %v", err)
			}
			if _, err := config.LoadEdge(f); err == nil {
				t.Fatal("expected validation error, got nil")
			}
		})
	}
}

// TestOpenAIPrincipalTokenConf_NoRawTokenField guards against reintroducing a
// raw-token-storing field on the principal token mapping struct: only
// token_ref, token_hash_sha256, principal_ref, and principal_alias are
// allowed YAML keys.
func TestOpenAIPrincipalTokenConf_NoRawTokenField(t *testing.T) {
	allowed := map[string]struct{}{
		"token_ref":         {},
		"token_hash_sha256": {},
		"principal_ref":     {},
		"principal_alias":   {},
	}
	typ := reflect.TypeOf(config.OpenAIPrincipalTokenConf{})
	for i := 0; i < typ.NumField(); i++ {
		tag := typ.Field(i).Tag.Get("yaml")
		key := strings.SplitN(tag, ",", 2)[0]
		if _, ok := allowed[key]; !ok {
			t.Fatalf("unexpected field %q (yaml tag %q) on OpenAIPrincipalTokenConf; raw token values must not be stored in config", typ.Field(i).Name, tag)
		}
	}
}

func TestEdgeOpenAIProviderAuthConf_NoRawTokenField(t *testing.T) {
	allowed := map[string]struct{}{
		"enabled":       {},
		"from_header":   {},
		"target_header": {},
		"scheme":        {},
		"required":      {},
	}
	typ := reflect.TypeOf(config.EdgeOpenAIProviderAuthConf{})
	for i := 0; i < typ.NumField(); i++ {
		tag := typ.Field(i).Tag.Get("yaml")
		key := strings.SplitN(tag, ",", 2)[0]
		if _, ok := allowed[key]; !ok {
			t.Fatalf("unexpected field %q (yaml tag %q) on EdgeOpenAIProviderAuthConf; raw provider tokens must not be stored in config", typ.Field(i).Name, tag)
		}
	}
}

func TestLoadEdge_OpenAIRouteCatalog(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  enabled: true
  provider_id: "test-provider"
  listen: "0.0.0.0:18081"
  adapter: "ollama"
  model_routes:
    - model: "model-a"
      adapter: "ollama"
      target: "llama3"
      node: "node-01"
      session_id: "sess-a"
      timeout_sec: 30
      max_queue: 10
      queue_timeout_ms: 5000
    - model: "model-b"
      adapter: "vllm"
      target: "qwen"
`
	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.OpenAI.ModelRoutes) != 2 {
		t.Fatalf("expected 2 model_routes, got %d", len(cfg.OpenAI.ModelRoutes))
	}
	r0 := cfg.OpenAI.ModelRoutes[0]
	if r0.Model != "model-a" || r0.Adapter != "ollama" || r0.Target != "llama3" {
		t.Errorf("route[0] mismatch: %+v", r0)
	}
	if r0.NodeRef != "node-01" || r0.SessionID != "sess-a" || r0.TimeoutSec != 30 {
		t.Errorf("route[0] optional fields mismatch: %+v", r0)
	}
	if r0.MaxQueue != 10 || r0.QueueTimeoutMS != 5000 {
		t.Errorf("route[0] queue policy mismatch: max_queue=%d, queue_timeout_ms=%d", r0.MaxQueue, r0.QueueTimeoutMS)
	}
	r1 := cfg.OpenAI.ModelRoutes[1]
	if r1.Model != "model-b" || r1.Adapter != "vllm" || r1.Target != "qwen" {
		t.Errorf("route[1] mismatch: %+v", r1)
	}
	if r1.MaxQueue != 0 || r1.QueueTimeoutMS != 0 {
		t.Errorf("route[1] queue policy should default to 0: max_queue=%d, queue_timeout_ms=%d", r1.MaxQueue, r1.QueueTimeoutMS)
	}
}

func TestLoadEdge_OpenAIRouteCatalogDuplicateModelRejects(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  model_routes:
    - model: "model-a"
      adapter: "ollama"
      target: "llama3"
    - model: "model-a"
      adapter: "vllm"
      target: "qwen"
`
	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 duplicate model route")
	}
	if !strings.Contains(err.Error(), "model_routes") || !strings.Contains(err.Error(), "model-a") {
		t.Fatalf("expected error mentioning model_routes and model-a, got %v", err)
	}
}

func TestLoadEdge_OpenAIRouteCatalogEmptyModelRejects(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
openai:
  model_routes:
    - model: ""
      target: "llama3"
`
	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 model in route catalog")
	}
	if !strings.Contains(err.Error(), "model_routes") {
		t.Fatalf("expected error mentioning model_routes, got %v", err)
	}
}

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

	// Test negative max_queue
	f1 := filepath.Join(dir, "edge_neg_max.yaml")
	yaml1 := `
server:
  listen: "0.0.0.0:9090"
openai:
  model_routes:
    - model: "model-a"
      target: "llama3"
      max_queue: -1
`
	if err := os.WriteFile(f1, []byte(yaml1), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}
	_, err := config.LoadEdge(f1)
	if err == nil {
		t.Fatal("expected error for negative max_queue")
	}
	if !strings.Contains(err.Error(), "max_queue must be non-negative") {
		t.Fatalf("expected error mentioning max_queue must be non-negative, got %v", err)
	}

	// Test negative queue_timeout_ms
	f2 := filepath.Join(dir, "edge_neg_timeout.yaml")
	yaml2 := `
server:
  listen: "0.0.0.0:9090"
openai:
  model_routes:
    - model: "model-a"
      target: "llama3"
      queue_timeout_ms: -500
`
	if err := os.WriteFile(f2, []byte(yaml2), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}
	_, err = config.LoadEdge(f2)
	if err == nil {
		t.Fatal("expected error for negative queue_timeout_ms")
	}
	if !strings.Contains(err.Error(), "queue_timeout_ms must be non-negative") {
		t.Fatalf("expected error mentioning queue_timeout_ms must be non-negative, got %v", err)
	}
}

// NormalizeAdapters regression tests

func TestLoadEdge_OpenAICompatInstances(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - alias: "lemonade-node"
    adapters:
      openai_compat_instances:
        - name: "lemonade"
          enabled: true
          provider: "lemonade"
          endpoint: "http://127.0.0.1:13305"
          headers:
            Authorization: "Bearer test-key"
          capacity: 4
          max_queue: 10
          queue_timeout_ms: 1500
          request_timeout_ms: 30000
`
	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.Nodes) == 0 {
		t.Fatal("expected 1 node")
	}
	insts := cfg.Nodes[0].Adapters.OpenAICompatInstances
	if len(insts) != 1 {
		t.Fatalf("expected 1 openai_compat instance, got %d", len(insts))
	}
	inst := insts[0]
	if inst.Name != "lemonade" || inst.Provider != "lemonade" || inst.Endpoint != "http://127.0.0.1:13305" {
		t.Errorf("unexpected instance config: %+v", inst)
	}
	if inst.Headers["authorization"] != "Bearer test-key" {
		t.Errorf("expected header authorization Bearer test-key, got %q", inst.Headers["authorization"])
	}
	if inst.Capacity != 4 || inst.MaxQueue != 10 || inst.QueueTimeoutMS != 1500 || inst.RequestTimeoutMS != 30000 {
		t.Errorf("unexpected queue config: %+v", inst)
	}
}

func TestNormalizeAdapters_OpenAICompatIdempotent(t *testing.T) {
	a := config.AdaptersConf{
		OpenAICompat: config.OpenAICompatConf{
			Enabled:  true,
			Provider: "lemonade",
			Endpoint: "http://127.0.0.1:13305",
			Headers:  map[string]string{"X-Test": "val"},
		},
	}
	if err := config.NormalizeAdapters(&a); err != nil {
		t.Fatalf("first normalize: %v", err)
	}
	if len(a.OpenAICompatInstances) != 1 {
		t.Fatalf("expected 1 instance after first normalize, got %d", len(a.OpenAICompatInstances))
	}
	if err := config.NormalizeAdapters(&a); err != nil {
		t.Fatalf("second normalize (idempotent): %v", err)
	}
	if len(a.OpenAICompatInstances) != 1 {
		t.Fatalf("expected still 1 instance after second normalize, got %d", len(a.OpenAICompatInstances))
	}
}

func TestNormalizeAdapters_OpenAICompatConflictErrors(t *testing.T) {
	a := config.AdaptersConf{
		OpenAICompat: config.OpenAICompatConf{Enabled: true, Endpoint: "http://127.0.0.1:13305"},
		OpenAICompatInstances: []config.OpenAICompatInstanceConf{
			{Name: "openai_compat", Enabled: true, Endpoint: "http://other-host:13305"},
		},
	}
	if err := config.NormalizeAdapters(&a); err == nil {
		t.Fatal("expected error for conflicting legacy openai_compat and explicit instance with same name")
	}
}

func TestNormalizeAdapters_OpenAICompatDuplicateNameRejects(t *testing.T) {
	a := config.AdaptersConf{
		OpenAICompatInstances: []config.OpenAICompatInstanceConf{
			{Name: "same", Enabled: true, Endpoint: "http://127.0.0.1:13305"},
			{Name: "same", Enabled: true, Endpoint: "http://127.0.0.2:13305"},
		},
	}
	if err := config.NormalizeAdapters(&a); err == nil {
		t.Fatal("expected error for duplicate openai_compat instance name")
	}
}

func TestNormalizeAdapters_OpenAICompatQueueValidation(t *testing.T) {
	a := config.AdaptersConf{
		OpenAICompatInstances: []config.OpenAICompatInstanceConf{
			{Name: "test", Enabled: true, Endpoint: "http://127.0.0.1:13305", Capacity: -1},
		},
	}
	err := config.NormalizeAdapters(&a)
	if err == nil {
		t.Fatal("expected negative provider queue config error")
	}
	if !strings.Contains(err.Error(), "capacity") {
		t.Fatalf("expected error to mention capacity, got %v", err)
	}
}

// S02 REVIEW_VLLM_CONFIG: vLLM OpenAI-compatible route + instance fixture
// verifies that alias/target/provider/endpoint/no-headers/timeout/queue are
// preserved through config load and mapper/factory contract.
