package config

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

	"github.com/spf13/viper"
)

// writeConfigFile writes yaml content to a temp path and returns it.
func writeConfigFile(t *testing.T, yaml string) string {
	t.Helper()
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	if err := os.WriteFile(path, []byte(yaml), 0600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	return path
}

// resetViper clears all global viper state between tests.
func resetViper(t *testing.T) {
	t.Helper()
	viper.Reset()
}

// minimalValidYAML is the smallest configuration that passes Validate.
const minimalValidYAML = `
server:
  host: "0.0.0.0"
  port: 8080
  environment: development
garage:
  endpoint: http://garage:3900
  admin_endpoint: http://garage:3903
  admin_token: supersecret
`

func TestLoad_YAMLOnly(t *testing.T) {
	resetViper(t)
	path := writeConfigFile(t, minimalValidYAML)

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if cfg.Server.Host != "0.0.0.0" {
		t.Errorf("Server.Host = %q, want 0.0.0.0", cfg.Server.Host)
	}
	if cfg.Server.Port != 8080 {
		t.Errorf("Server.Port = %d, want 8080", cfg.Server.Port)
	}
	if cfg.Server.Environment != "development" {
		t.Errorf("Server.Environment = %q, want development", cfg.Server.Environment)
	}
	if cfg.Garage.Endpoint != "http://garage:3900" {
		t.Errorf("Garage.Endpoint = %q", cfg.Garage.Endpoint)
	}
	if cfg.Garage.AdminToken != "supersecret" {
		t.Errorf("Garage.AdminToken = %q", cfg.Garage.AdminToken)
	}
}

func TestLoad_EnvOnly_MissingFile(t *testing.T) {
	resetViper(t)
	// Point at a path that definitely does not exist. Load tolerates missing
	// files and falls back to env + viper defaults.
	missing := filepath.Join(t.TempDir(), "does-not-exist.yaml")

	// Every required field provided via env.
	t.Setenv("GARAGE_UI_SERVER_PORT", "9090")
	t.Setenv("GARAGE_UI_GARAGE_ENDPOINT", "http://g:3900")
	t.Setenv("GARAGE_UI_GARAGE_ADMIN_ENDPOINT", "http://g:3903")
	t.Setenv("GARAGE_UI_GARAGE_ADMIN_TOKEN", "env-token")

	cfg, err := Load(missing)
	if err != nil {
		t.Fatalf("Load with env-only: %v", err)
	}
	if cfg.Server.Port != 9090 {
		t.Errorf("Server.Port = %d, want 9090 (from env)", cfg.Server.Port)
	}
	if cfg.Garage.AdminToken != "env-token" {
		t.Errorf("Garage.AdminToken = %q, want env-token", cfg.Garage.AdminToken)
	}
}

func TestLoad_EnvOverridesYAML(t *testing.T) {
	resetViper(t)
	path := writeConfigFile(t, minimalValidYAML)

	// YAML has port=8080; env should win.
	t.Setenv("GARAGE_UI_SERVER_PORT", "9090")
	t.Setenv("GARAGE_UI_GARAGE_ADMIN_TOKEN", "env-wins")

	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if cfg.Server.Port != 9090 {
		t.Errorf("Server.Port = %d, want 9090 (env override)", cfg.Server.Port)
	}
	if cfg.Garage.AdminToken != "env-wins" {
		t.Errorf("Garage.AdminToken = %q, want env-wins", cfg.Garage.AdminToken)
	}
	// Host was not overridden; YAML value should persist.
	if cfg.Server.Host != "0.0.0.0" {
		t.Errorf("Server.Host = %q, want 0.0.0.0 (from YAML)", cfg.Server.Host)
	}
}

func TestLoad_MalformedYAMLReturnsError(t *testing.T) {
	resetViper(t)
	// Deliberately broken YAML: unindented key after a mapping start.
	path := writeConfigFile(t, "server:\n  port: 8080\n:: not: valid ::\n")

	_, err := Load(path)
	if err == nil {
		t.Fatal("expected error for malformed YAML, got nil")
	}
	if !strings.Contains(err.Error(), "error reading config file") {
		t.Errorf("unexpected error: %v", err)
	}
}

func TestLoad_ValidationFailurePropagates(t *testing.T) {
	resetViper(t)
	// Valid YAML syntax but Garage.Endpoint is blank → Validate must fail.
	path := writeConfigFile(t, `
server:
  port: 8080
garage:
  endpoint: ""
  admin_endpoint: http://g:3903
  admin_token: t
`)

	_, err := Load(path)
	if err == nil {
		t.Fatal("expected validation error, got nil")
	}
	if !strings.Contains(err.Error(), "invalid configuration") {
		t.Errorf("expected wrapped invalid-config error, got %v", err)
	}
	if !strings.Contains(err.Error(), "garage endpoint is required") {
		t.Errorf("expected endpoint-required message, got %v", err)
	}
}

// validBaseConfig returns a deep copy of a minimal Config that passes Validate.
func validBaseConfig() Config {
	return Config{
		Server: ServerConfig{Port: 8080},
		Garage: GarageConfig{
			Endpoint:      "http://g:3900",
			AdminEndpoint: "http://g:3903",
			AdminToken:    "t",
		},
	}
}

// applyValidOIDC fills OIDC with all required fields.
func applyValidOIDC(c *Config) {
	c.Auth.OIDC.Enabled = true
	c.Auth.OIDC.ClientID = "client-xyz"
	c.Auth.OIDC.IssuerURL = "https://idp.example/realms/test"
	c.Auth.OIDC.Scopes = []string{"openid"}
	c.Auth.OIDC.AdminRole = "admin"
	c.Server.RootURL = "https://garage-ui.example"
}

// Note on spec coverage: spec/2026-04-17-backend-test-suite-design.md lists
// "invalid log level/format" as a Validate case, but the current Validate does
// not check Logging.Level or Logging.Format. That's a code-vs-spec gap to
// resolve in a follow-up plan; Stage 2 tests the current behavior only.
func TestValidate(t *testing.T) {
	tests := []struct {
		name            string
		mutate          func(*Config)
		wantErrContains string // empty = expect no error
	}{
		{
			name:   "valid minimal config",
			mutate: func(c *Config) {},
		},
		{
			name:            "port zero is invalid",
			mutate:          func(c *Config) { c.Server.Port = 0 },
			wantErrContains: "invalid server port",
		},
		{
			name:            "port negative is invalid",
			mutate:          func(c *Config) { c.Server.Port = -1 },
			wantErrContains: "invalid server port",
		},
		{
			name:            "port above 65535 is invalid",
			mutate:          func(c *Config) { c.Server.Port = 70000 },
			wantErrContains: "invalid server port",
		},
		{
			name:            "port at 65535 is valid",
			mutate:          func(c *Config) { c.Server.Port = 65535 },
			wantErrContains: "",
		},
		{
			name:            "missing garage endpoint",
			mutate:          func(c *Config) { c.Garage.Endpoint = "" },
			wantErrContains: "garage endpoint is required",
		},
		{
			name:            "missing garage admin_endpoint",
			mutate:          func(c *Config) { c.Garage.AdminEndpoint = "" },
			wantErrContains: "admin_endpoint is required",
		},
		{
			name:            "missing garage admin_token",
			mutate:          func(c *Config) { c.Garage.AdminToken = "" },
			wantErrContains: "admin_token is required",
		},
		{
			name: "admin auth enabled without username",
			mutate: func(c *Config) {
				c.Auth.Admin.Enabled = true
				c.Auth.Admin.Password = "p"
			},
			wantErrContains: "admin auth username and password are required",
		},
		{
			name: "admin auth enabled without password",
			mutate: func(c *Config) {
				c.Auth.Admin.Enabled = true
				c.Auth.Admin.Username = "u"
			},
			wantErrContains: "admin auth username and password are required",
		},
		{
			name: "admin auth enabled with both set is valid",
			mutate: func(c *Config) {
				c.Auth.Admin.Enabled = true
				c.Auth.Admin.Username = "u"
				c.Auth.Admin.Password = "p"
			},
			wantErrContains: "",
		},
		{
			name: "admin auth disabled ignores missing credentials",
			mutate: func(c *Config) {
				c.Auth.Admin.Enabled = false
				c.Auth.Admin.Username = ""
				c.Auth.Admin.Password = ""
			},
			wantErrContains: "",
		},
		{
			name: "oidc enabled without client_id",
			mutate: func(c *Config) {
				applyValidOIDC(c)
				c.Auth.OIDC.ClientID = ""
			},
			wantErrContains: "oidc client_id is required",
		},
		{
			name: "oidc enabled without issuer_url",
			mutate: func(c *Config) {
				applyValidOIDC(c)
				c.Auth.OIDC.IssuerURL = ""
			},
			wantErrContains: "oidc issuer_url is required",
		},
		{
			name: "oidc enabled without server.root_url",
			mutate: func(c *Config) {
				applyValidOIDC(c)
				c.Server.RootURL = ""
			},
			wantErrContains: "server.root_url is required",
		},
		{
			name: "oidc enabled without scopes",
			mutate: func(c *Config) {
				applyValidOIDC(c)
				c.Auth.OIDC.Scopes = nil
			},
			wantErrContains: "oidc scopes are required",
		},
		{
			name: "oidc enabled without admin_role rejected for safety",
			mutate: func(c *Config) {
				applyValidOIDC(c)
				c.Auth.OIDC.AdminRole = ""
			},
			wantErrContains: "oidc admin_role is required",
		},
		{
			name:            "oidc fully configured is valid",
			mutate:          applyValidOIDC,
			wantErrContains: "",
		},
		{
			name: "oidc disabled ignores missing client_id",
			mutate: func(c *Config) {
				c.Auth.OIDC.Enabled = false
				c.Auth.OIDC.ClientID = ""
			},
			wantErrContains: "",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			cfg := validBaseConfig()
			tc.mutate(&cfg)
			err := cfg.Validate()

			if tc.wantErrContains == "" {
				if err != nil {
					t.Errorf("expected no error, got %v", err)
				}
				return
			}

			if err == nil {
				t.Fatalf("expected error containing %q, got nil", tc.wantErrContains)
			}
			if !strings.Contains(err.Error(), tc.wantErrContains) {
				t.Errorf("error %q does not contain %q", err.Error(), tc.wantErrContains)
			}
		})
	}
}

func TestGetAddress(t *testing.T) {
	tests := []struct {
		host string
		port int
		want string
	}{
		{"localhost", 8080, "localhost:8080"},
		{"0.0.0.0", 80, "0.0.0.0:80"},
		{"", 443, ":443"},
	}
	for _, tc := range tests {
		t.Run(tc.want, func(t *testing.T) {
			cfg := &Config{Server: ServerConfig{Host: tc.host, Port: tc.port}}
			if got := cfg.GetAddress(); got != tc.want {
				t.Errorf("GetAddress() = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestIsDevelopment(t *testing.T) {
	tests := []struct {
		env  string
		want bool
	}{
		{"development", true},
		{"production", false},
		{"", false},
		// Case-sensitive per current impl; lock in that behavior.
		{"Development", false},
		{"DEV", false},
	}
	for _, tc := range tests {
		t.Run(tc.env, func(t *testing.T) {
			cfg := &Config{Server: ServerConfig{Environment: tc.env}}
			if got := cfg.IsDevelopment(); got != tc.want {
				t.Errorf("IsDevelopment(%q) = %v, want %v", tc.env, got, tc.want)
			}
		})
	}
}

func TestIsProduction(t *testing.T) {
	tests := []struct {
		env  string
		want bool
	}{
		{"production", true},
		{"development", false},
		{"", false},
		{"Production", false},
		{"PROD", false},
	}
	for _, tc := range tests {
		t.Run(tc.env, func(t *testing.T) {
			cfg := &Config{Server: ServerConfig{Environment: tc.env}}
			if got := cfg.IsProduction(); got != tc.want {
				t.Errorf("IsProduction(%q) = %v, want %v", tc.env, got, tc.want)
			}
		})
	}
}
