package config_test

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

	"iop/packages/go/config"
)

func TestLoadEdge_NodeProviderConfEmptyIDRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: ""
        type: "vllm"
        category: "api"
`
	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 node provider id")
	}
	if !strings.Contains(err.Error(), "providers[].id must not be empty") {
		t.Fatalf("expected error mentioning providers[].id must not be empty, got %v", err)
	}
}

func TestLoadEdge_NodeProviderConfEmptyTypeRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: "p1"
        type: ""
        category: "api"
`
	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 provider type")
	}
	if !strings.Contains(err.Error(), "type must not be empty") {
		t.Fatalf("expected error mentioning type must not be empty, got %v", err)
	}
}

func TestLoadEdge_NodeProviderConfInvalidCategoryRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: "p1"
        type: "vllm"
        category: "invalid_category"
`
	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 invalid provider category")
	}
	if !strings.Contains(err.Error(), "invalid provider category") {
		t.Fatalf("expected error mentioning invalid provider category, got %v", err)
	}
}

func TestLoadEdge_NodeProviderConfDuplicateIDRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: "p1"
        type: "vllm"
        category: "api"
      - id: "p1"
        type: "ollama"
        category: "local_inference"
`
	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 provider id within a node")
	}
	if !strings.Contains(err.Error(), "duplicate provider id") {
		t.Fatalf("expected error mentioning duplicate provider id, got %v", err)
	}
}

func TestLoadEdge_NodeProviderConfNegativeCapacityRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: "p1"
        type: "vllm"
        category: "api"
        capacity: -1
`
	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 negative provider capacity")
	}
	if !strings.Contains(err.Error(), "capacity must be non-negative") {
		t.Fatalf("expected error mentioning capacity must be non-negative, got %v", err)
	}
}

func TestLoadEdge_NodeProviderConfEmptyServedModelRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-01"
    providers:
      - id: "p1"
        type: "vllm"
        category: "api"
        models:
          - ""
          - "real-model"
`
	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 served model in provider models list")
	}
	if !strings.Contains(err.Error(), "served model name must not be empty") {
		t.Fatalf("expected error mentioning served model name must not be empty, got %v", err)
	}
}

func TestLoad_NodeReconnectDefaults(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "node.yaml")
	if err := os.WriteFile(f, []byte("transport:\n  edge_addr: \"localhost:9090\"\n  token: \"tok\"\n"), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}
	cfg, err := config.Load(f)
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if cfg.Reconnect.IntervalSec != 10 {
		t.Fatalf("expected reconnect.interval_sec=10, got %d", cfg.Reconnect.IntervalSec)
	}
	if cfg.Reconnect.MaxAttempts != 10 {
		t.Fatalf("expected reconnect.max_attempts=10, got %d", cfg.Reconnect.MaxAttempts)
	}
}

func TestLoad_NodeReconnectOverride(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "node.yaml")
	yaml := "transport:\n  edge_addr: \"localhost:9090\"\n  token: \"tok\"\nreconnect:\n  interval_sec: 30\n  max_attempts: 5\n"
	if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
		t.Fatalf("write yaml: %v", err)
	}
	cfg, err := config.Load(f)
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if cfg.Reconnect.IntervalSec != 30 {
		t.Fatalf("expected reconnect.interval_sec=30, got %d", cfg.Reconnect.IntervalSec)
	}
	if cfg.Reconnect.MaxAttempts != 5 {
		t.Fatalf("expected reconnect.max_attempts=5, got %d", cfg.Reconnect.MaxAttempts)
	}
}

// TestLoadReconnectPolicy verifies that config load distinguishes an omitted
// max_attempts (default 10), an explicit 0 (unlimited), and a positive finite
// limit, and preserves an explicit interval alongside each. SDD S16/S17.
func TestLoadReconnectPolicy(t *testing.T) {
	cases := []struct {
		name          string
		reconnectYAML string
		wantInterval  int
		wantMax       int
	}{
		{
			name:          "omitted keeps defaults",
			reconnectYAML: "",
			wantInterval:  10,
			wantMax:       10,
		},
		{
			name:          "explicit zero max is unlimited with default interval",
			reconnectYAML: "reconnect:\n  max_attempts: 0\n",
			wantInterval:  10,
			wantMax:       0,
		},
		{
			name:          "explicit zero max with positive interval stays unlimited",
			reconnectYAML: "reconnect:\n  max_attempts: 0\n  interval_sec: 5\n",
			wantInterval:  5,
			wantMax:       0,
		},
		{
			name:          "positive finite limit is preserved",
			reconnectYAML: "reconnect:\n  max_attempts: 3\n  interval_sec: 30\n",
			wantInterval:  30,
			wantMax:       3,
		},
		{
			name:          "positive finite limit allows zero interval",
			reconnectYAML: "reconnect:\n  max_attempts: 3\n  interval_sec: 0\n",
			wantInterval:  0,
			wantMax:       3,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "node.yaml")
			yaml := "transport:\n  edge_addr: \"localhost:9090\"\n  token: \"tok\"\n" + tc.reconnectYAML
			if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
				t.Fatalf("write yaml: %v", err)
			}
			cfg, err := config.Load(f)
			if err != nil {
				t.Fatalf("load: %v", err)
			}
			if cfg.Reconnect.IntervalSec != tc.wantInterval {
				t.Errorf("interval_sec: got %d want %d", cfg.Reconnect.IntervalSec, tc.wantInterval)
			}
			if cfg.Reconnect.MaxAttempts != tc.wantMax {
				t.Errorf("max_attempts: got %d want %d", cfg.Reconnect.MaxAttempts, tc.wantMax)
			}
		})
	}
}

// TestLoadRejectsInvalidReconnectPolicy verifies that negative attempts/interval
// and an unlimited policy with a non-positive interval are rejected at load with
// a clear config error. SDD S17.
func TestLoadRejectsInvalidReconnectPolicy(t *testing.T) {
	cases := []struct {
		name          string
		reconnectYAML string
		wantSubstr    string
	}{
		{
			name:          "negative max_attempts",
			reconnectYAML: "reconnect:\n  max_attempts: -1\n",
			wantSubstr:    "reconnect.max_attempts must not be negative",
		},
		{
			name:          "negative interval_sec",
			reconnectYAML: "reconnect:\n  max_attempts: 3\n  interval_sec: -1\n",
			wantSubstr:    "reconnect.interval_sec must not be negative",
		},
		{
			name:          "unlimited with zero interval",
			reconnectYAML: "reconnect:\n  max_attempts: 0\n  interval_sec: 0\n",
			wantSubstr:    "reconnect.interval_sec must be positive",
		},
		{
			name:          "unlimited with negative interval",
			reconnectYAML: "reconnect:\n  max_attempts: 0\n  interval_sec: -5\n",
			wantSubstr:    "reconnect.interval_sec must not be negative",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			f := filepath.Join(dir, "node.yaml")
			yaml := "transport:\n  edge_addr: \"localhost:9090\"\n  token: \"tok\"\n" + tc.reconnectYAML
			if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
				t.Fatalf("write yaml: %v", err)
			}
			_, err := config.Load(f)
			if err == nil {
				t.Fatalf("expected error for %s", tc.name)
			}
			if !strings.Contains(err.Error(), tc.wantSubstr) {
				t.Fatalf("expected error containing %q, got %v", tc.wantSubstr, err)
			}
		})
	}
}

// TestLoadEdge_NodeProviderPriorityOmittedDefaultsZero verifies that when
// priority is omitted from a provider entry, it defaults to zero.
func TestLoadEdge_NodeProviderPriorityOmittedDefaultsZero(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-p"
    alias: "n-p"
    token: "tok-p"
    providers:
      - id: "prov-p"
        type: "vllm"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        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.Nodes) != 1 {
		t.Fatalf("expected 1 node, got %d", len(cfg.Nodes))
	}
	if len(cfg.Nodes[0].Providers) != 1 {
		t.Fatalf("expected 1 provider, got %d", len(cfg.Nodes[0].Providers))
	}
	if cfg.Nodes[0].Providers[0].Priority != 0 {
		t.Fatalf("expected default priority=0, got %d", cfg.Nodes[0].Providers[0].Priority)
	}
}

// TestLoadEdge_NodeProviderPriorityExplicitLoads verifies that an explicit
// priority value is loaded correctly.
func TestLoadEdge_NodeProviderPriorityExplicitLoads(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-p2"
    alias: "n-p2"
    token: "tok-p2"
    providers:
      - id: "prov-p2"
        type: "vllm"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        models:
          - "model-a"
        capacity: 2
        priority: 5
`
	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.Nodes[0].Providers[0].Priority != 5 {
		t.Fatalf("expected priority=5, got %d", cfg.Nodes[0].Providers[0].Priority)
	}
}

// TestLoadEdge_NodeProviderNegativePriorityRejected verifies that a negative
// priority value is rejected during config load.
func TestLoadEdge_NodeProviderNegativePriorityRejected(t *testing.T) {
	dir := t.TempDir()
	f := filepath.Join(dir, "edge.yaml")
	yaml := `
server:
  listen: "0.0.0.0:9090"
nodes:
  - id: "node-p3"
    alias: "n-p3"
    token: "tok-p3"
    providers:
      - id: "prov-p3"
        type: "vllm"
        category: "api"
        endpoint: "http://127.0.0.1:8000/v1"
        models:
          - "model-a"
        capacity: 2
        priority: -1
`
	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 negative priority")
	}
	if !strings.Contains(err.Error(), "priority must be non-negative") {
		t.Fatalf("expected error mentioning priority non-negative, got %v", err)
	}
}
