package config

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

	fileencoding "reasonix/internal/fileutil/encoding"
)

func TestLoadDotEnvDoesNotImportProjectOrHomeEnv(t *testing.T) {
	cwd := t.TempDir()
	home := t.TempDir()

	if err := os.WriteFile(filepath.Join(cwd, ".env"), []byte("KEY_CWD=from_cwd\nKEY_SHARED=cwd_wins\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(home, ".env"), []byte("KEY_HOME=from_home\nKEY_SHARED=home_loses\n"), 0o600); err != nil {
		t.Fatal(err)
	}

	t.Chdir(cwd)
	t.Setenv("HOME", home)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", home) // os.UserHomeDir reads HOME on Unix and USERPROFILE on Windows.

	// Start clean so the file values are what land (Setenv auto-restores).
	t.Setenv("KEY_CWD", "")
	os.Unsetenv("KEY_CWD")
	t.Setenv("KEY_HOME", "")
	os.Unsetenv("KEY_HOME")
	t.Setenv("KEY_SHARED", "")
	os.Unsetenv("KEY_SHARED")

	loadDotEnv()

	if got := os.Getenv("KEY_CWD"); got != "" {
		t.Errorf("project .env key was imported into process env: KEY_CWD=%q", got)
	}
	if got := os.Getenv("KEY_HOME"); got != "" {
		t.Errorf("home .env key was loaded: KEY_HOME=%q", got)
	}
	if got := os.Getenv("KEY_SHARED"); got != "" {
		t.Errorf("project/home .env shared key was imported: KEY_SHARED=%q", got)
	}
}

// TestLoadDotEnvReadsGlobalCredentials proves `reasonix setup`'s target — the
// reasonix-owned credentials file under Reasonix home — is loaded from any
// working directory and wins over a project ./.env on a shared key.
func TestLoadDotEnvReadsGlobalCredentials(t *testing.T) {
	cwd := t.TempDir()
	cfgHome := t.TempDir()

	t.Chdir(cwd)
	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))

	cred := UserCredentialsPath()
	if cred == "" {
		t.Skip("user config dir unresolved on this platform")
	}
	if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, []byte("KEY_GLOBAL=from_credentials\nKEY_SHARED=global_wins\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(cwd, ".env"), []byte("KEY_SHARED=cwd_wins\n"), 0o600); err != nil {
		t.Fatal(err)
	}

	t.Setenv("KEY_GLOBAL", "")
	os.Unsetenv("KEY_GLOBAL")
	t.Setenv("KEY_SHARED", "")
	os.Unsetenv("KEY_SHARED")

	loadDotEnv()

	if got := os.Getenv("KEY_GLOBAL"); got != "from_credentials" {
		t.Errorf("global credentials not loaded: KEY_GLOBAL=%q want from_credentials", got)
	}
	if got := os.Getenv("KEY_SHARED"); got != "global_wins" {
		t.Errorf("global credentials should win over project .env: KEY_SHARED=%q want global_wins", got)
	}
}

func TestCredentialEnvNamesIncludesUnconfiguredStoredKeys(t *testing.T) {
	home := t.TempDir()
	t.Setenv("REASONIX_HOME", filepath.Join(home, "reasonix-home"))

	credentialPath := UserCredentialsPath()
	if err := os.MkdirAll(filepath.Dir(credentialPath), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(credentialPath, []byte("CONFIGURED_PROVIDER_KEY=configured\nSTALE_PROVIDER_KEY=stale\n"), 0o600); err != nil {
		t.Fatal(err)
	}

	cfg := &Config{Providers: []ProviderEntry{{APIKeyEnv: "CONFIGURED_PROVIDER_KEY"}}}
	got := cfg.CredentialEnvNames()
	for _, want := range []string{"CONFIGURED_PROVIDER_KEY", "STALE_PROVIDER_KEY"} {
		if !containsString(got, want) {
			t.Fatalf("credential env names = %v, missing %s", got, want)
		}
	}
}

func TestLoadDotEnvDecodesGB18030Credentials(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("REASONIX_HOME", filepath.Join(home, "reasonix-home"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("PINNED_CN", "")
	os.Unsetenv("PINNED_CN")

	cred := UserCredentialsPath()
	if err := os.MkdirAll(filepath.Dir(cred), 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, fileencoding.Encode("PINNED_CN=中文\n", fileencoding.GB18030), 0o600); err != nil {
		t.Fatal(err)
	}

	loadDotEnv()
	if got := os.Getenv("PINNED_CN"); got != "中文" {
		t.Fatalf("PINNED_CN = %q, want decoded Chinese value", got)
	}
}

func TestLoadForRootExpandsPluginAuthFromProjectDotEnv(t *testing.T) {
	project := t.TempDir()
	cfgHome := t.TempDir()

	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
	t.Setenv("STRIPE_KEY", "")
	os.Unsetenv("STRIPE_KEY")

	if err := os.WriteFile(filepath.Join(project, ".env"), []byte("STRIPE_KEY=project-token\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, ".mcp.json"), []byte(`{
  "mcpServers": {
    "stripe": {
      "type": "http",
      "url": "https://mcp.example.test",
      "headers": { "Authorization": "Bearer ${STRIPE_KEY}" }
    }
  }
}`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	if len(cfg.Plugins) != 1 {
		t.Fatalf("plugins = %+v", cfg.Plugins)
	}
	got := cfg.Plugins[0].ExpandedPlugin().Headers["Authorization"]
	if got != "Bearer project-token" {
		t.Fatalf("expanded auth header = %q, want project token", got)
	}
	if got := os.Getenv("STRIPE_KEY"); got != "" {
		t.Fatalf("project .env leaked into process env: STRIPE_KEY=%q", got)
	}
}

func TestProjectDotEnvUsesDotenvSyntax(t *testing.T) {
	project := t.TempDir()
	cfgHome := t.TempDir()

	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
	t.Setenv("PLUGIN_TOKEN", "")
	os.Unsetenv("PLUGIN_TOKEN")

	if err := os.WriteFile(filepath.Join(project, ".env"), []byte("export PLUGIN_TOKEN='project token # kept'\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, ".mcp.json"), []byte(`{
  "mcpServers": {
    "svc": {
      "type": "http",
      "url": "https://mcp.example.test",
      "headers": { "Authorization": "Bearer ${PLUGIN_TOKEN}" }
    }
  }
}`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	got := cfg.Plugins[0].ExpandedPlugin().Headers["Authorization"]
	if got != "Bearer project token # kept" {
		t.Fatalf("expanded auth header = %q", got)
	}
	if got := os.Getenv("PLUGIN_TOKEN"); got != "" {
		t.Fatalf("project .env leaked into process env: %q", got)
	}
}

func TestDotEnvFileWarningsReportDuplicateKeysWithoutValues(t *testing.T) {
	path := filepath.Join(t.TempDir(), ".env")
	if err := os.WriteFile(path, []byte("TOKEN=secret-one\nOTHER=ok\nexport TOKEN='secret two'\nOTHER=last\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	file, ok := readDotEnvFile(path)
	if !ok {
		t.Fatal("readDotEnvFile failed")
	}
	if got := strings.Join(file.Duplicates, ","); got != "OTHER,TOKEN" {
		t.Fatalf("Duplicates = %#v", file.Duplicates)
	}
	warnings := strings.Join(file.warnings(), "\n")
	for _, want := range []string{"duplicate .env key OTHER", "duplicate .env key TOKEN", "last parsed value wins"} {
		if !strings.Contains(warnings, want) {
			t.Fatalf("warnings missing %q:\n%s", want, warnings)
		}
	}
	for _, leak := range []string{"secret-one", "secret two"} {
		if strings.Contains(warnings, leak) {
			t.Fatalf("warnings leaked secret value %q:\n%s", leak, warnings)
		}
	}
}

func TestDotEnvFileFilteredPreservesProjectScopeRules(t *testing.T) {
	path := filepath.Join(t.TempDir(), ".env")
	if err := os.WriteFile(path, []byte("PLUGIN_TOKEN=project\nREASONIX_HOME=blocked\nHOME=blocked\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	file, ok := readDotEnvFile(path)
	if !ok {
		t.Fatal("readDotEnvFile failed")
	}
	got := file.filtered(func(key string) bool { return !isProjectDotEnvControlKey(key) })
	if got["PLUGIN_TOKEN"] != "project" {
		t.Fatalf("PLUGIN_TOKEN = %q", got["PLUGIN_TOKEN"])
	}
	if _, ok := got["REASONIX_HOME"]; ok {
		t.Fatalf("REASONIX_HOME should be filtered: %+v", got)
	}
	if _, ok := got["HOME"]; ok {
		t.Fatalf("HOME should be filtered: %+v", got)
	}
}

func TestLoadForRootScopesProjectDotEnvPerWorkspace(t *testing.T) {
	home := t.TempDir()
	projectA := t.TempDir()
	projectB := t.TempDir()

	t.Setenv("HOME", home)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("SHARED_TOKEN", "")
	os.Unsetenv("SHARED_TOKEN")

	for _, tc := range []struct {
		root  string
		value string
	}{
		{projectA, "token-a"},
		{projectB, "token-b"},
	} {
		if err := os.WriteFile(filepath.Join(tc.root, ".env"), []byte("SHARED_TOKEN="+tc.value+"\n"), 0o600); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(tc.root, ".mcp.json"), []byte(`{
  "mcpServers": {
    "svc": {
      "type": "http",
      "url": "https://mcp.example.test",
      "headers": { "Authorization": "Bearer ${SHARED_TOKEN}" }
    }
  }
}`), 0o644); err != nil {
			t.Fatal(err)
		}
	}

	cfgA, err := LoadForRoot(projectA)
	if err != nil {
		t.Fatalf("LoadForRoot A: %v", err)
	}
	cfgB, err := LoadForRoot(projectB)
	if err != nil {
		t.Fatalf("LoadForRoot B: %v", err)
	}
	if got := cfgA.Plugins[0].ExpandedPlugin().Headers["Authorization"]; got != "Bearer token-a" {
		t.Fatalf("project A auth = %q, want token-a", got)
	}
	if got := cfgB.Plugins[0].ExpandedPlugin().Headers["Authorization"]; got != "Bearer token-b" {
		t.Fatalf("project B auth = %q, want token-b", got)
	}
	if got := os.Getenv("SHARED_TOKEN"); got != "" {
		t.Fatalf("project token leaked into process env: %q", got)
	}
}

func TestLoadForRootFiltersProjectDotEnvControlVars(t *testing.T) {
	home := t.TempDir()
	project := t.TempDir()
	redirect := filepath.Join(project, "state")

	t.Setenv("HOME", home)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_STATE_HOME", "")
	os.Unsetenv("REASONIX_STATE_HOME")

	wantCred := UserCredentialsPath()
	if wantCred == "" {
		t.Skip("user credentials path unavailable")
	}
	if err := os.WriteFile(filepath.Join(project, ".env"), []byte("REASONIX_STATE_HOME="+redirect+"\nPLUGIN_TOKEN=project-token\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, ".mcp.json"), []byte(`{
  "mcpServers": {
    "svc": {
      "type": "http",
      "url": "https://mcp.example.test",
      "headers": {
        "Authorization": "Bearer ${PLUGIN_TOKEN}",
        "State": "${REASONIX_STATE_HOME:-default-state}"
      }
    }
  }
}`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	headers := cfg.Plugins[0].ExpandedPlugin().Headers
	if got := headers["Authorization"]; got != "Bearer project-token" {
		t.Fatalf("plugin token = %q, want project token", got)
	}
	if got := headers["State"]; got != "default-state" {
		t.Fatalf("control var expansion = %q, want default-state", got)
	}
	if got := UserCredentialsPath(); got != wantCred {
		t.Fatalf("project .env redirected credentials path: %q want %q", got, wantCred)
	}
	if got := os.Getenv("REASONIX_STATE_HOME"); got != "" {
		t.Fatalf("project control var leaked into process env: %q", got)
	}
}

func TestLoadForRootResolvesProviderCredentialsOverInheritedEnv(t *testing.T) {
	project := t.TempDir()
	cfgHome := t.TempDir()
	key := "KEY_PROVIDER_GLOBAL_PRIORITY"

	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
	t.Setenv(key, "from_env")

	cred := UserCredentialsPath()
	if cred == "" {
		t.Skip("user config dir unresolved on this platform")
	}
	if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, []byte(key+"=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
default_model = "custom/m"
[[providers]]
name = "custom"
kind = "openai"
base_url = "https://example.invalid/v1"
model = "m"
api_key_env = "`+key+`"
`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	provider, ok := cfg.Provider("custom")
	if !ok {
		t.Fatalf("provider missing: %+v", cfg.Providers)
	}
	if got := provider.APIKey(); got != "from_credentials" {
		t.Fatalf("provider API key = %q, want credentials value", got)
	}
	if got := os.Getenv(key); got != "from_credentials" {
		t.Fatalf("process env = %q, want credentials value pinned over inherited env", got)
	}
}

func TestLoadForRootIgnoresProjectProviderEnvAndInheritedEnv(t *testing.T) {
	project := t.TempDir()
	cfgHome := t.TempDir()
	key := "KEY_PROVIDER_PROJECT_PRIORITY"

	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
	t.Setenv(key, "from_env")

	if err := os.WriteFile(filepath.Join(project, ".env"), []byte(key+"=from_project\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
default_model = "custom/m"
[[providers]]
name = "custom"
kind = "openai"
base_url = "https://example.invalid/v1"
model = "m"
api_key_env = "`+key+`"
`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	provider, ok := cfg.Provider("custom")
	if !ok {
		t.Fatalf("provider missing: %+v", cfg.Providers)
	}
	if got := provider.APIKey(); got != "" {
		t.Fatalf("provider API key = %q, want no key without global credentials", got)
	}
	if got := os.Getenv(key); got != "from_env" {
		t.Fatalf("process env = %q, want inherited env left untouched", got)
	}
}

func TestResolveCredentialGlobalFirstDoesNotMutateProjectEnv(t *testing.T) {
	project := t.TempDir()
	home := t.TempDir()

	t.Setenv("HOME", home)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))

	key := "KEY_GLOBAL_PRIORITY"
	if err := os.MkdirAll(filepath.Dir(UserCredentialsPath()), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(UserCredentialsPath(), []byte(key+"=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, ".env"), []byte(key+"=from_project\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv(key, "")
	os.Unsetenv(key)
	t.Setenv(key, "from_project")

	if got := os.Getenv(key); got != "from_project" {
		t.Fatalf("precondition: existing env should be project value, got %q", got)
	}
	got := ResolveCredentialForRootGlobalFirst(project, key)
	if got.Value != "from_credentials" || got.Source.Kind != CredentialSourceCredentials {
		t.Fatalf("credential = %+v, want global credentials for settings display", got)
	}
	if env := os.Getenv(key); env != "from_project" {
		t.Fatalf("global-first resolution mutated process env: %q", env)
	}
}

func TestCredentialResolverCachesGlobalFirstLookups(t *testing.T) {
	project := t.TempDir()
	key := "KEY_SETTINGS_CACHE"
	calls := 0
	stubStoredCredentialValueForTest(t, func(got string) (string, CredentialSource, bool) {
		calls++
		if got != key {
			t.Fatalf("stored credential lookup key = %q, want %q", got, key)
		}
		return "from_credentials", CredentialSource{Kind: CredentialSourceCredentials, Label: "Reasonix credentials"}, true
	})

	resolver := NewCredentialResolverForRoot(project)
	first := resolver.ResolveGlobalFirst(key)
	second := resolver.ResolveGlobalFirst(key)

	if !first.Set || first.Value != "from_credentials" {
		t.Fatalf("first credential = %+v, want stored credential", first)
	}
	if !second.Set || second.Value != "from_credentials" {
		t.Fatalf("second credential = %+v, want cached stored credential", second)
	}
	if calls != 1 {
		t.Fatalf("stored credential lookups = %d, want 1 for repeated key in one resolver", calls)
	}
}

func stubStoredCredentialValueForTest(t *testing.T, fn func(string) (string, CredentialSource, bool)) {
	t.Helper()
	old := storedCredentialValueLookup
	storedCredentialValueLookup = fn
	t.Cleanup(func() {
		storedCredentialValueLookup = old
	})
}

func TestResolveCredentialSourceShowsCredentialsShadowingProjectEnv(t *testing.T) {
	cwd := t.TempDir()
	cfgHome := t.TempDir()

	t.Chdir(cwd)
	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))

	key := "KEY_SOURCE_PROJECT"
	cred := UserCredentialsPath()
	if cred == "" {
		t.Skip("user config dir unresolved on this platform")
	}
	if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, []byte(key+"=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(cwd, ".env"), []byte(key+"=from_project\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv(key, "")
	os.Unsetenv(key)

	loadDotEnv()

	got := ResolveCredentialForRoot(cwd, key)
	if !got.Set || got.Source.Kind != CredentialSourceCredentials {
		t.Fatalf("source = %+v set=%v, want Reasonix credentials", got.Source, got.Set)
	}
	foundProjectShadow := false
	for _, source := range got.Shadowed {
		if source.Kind == CredentialSourceProjectEnv {
			foundProjectShadow = true
		}
	}
	if !foundProjectShadow {
		t.Fatalf("shadowed = %+v, want project .env shadowed by credentials", got.Shadowed)
	}
}

func TestResolveCredentialSourceShowsCredentialsShadowingEmptyProjectEnv(t *testing.T) {
	cwd := t.TempDir()
	cfgHome := t.TempDir()

	t.Chdir(cwd)
	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))

	key := "KEY_SOURCE_EMPTY_PROJECT"
	cred := UserCredentialsPath()
	if cred == "" {
		t.Skip("user config dir unresolved on this platform")
	}
	if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, []byte(key+"=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(cwd, ".env"), []byte(key+"=\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv(key, "")
	os.Unsetenv(key)

	if _, err := StoreCredentialLines([]string{key + "=from_credentials"}); err != nil {
		t.Fatalf("StoreCredentialLines: %v", err)
	}

	got := ResolveCredentialForRoot(cwd, key)
	foundProjectShadow := false
	for _, source := range got.Shadowed {
		if source.Kind == CredentialSourceProjectEnv {
			foundProjectShadow = true
		}
	}
	if !foundProjectShadow {
		t.Fatalf("shadowed = %+v, want empty project .env shadowed by credentials", got.Shadowed)
	}
}

func TestResolveCredentialSourceShowsCredentialsBeforeHomeEnv(t *testing.T) {
	cwd := t.TempDir()
	cfgHome := t.TempDir()

	t.Chdir(cwd)
	t.Setenv("HOME", cfgHome)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", cfgHome)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
	t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))

	key := "KEY_SOURCE_CREDENTIALS"
	cred := UserCredentialsPath()
	if cred == "" {
		t.Skip("user config dir unresolved on this platform")
	}
	if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(cred, []byte(key+"=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(cfgHome, ".env"), []byte(key+"=from_home\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv(key, "")
	os.Unsetenv(key)

	loadDotEnv()

	got := ResolveCredentialForRoot(cwd, key)
	if !got.Set || got.Source.Kind != CredentialSourceCredentials {
		t.Fatalf("source = %+v set=%v, want Reasonix credentials", got.Source, got.Set)
	}
	if got.Value != "from_credentials" {
		t.Fatalf("value = %q, want credentials value", got.Value)
	}
}

func TestStoreCredentialLinesFileMode(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("KEY_FILE_MODE", "")
	os.Unsetenv("KEY_FILE_MODE")

	target, err := StoreCredentialLines([]string{"KEY_FILE_MODE=from_file_store"})
	if err != nil {
		t.Fatalf("StoreCredentialLines: %v", err)
	}
	if target != UserCredentialsPath() {
		t.Fatalf("target = %q, want %q", target, UserCredentialsPath())
	}
	data, err := os.ReadFile(UserCredentialsPath())
	if err != nil {
		t.Fatalf("read credentials file: %v", err)
	}
	if string(data) != "KEY_FILE_MODE=from_file_store\n" {
		t.Fatalf("credentials file = %q", data)
	}
	if got := os.Getenv("KEY_FILE_MODE"); got != "from_file_store" {
		t.Fatalf("process env = %q, want stored value", got)
	}
}

func TestUserCredentialsPathIgnoresReasonixStateHome(t *testing.T) {
	home := t.TempDir()
	state := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_HOME", filepath.Join(home, "reasonix-home"))
	t.Setenv("REASONIX_STATE_HOME", state)

	want := filepath.Join(home, "reasonix-home", ".env")
	if got := UserCredentialsPath(); got != want {
		t.Fatalf("UserCredentialsPath() = %q, want %q", got, want)
	}
	if strings.HasPrefix(UserCredentialsPath(), state) {
		t.Fatalf("credentials path must not live under REASONIX_STATE_HOME: %q", UserCredentialsPath())
	}
}

func TestRemoveCredentialMarksClearedAndSetRemovesMarker(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	if _, err := SetCredential("KEY_REMOVE_MARKER", "old"); err != nil {
		t.Fatalf("SetCredential old: %v", err)
	}
	if err := RemoveCredential("KEY_REMOVE_MARKER"); err != nil {
		t.Fatalf("RemoveCredential: %v", err)
	}
	if CredentialStored("KEY_REMOVE_MARKER") {
		t.Fatal("removed key should not be stored")
	}
	if !credentialCurrentStoreClearedKey("KEY_REMOVE_MARKER") {
		t.Fatal("removed key should leave a cleared marker")
	}
	if _, err := SetCredential("KEY_REMOVE_MARKER", "new"); err != nil {
		t.Fatalf("SetCredential new: %v", err)
	}
	data, err := os.ReadFile(UserCredentialsPath())
	if err != nil {
		t.Fatalf("read credentials: %v", err)
	}
	if got := string(data); got != "KEY_REMOVE_MARKER=new\n" {
		t.Fatalf("credentials after re-set = %q", got)
	}
}

func TestCredentialStoreRevisionChangesForSameLengthReplacement(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	if got := CredentialStoreRevision(); got != "missing" {
		t.Fatalf("initial credential revision = %q, want missing", got)
	}
	if _, err := SetCredential("REVISION_KEY", "old-key"); err != nil {
		t.Fatalf("SetCredential(old): %v", err)
	}
	oldRevision := CredentialStoreRevision()
	if _, err := SetCredential("REVISION_KEY", "new-key"); err != nil {
		t.Fatalf("SetCredential(new): %v", err)
	}
	newRevision := CredentialStoreRevision()
	if oldRevision == newRevision {
		t.Fatalf("same-length replacement kept credential revision %q", oldRevision)
	}
}

func TestSetCredentialIfRevisionRejectsStaleWriter(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	missingRevision := CredentialStoreRevision()
	if _, err := SetCredential("REVISION_CAS_KEY", "newer-key"); err != nil {
		t.Fatalf("SetCredential(newer): %v", err)
	}
	if _, applied, err := SetCredentialIfRevision("REVISION_CAS_KEY", "stale-key", missingRevision); err != nil {
		t.Fatalf("SetCredentialIfRevision(stale): %v", err)
	} else if applied {
		t.Fatal("stale credential revision overwrote a newer value")
	}
	resolved := ResolveCredentialForRootGlobalFirst(".", "REVISION_CAS_KEY")
	if !resolved.Set || resolved.Value != "newer-key" {
		t.Fatalf("credential after stale write = set:%v value:%q, want newer-key", resolved.Set, resolved.Value)
	}
}

func TestSetCredentialIfRevisionAppliesCurrentWriter(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	revision := CredentialStoreRevision()
	if _, applied, err := SetCredentialIfRevision("REVISION_CAS_KEY", "accepted-key", revision); err != nil {
		t.Fatalf("SetCredentialIfRevision(current): %v", err)
	} else if !applied {
		t.Fatal("current credential revision was rejected")
	}
	resolved := ResolveCredentialForRootGlobalFirst(".", "REVISION_CAS_KEY")
	if !resolved.Set || resolved.Value != "accepted-key" {
		t.Fatalf("stored credential = set:%v value:%q, want accepted-key", resolved.Set, resolved.Value)
	}
}

func TestStoreCredentialLinesRejectsUnsafeFileLines(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	_, err := StoreCredentialLines([]string{
		"VALID_KEY=kept",
		"BAD-KEY=ignored",
		"MULTILINE=first\nINJECTED=second",
	})
	if err != nil {
		t.Fatalf("StoreCredentialLines: %v", err)
	}
	data, err := os.ReadFile(UserCredentialsPath())
	if err != nil {
		t.Fatalf("read credentials file: %v", err)
	}
	if string(data) != "VALID_KEY=kept\n" {
		t.Fatalf("credentials file = %q", data)
	}
	if got := os.Getenv("INJECTED"); got != "" {
		t.Fatalf("injected env was set: %q", got)
	}
}

func TestStoreCredentialLinesParsesDotenvQuotes(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	_, err := StoreCredentialLines([]string{`export QUOTED_KEY="value with spaces # kept"`})
	if err != nil {
		t.Fatalf("StoreCredentialLines: %v", err)
	}
	data, err := os.ReadFile(UserCredentialsPath())
	if err != nil {
		t.Fatalf("read credentials file: %v", err)
	}
	if string(data) != "QUOTED_KEY=\"value with spaces # kept\"\n" {
		t.Fatalf("credentials file = %q", data)
	}
	if got := os.Getenv("QUOTED_KEY"); got != "value with spaces # kept" {
		t.Fatalf("env QUOTED_KEY = %q", got)
	}
}

func TestSetCredentialRejectsInvalidInput(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")

	if _, err := SetCredential("BAD-KEY", "value"); err == nil {
		t.Fatal("SetCredential accepted invalid key")
	}
	if _, err := SetCredential("VALID_KEY", "first\nsecond"); err == nil {
		t.Fatal("SetCredential accepted newline value")
	}
	if _, err := os.Stat(UserCredentialsPath()); !os.IsNotExist(err) {
		t.Fatalf("credentials file should not be created for rejected input, stat err=%v", err)
	}
}

func TestProjectConfigCannotOverrideCredentialStoreMode(t *testing.T) {
	home := isolateUserConfigHome(t)
	project := t.TempDir()
	t.Setenv("REASONIX_CREDENTIALS_STORE", "")
	os.Unsetenv("REASONIX_CREDENTIALS_STORE")
	requireTestPathWithin(t, home, UserConfigPath())
	if err := os.MkdirAll(filepath.Dir(UserConfigPath()), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(UserConfigPath(), []byte(`credentials_store = "file"`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`credentials_store = "keyring"`), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg, err := LoadForRoot(project)
	if err != nil {
		t.Fatalf("LoadForRoot: %v", err)
	}
	if cfg.CredentialsStore != CredentialsStoreFile {
		t.Fatalf("CredentialsStore = %q, want file from user config", cfg.CredentialsStore)
	}
}

// TestLoadDotEnvGlobalCredentialsOverrideEnv confirms Reasonix-owned global
// credentials beat inherited environment variables.
func TestLoadDotEnvGlobalCredentialsOverrideEnv(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("PINNED", "from_env")
	if err := os.MkdirAll(filepath.Dir(UserCredentialsPath()), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(UserCredentialsPath(), []byte("PINNED=from_credentials\n"), 0o600); err != nil {
		t.Fatal(err)
	}

	loadDotEnv()

	if got := os.Getenv("PINNED"); got != "from_credentials" {
		t.Errorf("global credentials must win over inherited env: PINNED=%q want from_credentials", got)
	}
}
