package config

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
	"pgregory.net/rapid"
)

// TestDefaultGlobalConfigPath_HonorsHomeChange guards the regression
// where DefaultGlobalConfigPath cached its result with sync.Once. Whichever
// caller fired first pinned the path for the rest of the process — any
// test that later set HOME via t.Setenv silently kept writing into the
// developer's real ~/.config/gortex/config.yaml. The function must
// re-resolve HOME on every call.
func TestDefaultGlobalConfigPath_HonorsHomeChange(t *testing.T) {
	// Pin XDG_CONFIG_HOME empty: when it is set in the ambient
	// environment (the GitHub ubuntu runner sets it), an absolute value
	// wins over $HOME/.config by design and would mask the HOME change
	// this test asserts.
	t.Setenv("XDG_CONFIG_HOME", "")
	homeA := t.TempDir()
	t.Setenv("HOME", homeA)
	gotA := DefaultGlobalConfigPath()
	wantA := filepath.Join(homeA, ".config", "gortex", "config.yaml")
	if gotA != wantA {
		t.Fatalf("first call: got %s, want %s", gotA, wantA)
	}

	homeB := t.TempDir()
	t.Setenv("HOME", homeB)
	gotB := DefaultGlobalConfigPath()
	wantB := filepath.Join(homeB, ".config", "gortex", "config.yaml")
	if gotB != wantB {
		t.Fatalf("after HOME change: got %s, want %s — path appears cached", gotB, wantB)
	}
}

// TestDefaultGlobalConfigPath_HonorsXDGConfigHome verifies the global
// config path is routed through the XDG resolver: an absolute
// $XDG_CONFIG_HOME relocates it, while an unset variable keeps the
// historical $HOME/.config/gortex location so existing installs are
// not orphaned.
func TestDefaultGlobalConfigPath_HonorsXDGConfigHome(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)

	// Unset: historical default.
	t.Setenv("XDG_CONFIG_HOME", "")
	wantUnset := filepath.Join(home, ".config", "gortex", "config.yaml")
	if got := DefaultGlobalConfigPath(); got != wantUnset {
		t.Fatalf("XDG_CONFIG_HOME unset: got %s, want %s", got, wantUnset)
	}

	// Set to an absolute path: honored.
	xdg := t.TempDir()
	t.Setenv("XDG_CONFIG_HOME", xdg)
	wantSet := filepath.Join(xdg, "gortex", "config.yaml")
	if got := DefaultGlobalConfigPath(); got != wantSet {
		t.Fatalf("XDG_CONFIG_HOME set: got %s, want %s", got, wantSet)
	}

	// Relative value: ignored per the XDG spec, fall back to default.
	t.Setenv("XDG_CONFIG_HOME", "relative/path")
	if got := DefaultGlobalConfigPath(); got != wantUnset {
		t.Fatalf("relative XDG_CONFIG_HOME: got %s, want %s (must be ignored)", got, wantUnset)
	}
}

func TestLoadGlobal_FileNotExist(t *testing.T) {
	gc, err := LoadGlobal("/tmp/nonexistent-gortex-test/config.yaml")
	require.NoError(t, err)
	assert.NotNil(t, gc)
	assert.Empty(t, gc.Repos)
	assert.Empty(t, gc.Projects)
	assert.Empty(t, gc.ActiveProject)
}

func TestLoadGlobal_ValidFile(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")

	content := `
active_project: my-project
repos:
  - path: /home/user/repo1
    name: repo1
  - path: /home/user/repo2
projects:
  my-project:
    repos:
      - path: /home/user/repo1
        name: repo1
        ref: work
`
	require.NoError(t, os.WriteFile(path, []byte(content), 0644))

	gc, err := LoadGlobal(path)
	require.NoError(t, err)
	assert.Equal(t, "my-project", gc.ActiveProject)
	assert.Len(t, gc.Repos, 2)
	assert.Equal(t, "repo1", gc.Repos[0].Name)
	assert.Equal(t, "/home/user/repo2", gc.Repos[1].Path)
	assert.Empty(t, gc.Repos[1].Name)
	assert.Len(t, gc.Projects, 1)
	proj := gc.Projects["my-project"]
	assert.Len(t, proj.Repos, 1)
	assert.Equal(t, "work", proj.Repos[0].Ref)
}

func TestLoadGlobal_MalformedYAML(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")
	require.NoError(t, os.WriteFile(path, []byte(":::invalid yaml"), 0644))

	_, err := LoadGlobal(path)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "parsing global config")
}

func TestSave_CreatesDirectoryAndFile(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "subdir", "config.yaml")

	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/home/user/repo1", Name: "repo1"},
		},
		ActiveProject: "test",
	}
	gc.SetConfigPath(path)

	require.NoError(t, gc.Save())

	// Verify file was created.
	data, err := os.ReadFile(path)
	require.NoError(t, err)

	var loaded GlobalConfig
	require.NoError(t, yaml.Unmarshal(data, &loaded))
	assert.Equal(t, "test", loaded.ActiveProject)
	assert.Len(t, loaded.Repos, 1)
	assert.Equal(t, "repo1", loaded.Repos[0].Name)
}

func TestSave_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.yaml")

	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"proj-1": {
				Repos: []RepoEntry{
					{Path: "/a/b", Name: "b", Ref: "work"},
				},
			},
		},
		Repos: []RepoEntry{
			{Path: "/x/y", Name: "y"},
		},
		ActiveProject: "proj-1",
	}
	gc.SetConfigPath(path)
	require.NoError(t, gc.Save())

	loaded, err := LoadGlobal(path)
	require.NoError(t, err)
	assert.Equal(t, gc.ActiveProject, loaded.ActiveProject)
	assert.Equal(t, gc.Repos, loaded.Repos)
	assert.Equal(t, gc.Projects, loaded.Projects)
}

func TestValidate_ValidConfig(t *testing.T) {
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/a", Name: "repo-a"},
			{Path: "/b", Name: "repo-b"},
		},
		Projects: map[string]ProjectConfig{
			"valid-project_1": {
				Repos: []RepoEntry{
					{Path: "/c", Name: "repo-c"},
				},
			},
		},
	}
	assert.NoError(t, gc.Validate())
}

func TestValidate_DuplicatePrefixes(t *testing.T) {
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/a", Name: "same"},
			{Path: "/b", Name: "same"},
		},
	}
	err := gc.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "duplicate repo prefix")
	assert.Contains(t, err.Error(), "same")
}

func TestValidate_DuplicatePrefixesDerived(t *testing.T) {
	// Two repos with same last path component and no explicit name.
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/home/user1/myrepo"},
			{Path: "/home/user2/myrepo"},
		},
	}
	err := gc.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "duplicate repo prefix")
	assert.Contains(t, err.Error(), "myrepo")
}

func TestValidate_InvalidProjectName(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"valid-name":   {Repos: []RepoEntry{}},
			"invalid name": {Repos: []RepoEntry{}},
		},
	}
	err := gc.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid project name")
	assert.Contains(t, err.Error(), "invalid name")
}

func TestValidate_ConflictingNameOverrides(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"proj-a": {
				Repos: []RepoEntry{
					{Path: "/shared/lib", Name: "lib-a"},
				},
			},
			"proj-b": {
				Repos: []RepoEntry{
					{Path: "/shared/lib", Name: "lib-b"},
				},
			},
		},
	}
	err := gc.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "conflicting name overrides")
}

func TestValidate_SharedRepoSameName(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"proj-a": {
				Repos: []RepoEntry{
					{Path: "/shared/lib", Name: "lib"},
				},
			},
			"proj-b": {
				Repos: []RepoEntry{
					{Path: "/shared/lib", Name: "lib"},
				},
			},
		},
	}
	assert.NoError(t, gc.Validate())
}

func TestResolvePrefix(t *testing.T) {
	tests := []struct {
		entry  RepoEntry
		expect string
	}{
		{RepoEntry{Path: "/home/user/gortex", Name: "gortex"}, "gortex"},
		{RepoEntry{Path: "/home/user/gortex"}, "gortex"},
		{RepoEntry{Path: "/home/user/my-lib", Name: ""}, "my-lib"},
		{RepoEntry{Path: "/a/b/c"}, "c"},
	}
	for _, tt := range tests {
		assert.Equal(t, tt.expect, ResolvePrefix(tt.entry))
	}
}

func TestAddRepo(t *testing.T) {
	gc := &GlobalConfig{}

	// Add a repo.
	err := gc.AddRepo(RepoEntry{Path: t.TempDir(), Name: "repo1"})
	require.NoError(t, err)
	assert.Len(t, gc.Repos, 1)

	// Add the same path again — should be skipped.
	err = gc.AddRepo(RepoEntry{Path: gc.Repos[0].Path, Name: "repo1-dup"})
	require.NoError(t, err)
	assert.Len(t, gc.Repos, 1) // still 1

	// Add a different repo.
	err = gc.AddRepo(RepoEntry{Path: t.TempDir(), Name: "repo2"})
	require.NoError(t, err)
	assert.Len(t, gc.Repos, 2)
}

func TestRemoveRepo(t *testing.T) {
	dir1 := t.TempDir()
	dir2 := t.TempDir()

	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: dir1, Name: "repo1"},
			{Path: dir2, Name: "repo2"},
		},
	}

	err := gc.RemoveRepo(dir1)
	require.NoError(t, err)
	assert.Len(t, gc.Repos, 1)
	assert.Equal(t, "repo2", gc.Repos[0].Name)

	// Remove non-existent.
	err = gc.RemoveRepo("/nonexistent")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "repository not found")
}

func TestResolveRepos_TopLevel(t *testing.T) {
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/a", Name: "a"},
			{Path: "/b", Name: "b"},
		},
	}

	repos, err := gc.ResolveRepos("")
	require.NoError(t, err)
	assert.Len(t, repos, 2)
}

func TestResolveRepos_Project(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"my-proj": {
				Repos: []RepoEntry{
					{Path: "/c", Name: "c"},
				},
			},
		},
	}

	repos, err := gc.ResolveRepos("my-proj")
	require.NoError(t, err)
	assert.Len(t, repos, 1)
	assert.Equal(t, "c", repos[0].Name)
}

func TestResolveRepos_ProjectNotFound(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"existing": {Repos: []RepoEntry{}},
		},
	}

	_, err := gc.ResolveRepos("nonexistent")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "project not found")
	assert.Contains(t, err.Error(), "existing")
}

// TestResolveRepos_PerRepoProjectTags verifies that ResolveRepos treats
// per-repo `project: <slug>` annotations as a valid project membership
// when the top-level Projects map has no matching entry. This is the
// shape produced by `gortex track --project <slug>`.
func TestResolveRepos_PerRepoProjectTags(t *testing.T) {
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/a", Name: "a", Project: "alpha"},
			{Path: "/b", Name: "b", Project: "alpha"},
			{Path: "/c", Name: "c", Project: "alpha"},
			{Path: "/d", Name: "d", Project: "beta"},
			{Path: "/e", Name: "e", Project: "beta"},
		},
	}

	alpha, err := gc.ResolveRepos("alpha")
	require.NoError(t, err)
	assert.Len(t, alpha, 3)
	assert.Equal(t, []string{"a", "b", "c"}, []string{alpha[0].Name, alpha[1].Name, alpha[2].Name})

	beta, err := gc.ResolveRepos("beta")
	require.NoError(t, err)
	assert.Len(t, beta, 2)
	assert.Equal(t, []string{"d", "e"}, []string{beta[0].Name, beta[1].Name})

	_, err = gc.ResolveRepos("gamma")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "project not found")
}

// TestResolveRepos_ProjectsMapWinsOverPerRepoTags verifies that when a
// project name is defined both via the top-level Projects map and via
// per-repo annotations on different entries, the explicit Projects map
// definition wins. Preserving this precedence keeps existing configs
// behaving exactly as before.
func TestResolveRepos_ProjectsMapWinsOverPerRepoTags(t *testing.T) {
	gc := &GlobalConfig{
		Projects: map[string]ProjectConfig{
			"alpha": {
				Repos: []RepoEntry{
					{Path: "/from-projects", Name: "from-projects"},
				},
			},
		},
		Repos: []RepoEntry{
			{Path: "/from-tags-1", Name: "from-tags-1", Project: "alpha"},
			{Path: "/from-tags-2", Name: "from-tags-2", Project: "alpha"},
		},
	}

	repos, err := gc.ResolveRepos("alpha")
	require.NoError(t, err)
	require.Len(t, repos, 1)
	assert.Equal(t, "from-projects", repos[0].Name)
}

// TestResolveRepos_BothEmpty verifies the "project not found" error path
// when neither the Projects map nor any per-repo annotation matches.
func TestResolveRepos_BothEmpty(t *testing.T) {
	gc := &GlobalConfig{
		Repos: []RepoEntry{
			{Path: "/a", Name: "a"}, // no Project annotation
		},
	}

	_, err := gc.ResolveRepos("missing")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "project not found")
}

func TestConfigPath_Default(t *testing.T) {
	gc := &GlobalConfig{}
	path := gc.ConfigPath()
	assert.Contains(t, path, "config.yaml")
	assert.Contains(t, path, "gortex")
}

func TestConfigPath_Override(t *testing.T) {
	gc := &GlobalConfig{}
	gc.SetConfigPath("/custom/path.yaml")
	assert.Equal(t, "/custom/path.yaml", gc.ConfigPath())
}

// Feature: multi-repo-support, Property 1: GlobalConfig serialization round-trip

// genRepoEntry generates a random RepoEntry with valid field values.
func genRepoEntry() *rapid.Generator[RepoEntry] {
	return rapid.Custom(func(t *rapid.T) RepoEntry {
		path := rapid.StringMatching(`/[a-z][a-z0-9/]{1,30}`).Draw(t, "path")
		hasName := rapid.Bool().Draw(t, "hasName")
		name := ""
		if hasName {
			name = rapid.StringMatching(`[a-zA-Z0-9_-]{1,20}`).Draw(t, "name")
		}
		hasRef := rapid.Bool().Draw(t, "hasRef")
		ref := ""
		if hasRef {
			ref = rapid.SampledFrom([]string{"work", "personal", "opensource", "dev", "staging"}).Draw(t, "ref")
		}
		return RepoEntry{
			Path: path,
			Name: name,
			Ref:  ref,
		}
	})
}

// genProjectConfig generates a random ProjectConfig with 0-5 repos.
func genProjectConfig() *rapid.Generator[ProjectConfig] {
	return rapid.Custom(func(t *rapid.T) ProjectConfig {
		n := rapid.IntRange(0, 5).Draw(t, "numRepos")
		repos := make([]RepoEntry, n)
		for i := range n {
			repos[i] = genRepoEntry().Draw(t, "repo")
		}
		return ProjectConfig{Repos: repos}
	})
}

// genGlobalConfig generates a random GlobalConfig with projects, repos, and active project.
func genGlobalConfig() *rapid.Generator[GlobalConfig] {
	return rapid.Custom(func(t *rapid.T) GlobalConfig {
		// Generate top-level repos (0-5).
		numRepos := rapid.IntRange(0, 5).Draw(t, "numRepos")
		repos := make([]RepoEntry, numRepos)
		for i := range numRepos {
			repos[i] = genRepoEntry().Draw(t, "repo")
		}

		// Generate projects (0-5) with valid names.
		numProjects := rapid.IntRange(0, 5).Draw(t, "numProjects")
		var projects map[string]ProjectConfig
		var projectNames []string
		if numProjects > 0 {
			projects = make(map[string]ProjectConfig, numProjects)
			for i := 0; i < numProjects; i++ {
				name := rapid.StringMatching(`[a-zA-Z][a-zA-Z0-9_-]{0,19}`).Draw(t, "projectName")
				// Avoid duplicate keys by appending index if needed.
				if _, exists := projects[name]; exists {
					name = name + rapid.StringMatching(`[0-9]{1,3}`).Draw(t, "suffix")
				}
				projects[name] = genProjectConfig().Draw(t, "project")
				projectNames = append(projectNames, name)
			}
		}

		// Active project: empty or one of the project names.
		activeProject := ""
		if len(projectNames) > 0 && rapid.Bool().Draw(t, "hasActiveProject") {
			activeProject = rapid.SampledFrom(projectNames).Draw(t, "activeProject")
		}

		return GlobalConfig{
			Projects:      projects,
			Repos:         repos,
			ActiveProject: activeProject,
		}
	})
}

// TestPropertyGlobalConfigSerializationRoundTrip verifies that serializing a GlobalConfig
// to YAML and deserializing it back produces an equivalent struct.
func TestPropertyGlobalConfigSerializationRoundTrip(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		original := genGlobalConfig().Draw(rt, "globalConfig")

		// Marshal to YAML.
		data, err := yaml.Marshal(&original)
		if err != nil {
			rt.Fatalf("failed to marshal GlobalConfig to YAML: %v", err)
		}

		// Unmarshal back.
		var restored GlobalConfig
		err = yaml.Unmarshal(data, &restored)
		if err != nil {
			rt.Fatalf("failed to unmarshal GlobalConfig from YAML: %v", err)
		}

		// Compare exported fields (configPath has yaml:"-" tag, so it won't round-trip).
		// Normalize nil vs empty for maps and slices.
		if original.Projects == nil {
			original.Projects = nil
		}
		if restored.Projects == nil {
			restored.Projects = nil
		}

		assert.Equal(rt, original.ActiveProject, restored.ActiveProject,
			"ActiveProject mismatch")
		assert.Equal(rt, len(original.Repos), len(restored.Repos),
			"Repos count mismatch")
		for i, want := range original.Repos {
			got := restored.Repos[i]
			assert.Equal(rt, want.Path, got.Path, "Repos[%d].Path mismatch", i)
			assert.Equal(rt, want.Name, got.Name, "Repos[%d].Name mismatch", i)
			assert.Equal(rt, want.Ref, got.Ref, "Repos[%d].Ref mismatch", i)
		}
		assert.Equal(rt, len(original.Projects), len(restored.Projects),
			"Projects count mismatch")
		for name, wantProj := range original.Projects {
			gotProj, ok := restored.Projects[name]
			assert.True(rt, ok, "Project %q missing after round-trip", name)
			if !ok {
				continue
			}
			assert.Equal(rt, len(wantProj.Repos), len(gotProj.Repos),
				"Project %q repos count mismatch", name)
			for i, want := range wantProj.Repos {
				got := gotProj.Repos[i]
				assert.Equal(rt, want.Path, got.Path,
					"Project %q Repos[%d].Path mismatch", name, i)
				assert.Equal(rt, want.Name, got.Name,
					"Project %q Repos[%d].Name mismatch", name, i)
				assert.Equal(rt, want.Ref, got.Ref,
					"Project %q Repos[%d].Ref mismatch", name, i)
			}
		}
	})
}

// Feature: multi-repo-support, Property 13: Project name validation

// genValidProjectName generates a project name matching ^[a-zA-Z0-9_-]+$.
func genValidProjectName() *rapid.Generator[string] {
	return rapid.StringMatching(`[a-zA-Z0-9_-]{1,30}`)
}

// genInvalidProjectName generates a project name containing at least one character outside [a-zA-Z0-9_-].
func genInvalidProjectName() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		// Build a name with a mix of valid chars and at least one invalid char.
		validPart := rapid.StringMatching(`[a-zA-Z0-9_-]{0,10}`).Draw(t, "validPrefix")
		// Pick an invalid character: space, dot, slash, @, !, etc.
		invalidChar := rapid.SampledFrom([]string{" ", ".", "/", "@", "!", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "[", "]", "{", "}", "|", "\\", ":", ";", "'", "\"", "<", ">", ",", "?", "~", "`"}).Draw(t, "invalidChar")
		suffix := rapid.StringMatching(`[a-zA-Z0-9_-]{0,10}`).Draw(t, "validSuffix")
		return validPart + invalidChar + suffix
	})
}

// TestPropertyProjectNameValidation verifies that Validate() rejects project names
// with characters outside [a-zA-Z0-9_-] and accepts valid names.
func TestPropertyProjectNameValidation(t *testing.T) {
	validNameRe := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

	rapid.Check(t, func(rt *rapid.T) {
		// Randomly choose whether to test a valid or invalid name.
		useValid := rapid.Bool().Draw(rt, "useValid")

		var projectName string
		if useValid {
			projectName = genValidProjectName().Draw(rt, "validName")
		} else {
			projectName = genInvalidProjectName().Draw(rt, "invalidName")
		}

		gc := &GlobalConfig{
			Projects: map[string]ProjectConfig{
				projectName: {Repos: []RepoEntry{}},
			},
		}

		err := gc.Validate()

		if validNameRe.MatchString(projectName) {
			// Valid name — Validate should pass (at least no project name error).
			if err != nil {
				assert.NotContains(rt, err.Error(), "invalid project name",
					"valid project name %q should not trigger 'invalid project name' error", projectName)
			}
		} else {
			// Invalid name — Validate must return an error mentioning "invalid project name".
			assert.Error(rt, err, "expected error for invalid project name %q", projectName)
			assert.Contains(rt, err.Error(), "invalid project name",
				"error should mention 'invalid project name' for %q", projectName)
		}
	})
}

// Feature: multi-repo-support, Property 3: Duplicate prefix validation

// TestPropertyDuplicatePrefixValidation verifies that Validate() returns an error
// if and only if two or more RepoEntry values resolve to the same prefix.
func TestPropertyDuplicatePrefixValidation(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		// Step 1: Generate a base list of unique prefixes.
		numEntries := rapid.IntRange(2, 8).Draw(rt, "numEntries")
		prefixes := make([]string, numEntries)
		seen := make(map[string]bool)
		for i := range numEntries {
			for {
				p := rapid.StringMatching(`[a-zA-Z][a-zA-Z0-9_-]{0,14}`).Draw(rt, "prefix")
				if !seen[p] {
					seen[p] = true
					prefixes[i] = p
					break
				}
			}
		}

		// Step 2: Build RepoEntry values with unique paths and explicit names set to the prefix.
		entries := make([]RepoEntry, numEntries)
		for i, p := range prefixes {
			// Use a unique path per entry so paths never collide.
			entries[i] = RepoEntry{
				Path: fmt.Sprintf("/repos/%d/%s", i, p),
				Name: p,
			}
		}

		// Step 3: Optionally inject a duplicate prefix.
		injectDuplicate := rapid.Bool().Draw(rt, "injectDuplicate")
		if injectDuplicate {
			// Pick a random existing prefix and assign it to a different entry.
			srcIdx := rapid.IntRange(0, numEntries-1).Draw(rt, "srcIdx")
			dstIdx := rapid.IntRange(0, numEntries-1).Draw(rt, "dstIdx")
			// Ensure src and dst are different entries.
			if dstIdx == srcIdx {
				dstIdx = (dstIdx + 1) % numEntries
			}
			entries[dstIdx].Name = entries[srcIdx].Name
		}

		// Step 4: Construct GlobalConfig and call Validate().
		gc := &GlobalConfig{
			Repos: entries,
		}
		err := gc.Validate()

		// Step 5: Determine expected outcome by checking for actual duplicates.
		resolvedPrefixes := make(map[string]bool)
		hasDuplicate := false
		for _, e := range entries {
			rp := ResolvePrefix(e)
			if resolvedPrefixes[rp] {
				hasDuplicate = true
				break
			}
			resolvedPrefixes[rp] = true
		}

		if hasDuplicate {
			assert.Error(rt, err, "expected error for duplicate prefixes")
			assert.Contains(rt, err.Error(), "duplicate repo prefix",
				"error should mention 'duplicate repo prefix'")
		} else {
			assert.NoError(rt, err, "expected no error when all prefixes are unique")
		}
	})
}
