package main

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

func TestInitialSync_BasicFiles(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "a.txt", "hello")
	writeFile(t, host, "b.txt", "world")
	writeFile(t, host, "sub/c.txt", "nested")

	stack := NewGitignoreStack(host)

	manifest, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "a.txt", "hello")
	assertFileExists(t, data, "b.txt", "world")
	assertFileExists(t, data, "sub/c.txt", "nested")
	assertManifestContains(t, manifest, "a.txt", "b.txt", "sub/c.txt")
}

func TestInitialSync_GitignoreExcludes(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "*.log\nbuild/\n")
	writeFile(t, host, "app.go", "package main")
	writeFile(t, host, "debug.log", "logs here")
	writeFile(t, host, "build/output.o", "binary")

	stack := NewGitignoreStack(host)

	manifest, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "app.go", "package main")
	assertFileNotExists(t, data, "debug.log")
	assertFileNotExists(t, data, "build/output.o")
	assertManifestContains(t, manifest, "app.go")
	assertManifestNotContains(t, manifest, "debug.log", "build/output.o")
}

func TestInitialSync_NegationPattern(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "*.log\n!important.log\n")
	writeFile(t, host, "debug.log", "debug")
	writeFile(t, host, "important.log", "important")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileNotExists(t, data, "debug.log")
	assertFileExists(t, data, "important.log", "important")
}

func TestInitialSync_DoubleStar(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "**/temp\n")
	writeFile(t, host, "src/main.go", "code")
	writeFile(t, host, "temp/cache", "cached")
	writeFile(t, host, "a/b/temp/data", "temp data")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/main.go", "code")
	assertFileNotExists(t, data, "temp/cache")
	assertFileNotExists(t, data, "a/b/temp/data")
}

func TestInitialSync_DirectoryPattern(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "node_modules/\n")
	writeFile(t, host, "src/app.js", "code")
	writeFile(t, host, "node_modules/pkg/index.js", "pkg")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/app.js", "code")
	assertFileNotExists(t, data, "node_modules/pkg/index.js")
}

func TestInitialSync_Symlinks(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "target.txt", "target content")
	if err := os.Symlink("target.txt", filepath.Join(host, "link.txt")); err != nil {
		t.Fatal(err)
	}

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	linkPath := filepath.Join(data, "link.txt")
	info, err := os.Lstat(linkPath)
	if err != nil {
		t.Fatal(err)
	}

	if info.Mode()&os.ModeSymlink == 0 {
		t.Error("expected symlink, got regular file")
	}

	target, err := os.Readlink(linkPath)
	if err != nil {
		t.Fatal(err)
	}

	if target != "target.txt" {
		t.Errorf("symlink target = %q, want %q", target, "target.txt")
	}
}

func TestInitialSync_PreservesPermissions(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "script.sh", "#!/bin/sh\necho hi")
	os.Chmod(filepath.Join(host, "script.sh"), 0755)

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	info, err := os.Stat(filepath.Join(data, "script.sh"))
	if err != nil {
		t.Fatal(err)
	}

	if info.Mode().Perm() != 0755 {
		t.Errorf("permissions = %v, want 0755", info.Mode().Perm())
	}
}

func TestInitialSync_DryRun(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "a.txt", "hello")
	writeFile(t, host, ".gitignore", "*.log\n")
	writeFile(t, host, "debug.log", "logs")

	stack := NewGitignoreStack(host)

	manifest, err := initialSync(host, data, stack, true)
	if err != nil {
		t.Fatal(err)
	}

	entries := listDir(t, data)
	if len(entries) != 0 {
		t.Errorf("dry run created files: %v", entries)
	}

	assertManifestContains(t, manifest, "a.txt")
}

func TestInitialSync_SkipsGit(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "src/main.go", "code")
	writeFile(t, host, ".git/config", "git config")
	writeFile(t, host, ".git/objects/abc", "obj")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/main.go", "code")
	assertFileNotExists(t, data, ".git/config")
}

func TestInitialSync_NestedGitignore(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "*.log\n")
	writeFile(t, host, "src/.gitignore", "*.tmp\n")
	writeFile(t, host, "src/app.go", "code")
	writeFile(t, host, "src/debug.log", "log")
	writeFile(t, host, "src/cache.tmp", "tmp")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/app.go", "code")
	assertFileNotExists(t, data, "src/debug.log")
	assertFileNotExists(t, data, "src/cache.tmp")
}

func TestInitialSync_ComplexGitignore(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	gitignore := `# Build outputs
*.o
*.a
build/

# Logs
*.log

# Temp files
**/temp
*.tmp

# But keep important ones
!important.log
!lib/special.o
`
	writeFile(t, host, ".gitignore", gitignore)
	writeFile(t, host, "src/main.c", "code")
	writeFile(t, host, "src/main.o", "object")
	writeFile(t, host, "lib.a", "archive")
	writeFile(t, host, "build/out.o", "built")
	writeFile(t, host, "app.log", "log")
	writeFile(t, host, "important.log", "important")
	writeFile(t, host, "lib/special.o", "special")
	writeFile(t, host, "temp/cache", "cached")
	writeFile(t, host, "a/b/temp/data", "temp")
	writeFile(t, host, "file.tmp", "tmp")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/main.c", "code")
	assertFileExists(t, data, "important.log", "important")
	assertFileExists(t, data, "lib/special.o", "special")

	assertFileNotExists(t, data, "src/main.o")
	assertFileNotExists(t, data, "lib.a")
	assertFileNotExists(t, data, "build/out.o")
	assertFileNotExists(t, data, "app.log")
	assertFileNotExists(t, data, "temp/cache")
	assertFileNotExists(t, data, "a/b/temp/data")
	assertFileNotExists(t, data, "file.tmp")
}

func TestInitialSync_NestedGitignore_Scoped(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "")
	writeFile(t, host, "a/.gitignore", "*.log\n")
	writeFile(t, host, "b/.gitignore", "*.tmp\n")

	writeFile(t, host, "a/debug.log", "log")
	writeFile(t, host, "a/file.go", "code")
	writeFile(t, host, "b/cache.tmp", "tmp")
	writeFile(t, host, "b/file.go", "code")
	writeFile(t, host, "c/debug.log", "log")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileNotExists(t, data, "a/debug.log")
	assertFileExists(t, data, "a/file.go", "code")
	assertFileNotExists(t, data, "b/cache.tmp")
	assertFileExists(t, data, "b/file.go", "code")
	assertFileExists(t, data, "c/debug.log", "log")
}

func TestInitialSync_NestedGitignore_DirScoped(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "build/\n")
	writeFile(t, host, "src/.gitignore", "dist/\n")
	writeFile(t, host, "src/app.js", "code")
	writeFile(t, host, "src/dist/bundle.js", "bundle")
	writeFile(t, host, "build/output.o", "built")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "src/app.js", "code")
	assertFileNotExists(t, data, "src/dist/bundle.js")
	assertFileNotExists(t, data, "build/output.o")
}

func TestInitialSync_NestedGitignore_NegationOverride(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "*.txt\n")
	writeFile(t, host, "sub/.gitignore", "!important.txt\n")
	writeFile(t, host, "sub/readme.txt", "readme")
	writeFile(t, host, "sub/important.txt", "important")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileNotExists(t, data, "sub/readme.txt")
	assertFileExists(t, data, "sub/important.txt", "important")
}

func TestInitialSync_NestedGitignore_DeepNesting(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, ".gitignore", "*.log\n")
	writeFile(t, host, "a/.gitignore", "*.tmp\n")
	writeFile(t, host, "a/b/.gitignore", "*.bak\n")

	writeFile(t, host, "a/b/c/debug.log", "log")
	writeFile(t, host, "a/b/c/cache.tmp", "tmp")
	writeFile(t, host, "a/b/c/backup.bak", "bak")
	writeFile(t, host, "a/b/c/file.go", "code")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileNotExists(t, data, "a/b/c/debug.log")
	assertFileNotExists(t, data, "a/b/c/cache.tmp")
	assertFileNotExists(t, data, "a/b/c/backup.bak")
	assertFileExists(t, data, "a/b/c/file.go", "code")
}

func TestInitialSync_NestedGitignore_ParentScopeDoesntLeak(t *testing.T) {
	host := t.TempDir()
	data := t.TempDir()

	writeFile(t, host, "parent/.gitignore", "*.log\n")
	writeFile(t, host, "sibling/file.go", "code")
	writeFile(t, host, "sibling/debug.log", "log")

	stack := NewGitignoreStack(host)

	_, err := initialSync(host, data, stack, false)
	if err != nil {
		t.Fatal(err)
	}

	assertFileExists(t, data, "sibling/file.go", "code")
	assertFileExists(t, data, "sibling/debug.log", "log")
}

// Helper functions

func writeFile(t *testing.T, dir, path, content string) {
	t.Helper()
	full := filepath.Join(dir, path)
	if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(full, []byte(content), 0644); err != nil {
		t.Fatal(err)
	}
}

func assertFileExists(t *testing.T, dir, path, expectedContent string) {
	t.Helper()
	full := filepath.Join(dir, path)
	data, err := os.ReadFile(full)
	if err != nil {
		t.Errorf("file %s does not exist: %v", path, err)
		return
	}
	if string(data) != expectedContent {
		t.Errorf("file %s content = %q, want %q", path, string(data), expectedContent)
	}
}

func assertFileNotExists(t *testing.T, dir, path string) {
	t.Helper()
	full := filepath.Join(dir, path)
	if _, err := os.Stat(full); err == nil {
		t.Errorf("file %s should not exist but does", path)
	}
}

func assertManifestContains(t *testing.T, manifest *Manifest, paths ...string) {
	t.Helper()
	for _, p := range paths {
		if _, ok := manifest.Files[p]; !ok {
			t.Errorf("manifest missing path %q", p)
		}
	}
}

func assertManifestNotContains(t *testing.T, manifest *Manifest, paths ...string) {
	t.Helper()
	for _, p := range paths {
		if _, ok := manifest.Files[p]; ok {
			t.Errorf("manifest should not contain path %q", p)
		}
	}
}

func listDir(t *testing.T, dir string) []string {
	t.Helper()
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatal(err)
	}
	var names []string
	for _, e := range entries {
		names = append(names, e.Name())
	}
	sort.Strings(names)
	return names
}
