package config

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

func TestConfig_SetHost(t *testing.T) {
	cfg := &Config{}

	hostConfig := HostConfig{
		Hostname: "codeberg.org",
		Token:    "test-token",
		User:     "testuser",
	}

	cfg.SetHost("codeberg.org", hostConfig)

	if cfg.Hosts == nil {
		t.Fatal("Hosts map should not be nil")
	}

	if len(cfg.Hosts) != 1 {
		t.Errorf("Expected 1 host, got %d", len(cfg.Hosts))
	}

	host, ok := cfg.Hosts["codeberg.org"]
	if !ok {
		t.Fatal("Host codeberg.org not found")
	}

	if host.Hostname != "codeberg.org" {
		t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname)
	}

	if host.Token != "test-token" {
		t.Errorf("Expected token 'test-token', got '%s'", host.Token)
	}
}

func TestConfig_GetHost(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"codeberg.org": {
				Hostname: "codeberg.org",
				Token:    "test-token",
			},
		},
	}

	host, err := cfg.GetHost("codeberg.org", "", "")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if host.Hostname != "codeberg.org" {
		t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname)
	}

	_, err = cfg.GetHost("nonexistent.org", "", "")
	if err == nil {
		t.Error("Expected error for nonexistent host")
	}
}

func TestGetConfigDir(t *testing.T) {
	dir, err := GetConfigDir()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if dir == "" {
		t.Error("Config directory should not be empty")
	}
}

func TestGetConfigDir_XDG(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", "/custom/config")

	dir, err := GetConfigDir()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	expected := "/custom/config/fj"
	if dir != expected {
		t.Errorf("Expected %q, got %q", expected, dir)
	}
}

func TestGetConfigPath(t *testing.T) {
	path, err := GetConfigPath()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if path == "" {
		t.Error("Config path should not be empty")
	}
}

func TestConfig_SaveAndLoad(t *testing.T) {
	// Create a temp directory for testing
	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "config.yaml")

	// Create a config with test data
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"codeberg.org": {
				Hostname:    "codeberg.org",
				Token:       "test-token-123",
				User:        "testuser",
				GitProtocol: "ssh",
			},
			"github.com": {
				Hostname:    "github.com",
				Token:       "github-token-456",
				User:        "githubuser",
				GitProtocol: "https",
			},
		},
	}

	// Test Save
	err := cfg.SaveToPath(tempFile)
	if err != nil {
		t.Fatalf("Failed to save config: %v", err)
	}

	// Verify file exists
	if _, err := os.Stat(tempFile); os.IsNotExist(err) {
		t.Fatal("Config file was not created")
	}

	// Test Load
	loadedCfg, err := LoadFromPath(tempFile)
	if err != nil {
		t.Fatalf("Failed to load config: %v", err)
	}

	// Verify loaded config matches saved config
	if len(loadedCfg.Hosts) != 2 {
		t.Errorf("Expected 2 hosts, got %d", len(loadedCfg.Hosts))
	}

	// Check codeberg.org host
	codebergHost, ok := loadedCfg.Hosts["codeberg.org"]
	if !ok {
		t.Fatal("codeberg.org host not found in loaded config")
	}
	if codebergHost.Token != "test-token-123" {
		t.Errorf("Expected token 'test-token-123', got '%s'", codebergHost.Token)
	}
	if codebergHost.User != "testuser" {
		t.Errorf("Expected user 'testuser', got '%s'", codebergHost.User)
	}
	if codebergHost.GitProtocol != "ssh" {
		t.Errorf("Expected git_protocol 'ssh', got '%s'", codebergHost.GitProtocol)
	}

	// Check github.com host
	githubHost, ok := loadedCfg.Hosts["github.com"]
	if !ok {
		t.Fatal("github.com host not found in loaded config")
	}
	if githubHost.Token != "github-token-456" {
		t.Errorf("Expected token 'github-token-456', got '%s'", githubHost.Token)
	}
}

func TestConfig_LoadNonexistentFile(t *testing.T) {
	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "nonexistent.yaml")

	// Test Load with nonexistent file
	cfg, err := LoadFromPath(tempFile)
	if err != nil {
		t.Fatalf("Load should not error on nonexistent file: %v", err)
	}

	// Should return empty config
	if cfg == nil {
		t.Fatal("Config should not be nil")
	}

	if cfg.Hosts == nil {
		t.Fatal("Hosts map should be initialized")
	}

	if len(cfg.Hosts) != 0 {
		t.Errorf("Expected empty hosts map, got %d entries", len(cfg.Hosts))
	}
}

func TestConfig_LoadInvalidYAML(t *testing.T) {
	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "invalid.yaml")

	// Write invalid YAML
	err := os.WriteFile(tempFile, []byte("invalid: yaml: content: [[["), 0600)
	if err != nil {
		t.Fatalf("Failed to write invalid YAML: %v", err)
	}

	// Test Load with invalid YAML
	_, err = LoadFromPath(tempFile)
	if err == nil {
		t.Error("Expected error when loading invalid YAML")
	}
}

func TestConfig_SaveCreatesDirectory(t *testing.T) {
	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "subdir", "config.yaml")

	cfg := &Config{
		Hosts: map[string]HostConfig{
			"test.org": {
				Hostname: "test.org",
				Token:    "token",
			},
		},
	}

	// Save should create the directory
	err := cfg.SaveToPath(tempFile)
	if err != nil {
		t.Fatalf("Failed to save config: %v", err)
	}

	// Verify directory was created
	subdir := filepath.Dir(tempFile)
	if _, err := os.Stat(subdir); os.IsNotExist(err) {
		t.Error("Save() should have created the directory")
	}

	// Verify file exists
	if _, err := os.Stat(tempFile); os.IsNotExist(err) {
		t.Error("Config file was not created")
	}
}

func TestConfig_SetHost_NilMap(t *testing.T) {
	cfg := &Config{
		Hosts: nil,
	}

	hostConfig := HostConfig{
		Hostname: "test.org",
		Token:    "test-token",
	}

	// Should not panic even with nil map
	cfg.SetHost("test.org", hostConfig)

	if cfg.Hosts == nil {
		t.Fatal("Hosts map should be initialized")
	}

	if len(cfg.Hosts) != 1 {
		t.Errorf("Expected 1 host, got %d", len(cfg.Hosts))
	}
}

func TestConfig_GetHost_EmptyString(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"codeberg.org": {
				Hostname: "codeberg.org",
				Token:    "test-token",
			},
		},
	}

	// Empty hostname should default to codeberg.org
	host, err := cfg.GetHost("", "", "")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if host.Hostname != "codeberg.org" {
		t.Errorf("Expected default hostname 'codeberg.org', got '%s'", host.Hostname)
	}
}

func TestConfig_GetHost_WhitespaceString(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"codeberg.org": {
				Hostname: "codeberg.org",
				Token:    "test-token",
			},
		},
	}

	// Whitespace-only hostname should default to codeberg.org
	host, err := cfg.GetHost("   ", "", "")
	if err == nil {
		t.Logf("Got host: %+v (this may be expected behavior)", host)
	} else {
		t.Logf("Got error: %v (this may be expected behavior)", err)
	}
}

func TestConfig_SetHost_EmptyToken(t *testing.T) {
	cfg := &Config{}

	hostConfig := HostConfig{
		Hostname: "codeberg.org",
		Token:    "", // Empty token
		User:     "testuser",
	}

	cfg.SetHost("codeberg.org", hostConfig)

	host, err := cfg.GetHost("codeberg.org", "", "")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if host.Token != "" {
		t.Errorf("Expected empty token, got '%s'", host.Token)
	}
}

func TestConfig_SetHost_OverwriteExisting(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"codeberg.org": {
				Hostname: "codeberg.org",
				Token:    "old-token",
				User:     "olduser",
			},
		},
	}

	// Overwrite with new config
	newConfig := HostConfig{
		Hostname: "codeberg.org",
		Token:    "new-token",
		User:     "newuser",
	}

	cfg.SetHost("codeberg.org", newConfig)

	host, err := cfg.GetHost("codeberg.org", "", "")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if host.Token != "new-token" {
		t.Errorf("Expected token 'new-token', got '%s'", host.Token)
	}

	if host.User != "newuser" {
		t.Errorf("Expected user 'newuser', got '%s'", host.User)
	}
}

func TestConfig_MultipleHosts(t *testing.T) {
	cfg := &Config{}

	hosts := []struct {
		hostname string
		token    string
		user     string
	}{
		{"codeberg.org", "token1", "user1"},
		{"github.com", "token2", "user2"},
		{"gitlab.com", "token3", "user3"},
	}

	// Add multiple hosts
	for _, h := range hosts {
		cfg.SetHost(h.hostname, HostConfig{
			Hostname: h.hostname,
			Token:    h.token,
			User:     h.user,
		})
	}

	// Verify all hosts are stored
	if len(cfg.Hosts) != 3 {
		t.Errorf("Expected 3 hosts, got %d", len(cfg.Hosts))
	}

	// Verify each host can be retrieved correctly
	for _, h := range hosts {
		host, err := cfg.GetHost(h.hostname, "", "")
		if err != nil {
			t.Errorf("Failed to get host %s: %v", h.hostname, err)
			continue
		}

		if host.Token != h.token {
			t.Errorf("Host %s: expected token '%s', got '%s'", h.hostname, h.token, host.Token)
		}

		if host.User != h.user {
			t.Errorf("Host %s: expected user '%s', got '%s'", h.hostname, h.user, host.User)
		}
	}
}

func TestConfig_GitProtocol(t *testing.T) {
	cfg := &Config{}

	// Test SSH protocol
	cfg.SetHost("test-ssh.org", HostConfig{
		Hostname:    "test-ssh.org",
		Token:       "token",
		GitProtocol: "ssh",
	})

	// Test HTTPS protocol
	cfg.SetHost("test-https.org", HostConfig{
		Hostname:    "test-https.org",
		Token:       "token",
		GitProtocol: "https",
	})

	// Verify protocols are stored correctly
	sshHost, _ := cfg.GetHost("test-ssh.org", "", "")
	if sshHost.GitProtocol != "ssh" {
		t.Errorf("Expected git_protocol 'ssh', got '%s'", sshHost.GitProtocol)
	}

	httpsHost, _ := cfg.GetHost("test-https.org", "", "")
	if httpsHost.GitProtocol != "https" {
		t.Errorf("Expected git_protocol 'https', got '%s'", httpsHost.GitProtocol)
	}
}

func TestResolveHostByPath(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"forgejo.zerova.net": {
				Hostname:  "forgejo.zerova.net",
				Token:     "token1",
				MatchDirs: []string{"/Users/sid/repos/fj", "/Users/sid/repos/zerova"},
			},
			"codeberg.org": {
				Hostname:  "codeberg.org",
				Token:     "token2",
				MatchDirs: []string{"/"},
			},
			"gitea.example.com": {
				Hostname: "gitea.example.com",
				Token:    "token3",
				// no match_dirs — should never be selected by path
			},
		},
	}

	tests := []struct {
		name string
		cwd  string
		want string
	}{
		{"exact dir match", "/Users/sid/repos/fj", "forgejo.zerova.net"},
		{"nested dir match", "/Users/sid/repos/fj/cmd/root.go", "forgejo.zerova.net"},
		{"second match dir", "/Users/sid/repos/zerova/pkg", "forgejo.zerova.net"},
		{"longest prefix wins over /", "/Users/sid/repos/fj/internal", "forgejo.zerova.net"},
		{"/ as global catch-all", "/tmp", "codeberg.org"},
		{"/ matches root itself", "/", "codeberg.org"},
		{"no match_dirs host not selected", "/some/random/path", "codeberg.org"},
		{"empty cwd returns empty", "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := cfg.ResolveHostByPath(tt.cwd)
			if got != tt.want {
				t.Errorf("ResolveHostByPath(%q) = %q, want %q", tt.cwd, got, tt.want)
			}
		})
	}
}

func TestResolveHostByPath_LongestPrefixAcrossHosts(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"broad.org": {
				Hostname:  "broad.org",
				Token:     "t1",
				MatchDirs: []string{"/Users/sid"},
			},
			"specific.org": {
				Hostname:  "specific.org",
				Token:     "t2",
				MatchDirs: []string{"/Users/sid/repos/myproject"},
			},
		},
	}

	got := cfg.ResolveHostByPath("/Users/sid/repos/myproject/main.go")
	if got != "specific.org" {
		t.Errorf("expected specific.org, got %q", got)
	}

	got = cfg.ResolveHostByPath("/Users/sid/other")
	if got != "broad.org" {
		t.Errorf("expected broad.org, got %q", got)
	}
}

func TestGetHost_MatchDirsIntegration(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"forgejo.zerova.net": {
				Hostname:  "forgejo.zerova.net",
				Token:     "token1",
				MatchDirs: []string{"/Users/sid/repos/fj"},
			},
			"codeberg.org": {
				Hostname: "codeberg.org",
				Token:    "token2",
			},
		},
	}

	// cwd match should resolve to forgejo.zerova.net
	host, err := cfg.GetHost("", "", "/Users/sid/repos/fj/cmd")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if host.Hostname != "forgejo.zerova.net" {
		t.Errorf("expected forgejo.zerova.net, got %s", host.Hostname)
	}

	// no cwd match falls through to codeberg.org default
	host, err = cfg.GetHost("", "", "/tmp")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if host.Hostname != "codeberg.org" {
		t.Errorf("expected codeberg.org, got %s", host.Hostname)
	}
}

func TestResolveHostByPath_TildeExpansion(t *testing.T) {
	home, err := os.UserHomeDir()
	if err != nil {
		t.Skip("cannot determine home directory")
	}

	cfg := &Config{
		Hosts: map[string]HostConfig{
			"tilde.org": {
				Hostname:  "tilde.org",
				Token:     "t1",
				MatchDirs: []string{"~/repos"},
			},
		},
	}

	got := cfg.ResolveHostByPath(filepath.Join(home, "repos", "myproject"))
	if got != "tilde.org" {
		t.Errorf("expected tilde.org, got %q", got)
	}

	got = cfg.ResolveHostByPath(filepath.Join(home, "other"))
	if got != "" {
		t.Errorf("expected empty, got %q", got)
	}
}

func TestResolveHostByPath_TieBreakByConfigOrder(t *testing.T) {
	cfg := &Config{
		Hosts: map[string]HostConfig{
			"second.org": {
				Hostname:  "second.org",
				Token:     "t2",
				MatchDirs: []string{"/shared/path"},
				Order:     1,
			},
			"first.org": {
				Hostname:  "first.org",
				Token:     "t1",
				MatchDirs: []string{"/shared/path"},
				Order:     0,
			},
		},
	}

	got := cfg.ResolveHostByPath("/shared/path/subdir")
	if got != "first.org" {
		t.Errorf("expected first.org (earlier in config), got %q", got)
	}
}

func TestAssignHostOrder(t *testing.T) {
	yamlData := []byte(`hosts:
  alpha.org:
    hostname: alpha.org
    token: t1
  beta.org:
    hostname: beta.org
    token: t2
  gamma.org:
    hostname: gamma.org
    token: t3
`)
	cfg, err := LoadFromPath(writeTempConfig(t, yamlData))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if cfg.Hosts["alpha.org"].Order != 0 {
		t.Errorf("alpha.org order = %d, want 0", cfg.Hosts["alpha.org"].Order)
	}
	if cfg.Hosts["beta.org"].Order != 1 {
		t.Errorf("beta.org order = %d, want 1", cfg.Hosts["beta.org"].Order)
	}
	if cfg.Hosts["gamma.org"].Order != 2 {
		t.Errorf("gamma.org order = %d, want 2", cfg.Hosts["gamma.org"].Order)
	}
}

func writeTempConfig(t *testing.T, data []byte) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, data, 0600); err != nil {
		t.Fatalf("failed to write temp config: %v", err)
	}
	return path
}

func TestExpandHome(t *testing.T) {
	home, err := os.UserHomeDir()
	if err != nil {
		t.Skip("cannot determine home directory")
	}

	tests := []struct {
		input string
		want  string
	}{
		{"~/repos", filepath.Join(home, "repos")},
		{"~", home},
		{"/absolute/path", "/absolute/path"},
		{"relative/path", "relative/path"},
		{"~other", "~other"}, // only ~/... is expanded, not ~user
	}

	for _, tt := range tests {
		got := expandHome(tt.input)
		if got != tt.want {
			t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.want)
		}
	}
}
