package utils

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

func TestCanonicalizePath(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		validate func(t *testing.T, result string)
	}{
		{
			name:  "absolute path",
			input: "/tmp/test",
			validate: func(t *testing.T, result string) {
				if !filepath.IsAbs(result) {
					t.Errorf("expected absolute path, got %q", result)
				}
			},
		},
		{
			name:  "relative path",
			input: ".",
			validate: func(t *testing.T, result string) {
				if !filepath.IsAbs(result) {
					t.Errorf("expected absolute path, got %q", result)
				}
			},
		},
		{
			name:  "current directory",
			input: ".",
			validate: func(t *testing.T, result string) {
				cwd, err := os.Getwd()
				if err != nil {
					t.Fatalf("failed to get cwd: %v", err)
				}
				// Result should be canonical form of current directory
				if !filepath.IsAbs(result) {
					t.Errorf("expected absolute path, got %q", result)
				}
				// The result should be related to cwd (could be same or canonical version)
				if result != cwd {
					// Try to canonicalize cwd to compare
					canonicalCwd, err := filepath.EvalSymlinks(cwd)
					if err == nil && result != canonicalCwd {
						t.Errorf("expected %q or %q, got %q", cwd, canonicalCwd, result)
					}
				}
			},
		},
		{
			name:  "empty path",
			input: "",
			validate: func(t *testing.T, result string) {
				// Empty path should be handled (likely becomes "." then current dir)
				if result == "" {
					t.Error("expected non-empty result for empty input")
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := CanonicalizePath(tt.input)
			tt.validate(t, result)
		})
	}
}

// TestFindJSONLInDir tests that FindJSONLInDir correctly prefers issues.jsonl
// and avoids deletions.jsonl and merge artifacts (bd-tqo fix)
func TestFindJSONLInDir(t *testing.T) {
	tests := []struct {
		name     string
		files    []string
		expected string
	}{
		{
			name:     "only issues.jsonl",
			files:    []string{"issues.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "issues.jsonl and deletions.jsonl - prefers issues",
			files:    []string{"deletions.jsonl", "issues.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "issues.jsonl with merge artifacts - prefers issues",
			files:    []string{"beads.base.jsonl", "beads.left.jsonl", "beads.right.jsonl", "issues.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "beads.jsonl as legacy fallback",
			files:    []string{"beads.jsonl"},
			expected: "beads.jsonl",
		},
		{
			name:     "issues.jsonl preferred over beads.jsonl",
			files:    []string{"beads.jsonl", "issues.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "only deletions.jsonl - returns default issues.jsonl",
			files:    []string{"deletions.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "only interactions.jsonl - returns default issues.jsonl",
			files:    []string{"interactions.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "interactions.jsonl with issues.jsonl - prefers issues",
			files:    []string{"interactions.jsonl", "issues.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "only merge artifacts - returns default issues.jsonl",
			files:    []string{"beads.base.jsonl", "beads.left.jsonl", "beads.right.jsonl"},
			expected: "issues.jsonl",
		},
		{
			name:     "no files - returns default issues.jsonl",
			files:    []string{},
			expected: "issues.jsonl",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpDir, err := os.MkdirTemp("", "bd-findjsonl-test-*")
			if err != nil {
				t.Fatal(err)
			}
			defer os.RemoveAll(tmpDir)

			// Create test files
			for _, file := range tt.files {
				path := filepath.Join(tmpDir, file)
				if err := os.WriteFile(path, []byte("{}"), 0644); err != nil {
					t.Fatal(err)
				}
			}

			result := FindJSONLInDir(tmpDir)
			got := filepath.Base(result)

			if got != tt.expected {
				t.Errorf("FindJSONLInDir() = %q, want %q", got, tt.expected)
			}
		})
	}
}

func TestCanonicalizePathSymlink(t *testing.T) {
	// Create a temporary directory
	tmpDir := t.TempDir()

	// Create a test file
	testFile := filepath.Join(tmpDir, "test.txt")
	if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
		t.Fatalf("failed to create test file: %v", err)
	}

	// Create a symlink to the temp directory
	symlinkPath := filepath.Join(tmpDir, "link")
	if err := os.Symlink(tmpDir, symlinkPath); err != nil {
		t.Skipf("failed to create symlink (may not be supported): %v", err)
	}

	// Canonicalize the symlink path
	result := CanonicalizePath(symlinkPath)

	// The result should be the resolved path (tmpDir), not the symlink
	if result != tmpDir {
		// Try to get canonical form of tmpDir for comparison
		canonicalTmpDir, err := filepath.EvalSymlinks(tmpDir)
		if err != nil {
			t.Fatalf("failed to canonicalize tmpDir: %v", err)
		}
		if result != canonicalTmpDir {
			t.Errorf("expected %q or %q, got %q", tmpDir, canonicalTmpDir, result)
		}
	}
}

func TestResolveForWrite(t *testing.T) {
	t.Run("regular file", func(t *testing.T) {
		tmpDir := t.TempDir()
		file := filepath.Join(tmpDir, "regular.txt")
		if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
			t.Fatal(err)
		}

		got, err := ResolveForWrite(file)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if got != file {
			t.Errorf("got %q, want %q", got, file)
		}
	})

	t.Run("symlink", func(t *testing.T) {
		tmpDir := t.TempDir()
		target := filepath.Join(tmpDir, "target.txt")
		if err := os.WriteFile(target, []byte("test"), 0644); err != nil {
			t.Fatal(err)
		}
		link := filepath.Join(tmpDir, "link.txt")
		if err := os.Symlink(target, link); err != nil {
			t.Fatal(err)
		}

		got, err := ResolveForWrite(link)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		// Resolve target too - on macOS, /var is symlink to /private/var
		wantTarget, _ := filepath.EvalSymlinks(target)
		if got != wantTarget {
			t.Errorf("got %q, want %q", got, wantTarget)
		}
	})

	t.Run("non-existent", func(t *testing.T) {
		tmpDir := t.TempDir()
		newFile := filepath.Join(tmpDir, "new.txt")

		got, err := ResolveForWrite(newFile)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if got != newFile {
			t.Errorf("got %q, want %q", got, newFile)
		}
	})
}

func TestFindMoleculesJSONLInDir(t *testing.T) {
	root := t.TempDir()
	molecules := filepath.Join(root, "molecules.jsonl")
	if err := os.WriteFile(molecules, []byte("[]"), 0o644); err != nil {
		t.Fatalf("failed to create molecules.jsonl: %v", err)
	}

	if got := FindMoleculesJSONLInDir(root); got != molecules {
		t.Fatalf("expected %q, got %q", molecules, got)
	}

	otherDir := t.TempDir()
	if got := FindMoleculesJSONLInDir(otherDir); got != "" {
		t.Fatalf("expected empty path when file missing, got %q", got)
	}
}

func TestNormalizePathForComparison(t *testing.T) {
	t.Run("empty path", func(t *testing.T) {
		result := NormalizePathForComparison("")
		if result != "" {
			t.Errorf("expected empty string for empty input, got %q", result)
		}
	})

	t.Run("absolute path", func(t *testing.T) {
		tmpDir := t.TempDir()
		result := NormalizePathForComparison(tmpDir)
		if !filepath.IsAbs(result) {
			t.Errorf("expected absolute path, got %q", result)
		}
	})

	t.Run("relative path becomes absolute", func(t *testing.T) {
		result := NormalizePathForComparison(".")
		if !filepath.IsAbs(result) {
			t.Errorf("expected absolute path, got %q", result)
		}
	})

	t.Run("symlink resolution", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create a subdirectory
		subDir := filepath.Join(tmpDir, "subdir")
		if err := os.MkdirAll(subDir, 0755); err != nil {
			t.Fatal(err)
		}

		// Create a symlink to the subdirectory
		linkPath := filepath.Join(tmpDir, "link")
		if err := os.Symlink(subDir, linkPath); err != nil {
			t.Skipf("symlink creation failed: %v", err)
		}

		normalizedLink := NormalizePathForComparison(linkPath)
		normalizedSubdir := NormalizePathForComparison(subDir)

		if normalizedLink != normalizedSubdir {
			t.Errorf("symlink and target should normalize to same path: %q vs %q", normalizedLink, normalizedSubdir)
		}
	})

	t.Run("case normalization on case-insensitive systems", func(t *testing.T) {
		if runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
			t.Skip("case normalization only applies to darwin/windows")
		}

		// On macOS/Windows, different case should normalize to same
		tmpDir := t.TempDir()
		lowerCase := strings.ToLower(tmpDir)
		upperCase := strings.ToUpper(tmpDir)

		normalizedLower := NormalizePathForComparison(lowerCase)
		normalizedUpper := NormalizePathForComparison(upperCase)

		if normalizedLower != normalizedUpper {
			t.Errorf("case-insensitive paths should normalize to same value: %q vs %q", normalizedLower, normalizedUpper)
		}
	})
}

func TestCanonicalizePathCase(t *testing.T) {
	if runtime.GOOS != "darwin" {
		t.Skip("case canonicalization test only runs on macOS")
	}

	// Create a directory with mixed case
	tmpDir := t.TempDir()
	mixedCaseDir := filepath.Join(tmpDir, "TestCase")
	if err := os.MkdirAll(mixedCaseDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Access via wrong case (lowercase)
	wrongCasePath := filepath.Join(tmpDir, "testcase")

	// Verify the wrong case path exists (macOS case-insensitive)
	if _, err := os.Stat(wrongCasePath); err != nil {
		t.Fatalf("wrong case path should exist on macOS: %v", err)
	}

	// CanonicalizePath should return the correct case
	result := CanonicalizePath(wrongCasePath)

	// The result should have the correct case "TestCase", not "testcase"
	if !strings.HasSuffix(result, "TestCase") {
		t.Errorf("CanonicalizePath(%q) = %q, want path ending in 'TestCase'", wrongCasePath, result)
	}
}

func TestPathsEqual(t *testing.T) {
	t.Run("identical paths", func(t *testing.T) {
		tmpDir := t.TempDir()
		if !PathsEqual(tmpDir, tmpDir) {
			t.Error("identical paths should be equal")
		}
	})

	t.Run("empty paths", func(t *testing.T) {
		if !PathsEqual("", "") {
			t.Error("two empty paths should be equal")
		}
	})

	t.Run("one empty path", func(t *testing.T) {
		if PathsEqual("/tmp/foo", "") {
			t.Error("non-empty and empty paths should not be equal")
		}
	})

	t.Run("different paths", func(t *testing.T) {
		if PathsEqual("/tmp/foo", "/tmp/bar") {
			t.Error("different paths should not be equal")
		}
	})

	t.Run("symlink equality", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create a subdirectory
		subDir := filepath.Join(tmpDir, "subdir")
		if err := os.MkdirAll(subDir, 0755); err != nil {
			t.Fatal(err)
		}

		// Create a symlink to the subdirectory
		linkPath := filepath.Join(tmpDir, "link")
		if err := os.Symlink(subDir, linkPath); err != nil {
			t.Skipf("symlink creation failed: %v", err)
		}

		if !PathsEqual(linkPath, subDir) {
			t.Error("symlink and target should be equal")
		}
	})

	t.Run("case-insensitive equality on macOS/Windows (GH#869)", func(t *testing.T) {
		if runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
			t.Skip("case normalization only applies to darwin/windows")
		}

		// This is the actual bug from GH#869: Desktop vs desktop
		tmpDir := t.TempDir()

		// Create a subdirectory with mixed case
		mixedCase := filepath.Join(tmpDir, "Desktop")
		if err := os.MkdirAll(mixedCase, 0755); err != nil {
			t.Fatal(err)
		}

		// The lowercase version should still refer to the same directory
		lowerCase := filepath.Join(tmpDir, "desktop")

		if !PathsEqual(mixedCase, lowerCase) {
			t.Errorf("paths with different case should be equal on case-insensitive FS: %q vs %q", mixedCase, lowerCase)
		}
	})
}
