package config

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

func TestAuthConfigPersistsAndReadsStoredToken(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_TOKEN", "")
	t.Setenv("GITCODE_TOKEN", "")

	cfg := New()
	authCfg := cfg.Authentication()

	changed, err := authCfg.Login("gitcode.com", "tester", "stored-token", "ssh", false)
	if err != nil {
		t.Fatalf("Login() error = %v", err)
	}
	if !changed {
		t.Fatalf("Login() changed = false, want true")
	}

	token, source := authCfg.ActiveToken("gitcode.com")
	if token != "stored-token" || source != "config" {
		t.Fatalf("ActiveToken() = %q, %q", token, source)
	}

	user, err := authCfg.ActiveUser("gitcode.com")
	if err != nil {
		t.Fatalf("ActiveUser() error = %v", err)
	}
	if user != "tester" {
		t.Fatalf("ActiveUser() = %q", user)
	}

	if protocol := cfg.GitProtocol("gitcode.com"); protocol.Value != "ssh" || protocol.Source != "config" {
		t.Fatalf("GitProtocol() = %+v", protocol)
	}
}

func TestAuthConfigEnvironmentOverridesStoredToken(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_TOKEN", "env-token")
	t.Setenv("GITCODE_TOKEN", "")

	cfg := New()
	authCfg := cfg.Authentication()
	if _, err := authCfg.Login("gitcode.com", "tester", "stored-token", "https", false); err != nil {
		t.Fatalf("Login() error = %v", err)
	}

	token, source := authCfg.ActiveToken("gitcode.com")
	if token != "env-token" || source != "GC_TOKEN" {
		t.Fatalf("ActiveToken() = %q, %q", token, source)
	}
}

func TestAuthConfigStoredTokenIgnoresEnvironment(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_TOKEN", "env-token")
	t.Setenv("GITCODE_TOKEN", "")

	cfg := New()
	authCfg := cfg.Authentication()
	if _, err := authCfg.Login("other.example.com", "tester", "stored-token", "https", false); err != nil {
		t.Fatalf("Login() error = %v", err)
	}

	token, source := authCfg.StoredToken("other.example.com")
	if token != "stored-token" || source != "config" {
		t.Fatalf("StoredToken() = %q, %q", token, source)
	}
}

func TestAuthConfigLogoutRemovesStoredToken(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_TOKEN", "")
	t.Setenv("GITCODE_TOKEN", "")

	cfg := New()
	authCfg := cfg.Authentication()
	if _, err := authCfg.Login("gitcode.com", "tester", "stored-token", "https", false); err != nil {
		t.Fatalf("Login() error = %v", err)
	}

	if err := authCfg.Logout("gitcode.com", "tester"); err != nil {
		t.Fatalf("Logout() error = %v", err)
	}

	token, source := authCfg.ActiveToken("gitcode.com")
	if token != "" || source != "" {
		t.Fatalf("ActiveToken() after logout = %q, %q", token, source)
	}
}

func TestAuthConfigLoginRejectsUnsupportedSecureStorage(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())

	cfg := New()
	authCfg := cfg.Authentication()
	changed, err := authCfg.Login("gitcode.com", "tester", "stored-token", "https", true)
	if err == nil {
		t.Fatal("Login() error = nil, want unsupported secure storage error")
	}
	if changed {
		t.Fatal("Login() changed = true, want false")
	}
}

func TestConfigWriteCreatesRestrictedDirectory(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Unix permissions not supported on Windows")
	}
	dir := t.TempDir()
	cfg := &config{configDir: filepath.Join(dir, "gc")}

	if err := os.MkdirAll(cfg.configDir, 0o755); err != nil {
		t.Fatalf("MkdirAll() error = %v", err)
	}

	if err := cfg.Write(); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	info, err := os.Stat(cfg.configDir)
	if err != nil {
		t.Fatalf("Stat() error = %v", err)
	}
	if info.Mode().Perm()&0o077 != 0 {
		t.Fatalf("config dir permissions = %o, want no group/other access", info.Mode().Perm())
	}
}

func TestConfigSetGetAndWritePersistValues(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_EDITOR", "")
	t.Setenv("EDITOR", "")
	t.Setenv("GC_BROWSER", "")

	cfg := New()
	if err := cfg.Set("gitcode.com", "editor", "nano"); err != nil {
		t.Fatalf("Set() error = %v", err)
	}
	if err := cfg.Set("gitcode.com", "browser", "firefox"); err != nil {
		t.Fatalf("Set() error = %v", err)
	}
	if err := cfg.Write(); err != nil {
		t.Fatalf("Write() error = %v", err)
	}

	cfg = New()
	editor, err := cfg.Get("gitcode.com", "editor")
	if err != nil {
		t.Fatalf("Get(editor) error = %v", err)
	}
	if editor != "nano" {
		t.Fatalf("Get(editor) = %q, want nano", editor)
	}
	if got := cfg.Editor("gitcode.com"); got.Value != "nano" || got.Source != "config" {
		t.Fatalf("Editor() = %+v, want config nano", got)
	}
	if got := cfg.Browser("gitcode.com"); got.Value != "firefox" || got.Source != "config" {
		t.Fatalf("Browser() = %+v, want config firefox", got)
	}
}

func TestConfigEnvironmentOverridesStoredValue(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())
	t.Setenv("GC_EDITOR", "code")

	cfg := New()
	if err := cfg.Set("gitcode.com", "editor", "nano"); err != nil {
		t.Fatalf("Set() error = %v", err)
	}

	editor, err := cfg.Get("gitcode.com", "editor")
	if err != nil {
		t.Fatalf("Get(editor) error = %v", err)
	}
	if editor != "code" {
		t.Fatalf("Get(editor) = %q, want environment value code", editor)
	}
}

func TestConfigRejectsUnsupportedKeys(t *testing.T) {
	t.Setenv("GC_CONFIG_DIR", t.TempDir())

	cfg := New()
	if err := cfg.Set("gitcode.com", "token", "secret"); err == nil {
		t.Fatal("Set(token) error = nil, want unsupported config key error")
	}
	if _, err := cfg.Get("gitcode.com", "token"); err == nil {
		t.Fatal("Get(token) error = nil, want unsupported config key error")
	}
}

func TestNormalizeTrustedHost(t *testing.T) {
	tests := []struct {
		name    string
		host    string
		want    string
		wantErr bool
	}{
		{name: "empty defaults", want: "gitcode.com"},
		{name: "trims and lowercases", host: " Enterprise.Example.COM ", want: "enterprise.example.com"},
		{name: "rejects scheme", host: "https://gitcode.com", wantErr: true},
		{name: "rejects path", host: "gitcode.com/path", wantErr: true},
		{name: "rejects port", host: "gitcode.com:8443", wantErr: true},
		{name: "rejects userinfo", host: "user@gitcode.com", wantErr: true},
		{name: "rejects embedded whitespace", host: "git code.com", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := NormalizeTrustedHost(tt.host)
			if (err != nil) != tt.wantErr {
				t.Fatalf("NormalizeTrustedHost() error = %v, wantErr %v", err, tt.wantErr)
			}
			if err == nil && got != tt.want {
				t.Fatalf("NormalizeTrustedHost() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestSecureWriteFileRejectsSymlink(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("symlink semantics differ on Windows")
	}
	dir := t.TempDir()
	target := filepath.Join(dir, "target")
	if err := os.WriteFile(target, []byte("untouched"), 0o600); err != nil {
		t.Fatalf("WriteFile(target) error = %v", err)
	}
	link := filepath.Join(dir, "link")
	if err := os.Symlink(target, link); err != nil {
		t.Fatalf("Symlink() error = %v", err)
	}

	err := secureWriteFile(link, []byte("payload"), 0o600)
	if err == nil {
		t.Fatal("secureWriteFile() error = nil, want symlink rejection")
	}
	if !strings.Contains(err.Error(), "symlink") {
		t.Fatalf("secureWriteFile() error = %q, want mention of symlink", err.Error())
	}

	got, err := os.ReadFile(target)
	if err != nil {
		t.Fatalf("ReadFile(target) error = %v", err)
	}
	if string(got) != "untouched" {
		t.Fatalf("target content = %q, want unchanged", got)
	}
}

func TestSecureWriteFileHardensPermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Unix permissions not supported on Windows")
	}
	dir := t.TempDir()
	path := filepath.Join(dir, "file")
	if err := os.WriteFile(path, []byte("old"), 0o644); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}

	if err := secureWriteFile(path, []byte("new"), 0o600); err != nil {
		t.Fatalf("secureWriteFile() error = %v", err)
	}

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("Stat() error = %v", err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Fatalf("permissions = %o, want 0600", info.Mode().Perm())
	}

	got, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile() error = %v", err)
	}
	if string(got) != "new" {
		t.Fatalf("content = %q, want new", got)
	}
}

func TestSecureWriteFileCreatesNewFileWithRestrictedPermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Unix permissions not supported on Windows")
	}
	dir := t.TempDir()
	path := filepath.Join(dir, "fresh")

	if err := secureWriteFile(path, []byte("payload"), 0o600); err != nil {
		t.Fatalf("secureWriteFile() error = %v", err)
	}

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("Stat() error = %v", err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Fatalf("permissions = %o, want 0600", info.Mode().Perm())
	}
}

func TestSecureWriteFileRejectsDirectory(t *testing.T) {
	dir := t.TempDir()
	target := filepath.Join(dir, "subdir")
	if err := os.Mkdir(target, 0o700); err != nil {
		t.Fatalf("Mkdir() error = %v", err)
	}

	err := secureWriteFile(target, []byte("payload"), 0o600)
	if err == nil {
		t.Fatal("secureWriteFile() error = nil, want failure when path is a directory")
	}
}