package shared

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

// Closes the config.go side of R-002 — TOML loader, default values,
// missing-file warn semantics. The audit flagged "internal/shared
// has zero tests" without distinguishing server.go from config.go;
// this file covers the latter.

func TestDefaultConfig_G0Ports(t *testing.T) {
	cfg := DefaultConfig()
	// Ports are shifted to 3110+ to coexist with the live Rust
	// lakehouse on 3100/3201-3204 during the migration. Locking
	// these values via test means a refactor that flips a port
	// silently can't ship without a test edit.
	checks := []struct {
		name     string
		actual   string
		expected string
	}{
		{"gateway bind", cfg.Gateway.Bind, "127.0.0.1:3110"},
		{"storaged bind", cfg.Storaged.Bind, "127.0.0.1:3211"},
		{"catalogd bind", cfg.Catalogd.Bind, "127.0.0.1:3212"},
		{"ingestd bind", cfg.Ingestd.Bind, "127.0.0.1:3213"},
		{"queryd bind", cfg.Queryd.Bind, "127.0.0.1:3214"},
		{"vectord bind", cfg.Vectord.Bind, "127.0.0.1:3215"},
		{"embedd bind", cfg.Embedd.Bind, "127.0.0.1:3216"},
	}
	for _, c := range checks {
		if c.actual != c.expected {
			t.Errorf("%s = %q, want %q", c.name, c.actual, c.expected)
		}
	}
	// G0 default: 256 MiB ingest cap (real-scale 500K test bumped
	// this to 512 — still 256 here as the documented default).
	if cfg.Ingestd.MaxIngestBytes != 256<<20 {
		t.Errorf("ingestd MaxIngestBytes = %d, want %d", cfg.Ingestd.MaxIngestBytes, 256<<20)
	}
	// embedd default model is the G2 nomic-embed-text default.
	if cfg.Embedd.DefaultModel != "nomic-embed-text" {
		t.Errorf("embedd DefaultModel = %q, want nomic-embed-text", cfg.Embedd.DefaultModel)
	}
	// queryd refresh ticker default — production value, not the proof
	// harness's 500ms override.
	if cfg.Queryd.RefreshEvery != "30s" {
		t.Errorf("queryd RefreshEvery = %q, want 30s", cfg.Queryd.RefreshEvery)
	}
}

func TestLoadConfig_EmptyPath_ReturnsDefaults(t *testing.T) {
	cfg, err := LoadConfig("")
	if err != nil {
		t.Fatalf("empty path should not error, got %v", err)
	}
	if cfg.Gateway.Bind != "127.0.0.1:3110" {
		t.Errorf("expected default gateway bind, got %q", cfg.Gateway.Bind)
	}
}

func TestLoadConfig_MissingFile_FallsBackToDefaults(t *testing.T) {
	// Per the comment in config.go: "non-empty + missing is suspicious"
	// — but the contract is to log a warn and return defaults, not
	// fail. We verify the contract; capturing the warn line is a
	// stretch for a unit test (slog default sink is os.Stderr).
	cfg, err := LoadConfig("/nonexistent/path/lakehouse.toml")
	if err != nil {
		t.Fatalf("missing file should not error, got %v", err)
	}
	if cfg.Storaged.Bind != "127.0.0.1:3211" {
		t.Errorf("expected default storaged bind on missing file, got %q", cfg.Storaged.Bind)
	}
}

func TestLoadConfig_ValidTOML_RoundTrip(t *testing.T) {
	// Write a partial config; verify only the overridden sections
	// land while the rest stay at defaults.
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "lakehouse.toml")
	body := `[gateway]
bind = "0.0.0.0:8080"

[s3]
endpoint = "http://other-minio:9000"
bucket   = "custom-bucket"
`
	if err := os.WriteFile(cfgPath, []byte(body), 0o644); err != nil {
		t.Fatalf("write config: %v", err)
	}

	cfg, err := LoadConfig(cfgPath)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Gateway.Bind != "0.0.0.0:8080" {
		t.Errorf("gateway.bind = %q, want 0.0.0.0:8080", cfg.Gateway.Bind)
	}
	if cfg.S3.Bucket != "custom-bucket" {
		t.Errorf("s3.bucket = %q, want custom-bucket", cfg.S3.Bucket)
	}
	// Unspecified sections keep defaults (TOML decoder doesn't zero
	// fields it didn't see).
	if cfg.Storaged.Bind != "127.0.0.1:3211" {
		t.Errorf("storaged.bind drifted to %q, want default 127.0.0.1:3211", cfg.Storaged.Bind)
	}
}

func TestDefaultConfig_ModelsTier(t *testing.T) {
	cfg := DefaultConfig()
	// Locks tier defaults so a refactor that drops a tier or renames
	// a default silently can't ship without a test edit.
	checks := []struct {
		tier, want string
	}{
		{"local_fast", "qwen3.5:latest"},
		{"local_embed", "nomic-embed-text"},
		{"local_judge", "qwen3.5:latest"},
		{"cloud_judge", "kimi-k2.6:cloud"},
		{"cloud_review", "qwen3-coder:480b"},
		{"frontier_review", "openrouter/anthropic/claude-opus-4-7"},
		{"frontier_free", "opencode/claude-opus-4-7"},
	}
	for _, c := range checks {
		if got := cfg.Models.Resolve(c.tier); got != c.want {
			t.Errorf("Models.Resolve(%q) = %q, want %q", c.tier, got, c.want)
		}
	}
	// Unknown tier returns "" — caller's responsibility to fall back.
	if got := cfg.Models.Resolve("nonexistent"); got != "" {
		t.Errorf("Models.Resolve(nonexistent) = %q, want empty string", got)
	}
}

func TestModelsConfig_IsWeak(t *testing.T) {
	cfg := DefaultConfig()
	// Default WeakModels set is the matrix.downgrade bypass list.
	if !cfg.Models.IsWeak("qwen3.5:latest") {
		t.Errorf("qwen3.5:latest should be weak (in default WeakModels)")
	}
	if !cfg.Models.IsWeak("qwen3:latest") {
		t.Errorf("qwen3:latest should be weak")
	}
	// Strong / cloud / frontier models are NOT weak.
	for _, m := range []string{
		"opencode/claude-opus-4-7",
		"openrouter/openai/gpt-5",
		"qwen3-coder:480b",
		"deepseek-v3.2",
	} {
		if cfg.Models.IsWeak(m) {
			t.Errorf("%s should NOT be weak", m)
		}
	}
}

func TestLoadConfig_ModelsTOMLRoundTrip(t *testing.T) {
	// Override one tier name; verify it loads, rest stay at defaults.
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "lakehouse.toml")
	body := `[models]
local_judge = "custom-judge:latest"
weak_models = ["custom-judge:latest", "qwen3:latest"]
`
	if err := os.WriteFile(cfgPath, []byte(body), 0o644); err != nil {
		t.Fatalf("write config: %v", err)
	}
	cfg, err := LoadConfig(cfgPath)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}
	if cfg.Models.LocalJudge != "custom-judge:latest" {
		t.Errorf("local_judge = %q, want custom-judge:latest", cfg.Models.LocalJudge)
	}
	if !cfg.Models.IsWeak("custom-judge:latest") {
		t.Errorf("custom-judge:latest should be weak after override")
	}
	// Unspecified tiers keep defaults — TOML decoder doesn't zero
	// fields it didn't see, BUT slices are replaced wholesale. The
	// weak_models override above is intentional so callers know
	// they're trading the full default list when they set this.
	if cfg.Models.LocalFast != "qwen3.5:latest" {
		t.Errorf("local_fast drifted to %q, want default qwen3.5:latest", cfg.Models.LocalFast)
	}
}

// TestLoadConfig_AuthTokenFromEnv locks ADR-006 Decision 6.2:
// production deploys put the secret in /etc/lakehouse/auth.env (mode
// 0600), loaded by systemd EnvironmentFile=, NEVER in the committed
// TOML. The TOML names the env var via token_env; the loader fills
// Token from os.Getenv. TokenEnv defaults to "AUTH_TOKEN" so the
// happy path needs no TOML config at all.
func TestLoadConfig_AuthTokenFromEnv(t *testing.T) {
	t.Run("default env name AUTH_TOKEN", func(t *testing.T) {
		t.Setenv("AUTH_TOKEN", "from-default-env")
		dir := t.TempDir()
		path := filepath.Join(dir, "lakehouse.toml")
		if err := os.WriteFile(path, []byte(`[auth]
allowed_ips = []
`), 0o644); err != nil {
			t.Fatal(err)
		}
		cfg, err := LoadConfig(path)
		if err != nil {
			t.Fatalf("LoadConfig: %v", err)
		}
		if cfg.Auth.Token != "from-default-env" {
			t.Errorf("Token = %q, want from AUTH_TOKEN env", cfg.Auth.Token)
		}
	})

	t.Run("custom env name from token_env", func(t *testing.T) {
		t.Setenv("CUSTOM_AUTH_TOKEN", "from-custom-env")
		dir := t.TempDir()
		path := filepath.Join(dir, "lakehouse.toml")
		if err := os.WriteFile(path, []byte(`[auth]
token_env = "CUSTOM_AUTH_TOKEN"
`), 0o644); err != nil {
			t.Fatal(err)
		}
		cfg, err := LoadConfig(path)
		if err != nil {
			t.Fatalf("LoadConfig: %v", err)
		}
		if cfg.Auth.Token != "from-custom-env" {
			t.Errorf("Token = %q, want from CUSTOM_AUTH_TOKEN env", cfg.Auth.Token)
		}
	})

	t.Run("explicit token wins over env", func(t *testing.T) {
		t.Setenv("AUTH_TOKEN", "from-env")
		dir := t.TempDir()
		path := filepath.Join(dir, "lakehouse.toml")
		if err := os.WriteFile(path, []byte(`[auth]
token = "from-toml"
`), 0o644); err != nil {
			t.Fatal(err)
		}
		cfg, err := LoadConfig(path)
		if err != nil {
			t.Fatalf("LoadConfig: %v", err)
		}
		// Explicit Token in TOML wins over env — the loader only
		// fills from env when Token is empty. Lets local dev
		// override prod env vars.
		if cfg.Auth.Token != "from-toml" {
			t.Errorf("Token = %q, want explicit TOML value", cfg.Auth.Token)
		}
	})
}

func TestLoadConfig_InvalidTOML_ReturnsError(t *testing.T) {
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "bad.toml")
	if err := os.WriteFile(cfgPath, []byte("this is = not [toml"), 0o644); err != nil {
		t.Fatalf("write bad config: %v", err)
	}

	_, err := LoadConfig(cfgPath)
	if err == nil {
		t.Fatal("expected parse error on malformed TOML, got nil")
	}
	if !strings.Contains(err.Error(), "parse config") {
		t.Errorf("error = %v, want 'parse config' wrapper", err)
	}
}

func TestLoadConfig_FileButUnreadable(t *testing.T) {
	// Skip on non-unix or when running as root (which can read
	// 0000-permission files). We only need this case in CI/local-dev
	// where test user isn't root. Per memory `feedback_pkill_scope.md`
	// J's box runs many things as root; treat this as informational.
	if os.Geteuid() == 0 {
		t.Skip("root can read 0000 files; skipping unreadable-file case")
	}
	dir := t.TempDir()
	cfgPath := filepath.Join(dir, "locked.toml")
	if err := os.WriteFile(cfgPath, []byte("[gateway]\nbind=\":1\""), 0o000); err != nil {
		t.Fatalf("write: %v", err)
	}

	_, err := LoadConfig(cfgPath)
	if err == nil {
		t.Fatal("expected read error on unreadable file, got nil")
	}
	if !strings.Contains(err.Error(), "read config") {
		t.Errorf("error = %v, want 'read config' wrapper", err)
	}
}
