package config

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

func TestInitialize(t *testing.T) {
	// Test that initialization doesn't error
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	if v == nil {
		t.Fatal("viper instance is nil after Initialize()")
	}
}

func TestDefaults(t *testing.T) {
	// Reset viper for test isolation
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	tests := []struct {
		key      string
		expected interface{}
		getter   func(string) interface{}
	}{
		{"json", false, func(k string) interface{} { return GetBool(k) }},
		{"no-daemon", false, func(k string) interface{} { return GetBool(k) }},
		{"no-auto-flush", false, func(k string) interface{} { return GetBool(k) }},
		{"no-auto-import", false, func(k string) interface{} { return GetBool(k) }},
		{"db", "", func(k string) interface{} { return GetString(k) }},
		{"actor", "", func(k string) interface{} { return GetString(k) }},
		{"flush-debounce", 30 * time.Second, func(k string) interface{} { return GetDuration(k) }},
		{"auto-start-daemon", true, func(k string) interface{} { return GetBool(k) }},
	}
	
	for _, tt := range tests {
		t.Run(tt.key, func(t *testing.T) {
			got := tt.getter(tt.key)
			if got != tt.expected {
				t.Errorf("GetXXX(%q) = %v, want %v", tt.key, got, tt.expected)
			}
		})
	}
}

func TestEnvironmentBinding(t *testing.T) {
	// Test environment variable binding
	tests := []struct {
		envVar   string
		key      string
		value    string
		expected interface{}
		getter   func(string) interface{}
	}{
		{"BD_JSON", "json", "true", true, func(k string) interface{} { return GetBool(k) }},
		{"BD_NO_DAEMON", "no-daemon", "true", true, func(k string) interface{} { return GetBool(k) }},
		{"BD_ACTOR", "actor", "testuser", "testuser", func(k string) interface{} { return GetString(k) }},
		{"BD_DB", "db", "/tmp/test.db", "/tmp/test.db", func(k string) interface{} { return GetString(k) }},
		{"BEADS_FLUSH_DEBOUNCE", "flush-debounce", "10s", 10 * time.Second, func(k string) interface{} { return GetDuration(k) }},
		{"BEADS_AUTO_START_DAEMON", "auto-start-daemon", "false", false, func(k string) interface{} { return GetBool(k) }},
	}
	
	for _, tt := range tests {
		t.Run(tt.envVar, func(t *testing.T) {
			// Set environment variable
			oldValue := os.Getenv(tt.envVar)
			_ = os.Setenv(tt.envVar, tt.value)
			defer os.Setenv(tt.envVar, oldValue)
			
			// Re-initialize viper to pick up env var
			err := Initialize()
			if err != nil {
				t.Fatalf("Initialize() returned error: %v", err)
			}
			
			got := tt.getter(tt.key)
			if got != tt.expected {
				t.Errorf("GetXXX(%q) with %s=%s = %v, want %v", tt.key, tt.envVar, tt.value, got, tt.expected)
			}
		})
	}
}

func TestConfigFile(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()
	
	// Create a config file
	configContent := `
json: true
no-daemon: true
actor: configuser
flush-debounce: 15s
`
	configPath := filepath.Join(tmpDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}
	
	// Create .beads directory
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	// Move config to .beads directory
	beadsConfigPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.Rename(configPath, beadsConfigPath); err != nil {
		t.Fatalf("failed to move config file: %v", err)
	}

	// Change to tmp directory so config file is discovered
	t.Chdir(tmpDir)

	// Initialize viper
	var err error
	err = Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	// Test that config file values are loaded
	if got := GetBool("json"); got != true {
		t.Errorf("GetBool(json) = %v, want true", got)
	}
	
	if got := GetBool("no-daemon"); got != true {
		t.Errorf("GetBool(no-daemon) = %v, want true", got)
	}
	
	if got := GetString("actor"); got != "configuser" {
		t.Errorf("GetString(actor) = %q, want \"configuser\"", got)
	}
	
	if got := GetDuration("flush-debounce"); got != 15*time.Second {
		t.Errorf("GetDuration(flush-debounce) = %v, want 15s", got)
	}
}

func TestConfigPrecedence(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()
	
	// Create a config file with json: false
	configContent := `json: false`
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}
	
	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}
	
	// Change to tmp directory
	t.Chdir(tmpDir)

	// Test 1: Config file value (json: false)
	var err error
	err = Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	if got := GetBool("json"); got != false {
		t.Errorf("GetBool(json) from config file = %v, want false", got)
	}
	
	// Test 2: Environment variable overrides config file
	_ = os.Setenv("BD_JSON", "true")
	defer func() { _ = os.Unsetenv("BD_JSON") }()
	
	err = Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	if got := GetBool("json"); got != true {
		t.Errorf("GetBool(json) with env var = %v, want true (env should override config)", got)
	}
}

func TestSetAndGet(t *testing.T) {
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}
	
	// Test Set and Get
	Set("test-key", "test-value")
	if got := GetString("test-key"); got != "test-value" {
		t.Errorf("GetString(test-key) = %q, want \"test-value\"", got)
	}
	
	Set("test-bool", true)
	if got := GetBool("test-bool"); got != true {
		t.Errorf("GetBool(test-bool) = %v, want true", got)
	}
	
	Set("test-int", 42)
	if got := GetInt("test-int"); got != 42 {
		t.Errorf("GetInt(test-int) = %d, want 42", got)
	}
}

func TestAllSettings(t *testing.T) {
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	Set("custom-key", "custom-value")

	settings := AllSettings()
	if settings == nil {
		t.Fatal("AllSettings() returned nil")
	}

	// Check that our custom key is in the settings
	if val, ok := settings["custom-key"]; !ok || val != "custom-value" {
		t.Errorf("AllSettings() missing or incorrect custom-key: got %v", val)
	}
}

func TestGetStringSlice(t *testing.T) {
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test with Set
	Set("test-slice", []string{"a", "b", "c"})
	got := GetStringSlice("test-slice")
	if len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" {
		t.Errorf("GetStringSlice(test-slice) = %v, want [a b c]", got)
	}

	// Test with non-existent key - should return empty/nil slice
	got = GetStringSlice("nonexistent-key")
	if len(got) != 0 {
		t.Errorf("GetStringSlice(nonexistent-key) = %v, want empty slice", got)
	}
}

func TestGetStringSliceFromConfig(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()

	// Create a config file with string slice
	configContent := `
repos:
  primary: /path/to/primary
  additional:
    - /path/to/repo1
    - /path/to/repo2
`
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}

	// Change to tmp directory
	t.Chdir(tmpDir)

	// Initialize viper
	var err error
	err = Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test that string slice is loaded correctly
	got := GetStringSlice("repos.additional")
	if len(got) != 2 || got[0] != "/path/to/repo1" || got[1] != "/path/to/repo2" {
		t.Errorf("GetStringSlice(repos.additional) = %v, want [/path/to/repo1 /path/to/repo2]", got)
	}
}

func TestGetMultiRepoConfig(t *testing.T) {
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test when repos.primary is not set (single-repo mode)
	config := GetMultiRepoConfig()
	if config != nil {
		t.Errorf("GetMultiRepoConfig() with no repos.primary = %+v, want nil", config)
	}

	// Test when repos.primary is set (multi-repo mode)
	Set("repos.primary", "/path/to/primary")
	Set("repos.additional", []string{"/path/to/repo1", "/path/to/repo2"})

	config = GetMultiRepoConfig()
	if config == nil {
		t.Fatal("GetMultiRepoConfig() returned nil when repos.primary is set")
	}

	if config.Primary != "/path/to/primary" {
		t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/path/to/primary\"", config.Primary)
	}

	if len(config.Additional) != 2 || config.Additional[0] != "/path/to/repo1" || config.Additional[1] != "/path/to/repo2" {
		t.Errorf("GetMultiRepoConfig().Additional = %v, want [/path/to/repo1 /path/to/repo2]", config.Additional)
	}
}

func TestGetMultiRepoConfigFromFile(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()

	// Create a config file with multi-repo config
	configContent := `
repos:
  primary: /main/repo
  additional:
    - /extra/repo1
    - /extra/repo2
    - /extra/repo3
`
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}

	// Change to tmp directory
	t.Chdir(tmpDir)

	// Initialize viper
	var err error
	err = Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test that multi-repo config is loaded correctly
	config := GetMultiRepoConfig()
	if config == nil {
		t.Fatal("GetMultiRepoConfig() returned nil")
	}

	if config.Primary != "/main/repo" {
		t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/main/repo\"", config.Primary)
	}

	if len(config.Additional) != 3 {
		t.Errorf("GetMultiRepoConfig().Additional has %d items, want 3", len(config.Additional))
	}
}

func TestNilViperBehavior(t *testing.T) {
	// Save the current viper instance
	savedV := v

	// Set viper to nil to test nil-safety
	v = nil
	defer func() { v = savedV }()

	// All getters should return zero values without panicking
	if got := GetString("any-key"); got != "" {
		t.Errorf("GetString with nil viper = %q, want \"\"", got)
	}

	if got := GetBool("any-key"); got != false {
		t.Errorf("GetBool with nil viper = %v, want false", got)
	}

	if got := GetInt("any-key"); got != 0 {
		t.Errorf("GetInt with nil viper = %d, want 0", got)
	}

	if got := GetDuration("any-key"); got != 0 {
		t.Errorf("GetDuration with nil viper = %v, want 0", got)
	}

	if got := GetStringSlice("any-key"); got == nil || len(got) != 0 {
		t.Errorf("GetStringSlice with nil viper = %v, want empty slice", got)
	}

	if got := AllSettings(); got == nil || len(got) != 0 {
		t.Errorf("AllSettings with nil viper = %v, want empty map", got)
	}

	if got := GetMultiRepoConfig(); got != nil {
		t.Errorf("GetMultiRepoConfig with nil viper = %+v, want nil", got)
	}

	// Set should not panic
	Set("any-key", "any-value") // Should be a no-op
}

func TestGetIdentity(t *testing.T) {
	// Initialize viper
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test 1: Flag value takes precedence over everything
	got := GetIdentity("flag-identity")
	if got != "flag-identity" {
		t.Errorf("GetIdentity(flag-identity) = %q, want \"flag-identity\"", got)
	}

	// Test 2: Empty flag falls back to BEADS_IDENTITY env
	oldEnv := os.Getenv("BEADS_IDENTITY")
	_ = os.Setenv("BEADS_IDENTITY", "env-identity")
	defer func() {
		if oldEnv == "" {
			_ = os.Unsetenv("BEADS_IDENTITY")
		} else {
			_ = os.Setenv("BEADS_IDENTITY", oldEnv)
		}
	}()

	// Re-initialize to pick up env var
	_ = Initialize()
	got = GetIdentity("")
	if got != "env-identity" {
		t.Errorf("GetIdentity(\"\") with BEADS_IDENTITY = %q, want \"env-identity\"", got)
	}

	// Test 3: Without flag or env, should fall back to git user.name or hostname
	_ = os.Unsetenv("BEADS_IDENTITY")
	_ = Initialize()
	got = GetIdentity("")
	// We can't predict the exact value (depends on git config and hostname)
	// but it should not be empty or "unknown" on most systems
	if got == "" {
		t.Error("GetIdentity(\"\") without flag or env returned empty string")
	}
}

func TestGetExternalProjects(t *testing.T) {
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test default (empty map)
	got := GetExternalProjects()
	if got == nil {
		t.Error("GetExternalProjects() returned nil, want empty map")
	}
	if len(got) != 0 {
		t.Errorf("GetExternalProjects() = %v, want empty map", got)
	}

	// Test with Set
	Set("external_projects", map[string]string{
		"beads":   "../beads",
		"gastown": "/absolute/path/to/gastown",
	})

	got = GetExternalProjects()
	if len(got) != 2 {
		t.Errorf("GetExternalProjects() has %d items, want 2", len(got))
	}
	if got["beads"] != "../beads" {
		t.Errorf("GetExternalProjects()[beads] = %q, want \"../beads\"", got["beads"])
	}
	if got["gastown"] != "/absolute/path/to/gastown" {
		t.Errorf("GetExternalProjects()[gastown] = %q, want \"/absolute/path/to/gastown\"", got["gastown"])
	}
}

func TestGetExternalProjectsFromConfig(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()

	// Create a config file with external_projects
	configContent := `
external_projects:
  beads: ../beads
  gastown: /path/to/gastown
  other: ./relative/path
`
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}

	// Change to tmp directory
	t.Chdir(tmpDir)

	// Initialize viper
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test that external_projects is loaded correctly
	got := GetExternalProjects()
	if len(got) != 3 {
		t.Errorf("GetExternalProjects() has %d items, want 3", len(got))
	}
	if got["beads"] != "../beads" {
		t.Errorf("GetExternalProjects()[beads] = %q, want \"../beads\"", got["beads"])
	}
	if got["gastown"] != "/path/to/gastown" {
		t.Errorf("GetExternalProjects()[gastown] = %q, want \"/path/to/gastown\"", got["gastown"])
	}
	if got["other"] != "./relative/path" {
		t.Errorf("GetExternalProjects()[other] = %q, want \"./relative/path\"", got["other"])
	}
}

func TestResolveExternalProjectPath(t *testing.T) {
	// Create a temporary directory structure
	tmpDir := t.TempDir()

	// Create a project directory to resolve to
	projectDir := filepath.Join(tmpDir, "beads-project")
	if err := os.MkdirAll(projectDir, 0750); err != nil {
		t.Fatalf("failed to create project directory: %v", err)
	}

	// Create config file
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	configContent := `
external_projects:
  beads: beads-project
  missing: nonexistent-path
  absolute: ` + projectDir + `
`
	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}

	// Change to tmp directory
	t.Chdir(tmpDir)

	// Initialize viper
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test resolving a relative path that exists
	got := ResolveExternalProjectPath("beads")
	if got != projectDir {
		t.Errorf("ResolveExternalProjectPath(beads) = %q, want %q", got, projectDir)
	}

	// Test resolving a path that doesn't exist
	got = ResolveExternalProjectPath("missing")
	if got != "" {
		t.Errorf("ResolveExternalProjectPath(missing) = %q, want empty string", got)
	}

	// Test resolving a project that isn't configured
	got = ResolveExternalProjectPath("unknown")
	if got != "" {
		t.Errorf("ResolveExternalProjectPath(unknown) = %q, want empty string", got)
	}

	// Test resolving an absolute path
	got = ResolveExternalProjectPath("absolute")
	if got != projectDir {
		t.Errorf("ResolveExternalProjectPath(absolute) = %q, want %q", got, projectDir)
	}
}

func TestGetIdentityFromConfig(t *testing.T) {
	// Create a temporary directory for config file
	tmpDir := t.TempDir()

	// Create a config file with identity
	configContent := `identity: config-identity`
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0750); err != nil {
		t.Fatalf("failed to create .beads directory: %v", err)
	}

	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
		t.Fatalf("failed to write config file: %v", err)
	}

	// Clear BEADS_IDENTITY env var
	oldEnv := os.Getenv("BEADS_IDENTITY")
	_ = os.Unsetenv("BEADS_IDENTITY")
	defer func() {
		if oldEnv != "" {
			_ = os.Setenv("BEADS_IDENTITY", oldEnv)
		}
	}()

	// Change to tmp directory
	t.Chdir(tmpDir)

	// Initialize viper
	err := Initialize()
	if err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Test that identity from config file is used
	got := GetIdentity("")
	if got != "config-identity" {
		t.Errorf("GetIdentity(\"\") with config file = %q, want \"config-identity\"", got)
	}

	// Test that flag still takes precedence
	got = GetIdentity("flag-override")
	if got != "flag-override" {
		t.Errorf("GetIdentity(flag-override) = %q, want \"flag-override\"", got)
	}
}

func TestGetValueSource(t *testing.T) {
	// Initialize config
	if err := Initialize(); err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	tests := []struct {
		name     string
		key      string
		setup    func()
		cleanup  func()
		expected ConfigSource
	}{
		{
			name:     "default value returns SourceDefault",
			key:      "json",
			setup:    func() {},
			cleanup:  func() {},
			expected: SourceDefault,
		},
		{
			name: "env var returns SourceEnvVar",
			key:  "json",
			setup: func() {
				os.Setenv("BD_JSON", "true")
			},
			cleanup: func() {
				os.Unsetenv("BD_JSON")
			},
			expected: SourceEnvVar,
		},
		{
			name: "BEADS_ prefixed env var returns SourceEnvVar",
			key:  "identity",
			setup: func() {
				os.Setenv("BEADS_IDENTITY", "test-identity")
			},
			cleanup: func() {
				os.Unsetenv("BEADS_IDENTITY")
			},
			expected: SourceEnvVar,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Reinitialize to clear state
			if err := Initialize(); err != nil {
				t.Fatalf("Initialize() returned error: %v", err)
			}

			tt.setup()
			defer tt.cleanup()

			got := GetValueSource(tt.key)
			if got != tt.expected {
				t.Errorf("GetValueSource(%q) = %v, want %v", tt.key, got, tt.expected)
			}
		})
	}
}

func TestCheckOverrides_FlagOverridesEnvVar(t *testing.T) {
	// Initialize config
	if err := Initialize(); err != nil {
		t.Fatalf("Initialize() returned error: %v", err)
	}

	// Set an env var
	os.Setenv("BD_JSON", "true")
	defer os.Unsetenv("BD_JSON")

	// Simulate flag override
	flagOverrides := map[string]struct {
		Value  interface{}
		WasSet bool
	}{
		"json": {Value: false, WasSet: true},
	}

	overrides := CheckOverrides(flagOverrides)

	// Should detect that flag overrides env var
	found := false
	for _, o := range overrides {
		if o.Key == "json" && o.OverriddenBy == SourceFlag {
			found = true
			break
		}
	}

	if !found {
		t.Error("Expected to find flag override for 'json' key")
	}
}

func TestConfigSourceConstants(t *testing.T) {
	// Verify source constants have expected string values
	if SourceDefault != "default" {
		t.Errorf("SourceDefault = %q, want \"default\"", SourceDefault)
	}
	if SourceConfigFile != "config_file" {
		t.Errorf("SourceConfigFile = %q, want \"config_file\"", SourceConfigFile)
	}
	if SourceEnvVar != "env_var" {
		t.Errorf("SourceEnvVar = %q, want \"env_var\"", SourceEnvVar)
	}
	if SourceFlag != "flag" {
		t.Errorf("SourceFlag = %q, want \"flag\"", SourceFlag)
	}
}
