package tspath

import (
	"fmt"
	"regexp"
	"strings"
	"testing"

	"gotest.tools/v3/assert"
)

func TestNormalizeSlashes(t *testing.T) {
	t.Parallel()
	assert.Equal(t, NormalizeSlashes("a"), "a")
	assert.Equal(t, NormalizeSlashes("a/b"), "a/b")
	assert.Equal(t, NormalizeSlashes("a\\b"), "a/b")
	assert.Equal(t, NormalizeSlashes("\\\\server\\path"), "//server/path")
}

func TestGetRootLength(t *testing.T) {
	t.Parallel()
	assert.Equal(t, GetRootLength("a"), 0)
	assert.Equal(t, GetRootLength("/"), 1)
	assert.Equal(t, GetRootLength("/path"), 1)
	assert.Equal(t, GetRootLength("c:"), 2)
	assert.Equal(t, GetRootLength("c:d"), 0)
	assert.Equal(t, GetRootLength("c:/"), 3)
	assert.Equal(t, GetRootLength("c:\\"), 3)
	assert.Equal(t, GetRootLength("//server"), 8)
	assert.Equal(t, GetRootLength("//server/share"), 9)
	assert.Equal(t, GetRootLength("\\\\server"), 8)
	assert.Equal(t, GetRootLength("\\\\server\\share"), 9)
	assert.Equal(t, GetRootLength("file:///"), 8)
	assert.Equal(t, GetRootLength("file:///path"), 8)
	assert.Equal(t, GetRootLength("file:///c:"), 10)
	assert.Equal(t, GetRootLength("file:///c:d"), 8)
	assert.Equal(t, GetRootLength("file:///c:/path"), 11)
	assert.Equal(t, GetRootLength("file:///c%3a"), 12)
	assert.Equal(t, GetRootLength("file:///c%3ad"), 8)
	assert.Equal(t, GetRootLength("file:///c%3a/path"), 13)
	assert.Equal(t, GetRootLength("file:///c%3A"), 12)
	assert.Equal(t, GetRootLength("file:///c%3Ad"), 8)
	assert.Equal(t, GetRootLength("file:///c%3A/path"), 13)
	assert.Equal(t, GetRootLength("file://localhost"), 16)
	assert.Equal(t, GetRootLength("file://localhost/"), 17)
	assert.Equal(t, GetRootLength("file://localhost/path"), 17)
	assert.Equal(t, GetRootLength("file://localhost/c:"), 19)
	assert.Equal(t, GetRootLength("file://localhost/c:d"), 17)
	assert.Equal(t, GetRootLength("file://localhost/c:/path"), 20)
	assert.Equal(t, GetRootLength("file://localhost/c%3a"), 21)
	assert.Equal(t, GetRootLength("file://localhost/c%3ad"), 17)
	assert.Equal(t, GetRootLength("file://localhost/c%3a/path"), 22)
	assert.Equal(t, GetRootLength("file://localhost/c%3A"), 21)
	assert.Equal(t, GetRootLength("file://localhost/c%3Ad"), 17)
	assert.Equal(t, GetRootLength("file://localhost/c%3A/path"), 22)
	assert.Equal(t, GetRootLength("file://server"), 13)
	assert.Equal(t, GetRootLength("file://server/"), 14)
	assert.Equal(t, GetRootLength("file://server/path"), 14)
	assert.Equal(t, GetRootLength("file://server/c:"), 14)
	assert.Equal(t, GetRootLength("file://server/c:d"), 14)
	assert.Equal(t, GetRootLength("file://server/c:/d"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3a"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3ad"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3a/d"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3A"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3Ad"), 14)
	assert.Equal(t, GetRootLength("file://server/c%3A/d"), 14)
	assert.Equal(t, GetRootLength("http://server"), 13)
	assert.Equal(t, GetRootLength("http://server/path"), 14)
}

func TestPathIsAbsolute(t *testing.T) {
	t.Parallel()
	// POSIX
	assert.Equal(t, PathIsAbsolute("/path/to/file.ext"), true)
	// DOS
	assert.Equal(t, PathIsAbsolute("c:/path/to/file.ext"), true)
	// URL
	assert.Equal(t, PathIsAbsolute("file:///path/to/file.ext"), true)
	// Non-absolute
	assert.Equal(t, PathIsAbsolute("path/to/file.ext"), false)
	assert.Equal(t, PathIsAbsolute("./path/to/file.ext"), false)
}

func TestIsUrl(t *testing.T) {
	t.Parallel()
	assert.Equal(t, IsUrl("a"), false)
	assert.Equal(t, IsUrl("/"), false)
	assert.Equal(t, IsUrl("c:"), false)
	assert.Equal(t, IsUrl("c:d"), false)
	assert.Equal(t, IsUrl("c:/"), false)
	assert.Equal(t, IsUrl("c:\\"), false)
	assert.Equal(t, IsUrl("//server"), false)
	assert.Equal(t, IsUrl("//server/share"), false)
	assert.Equal(t, IsUrl("\\\\server"), false)
	assert.Equal(t, IsUrl("\\\\server\\share"), false)

	assert.Equal(t, IsUrl("file:///path"), true)
	assert.Equal(t, IsUrl("file:///c:"), true)
	assert.Equal(t, IsUrl("file:///c:d"), true)
	assert.Equal(t, IsUrl("file:///c:/path"), true)
	assert.Equal(t, IsUrl("file://server"), true)
	assert.Equal(t, IsUrl("file://server/path"), true)
	assert.Equal(t, IsUrl("http://server"), true)
	assert.Equal(t, IsUrl("http://server/path"), true)
}

func TestIsRootedDiskPath(t *testing.T) {
	t.Parallel()
	assert.Equal(t, IsRootedDiskPath("a"), false)
	assert.Equal(t, IsRootedDiskPath("/"), true)
	assert.Equal(t, IsRootedDiskPath("c:"), true)
	assert.Equal(t, IsRootedDiskPath("c:d"), false)
	assert.Equal(t, IsRootedDiskPath("c:/"), true)
	assert.Equal(t, IsRootedDiskPath("c:\\"), true)
	assert.Equal(t, IsRootedDiskPath("//server"), true)
	assert.Equal(t, IsRootedDiskPath("//server/share"), true)
	assert.Equal(t, IsRootedDiskPath("\\\\server"), true)
	assert.Equal(t, IsRootedDiskPath("\\\\server\\share"), true)
	assert.Equal(t, IsRootedDiskPath("file:///path"), false)
	assert.Equal(t, IsRootedDiskPath("file:///c:"), false)
	assert.Equal(t, IsRootedDiskPath("file:///c:d"), false)
	assert.Equal(t, IsRootedDiskPath("file:///c:/path"), false)
	assert.Equal(t, IsRootedDiskPath("file://server"), false)
	assert.Equal(t, IsRootedDiskPath("file://server/path"), false)
	assert.Equal(t, IsRootedDiskPath("http://server"), false)
	assert.Equal(t, IsRootedDiskPath("http://server/path"), false)
}

func TestGetDirectoryPath(t *testing.T) {
	t.Parallel()
	assert.Equal(t, GetDirectoryPath(""), "")
	assert.Equal(t, GetDirectoryPath("a"), "")
	assert.Equal(t, GetDirectoryPath("a/b"), "a")
	assert.Equal(t, GetDirectoryPath("/"), "/")
	assert.Equal(t, GetDirectoryPath("/a"), "/")
	assert.Equal(t, GetDirectoryPath("/a/"), "/")
	assert.Equal(t, GetDirectoryPath("/a/b"), "/a")
	assert.Equal(t, GetDirectoryPath("/a/b/"), "/a")
	assert.Equal(t, GetDirectoryPath("c:"), "c:")
	assert.Equal(t, GetDirectoryPath("c:d"), "")
	assert.Equal(t, GetDirectoryPath("c:/"), "c:/")
	assert.Equal(t, GetDirectoryPath("c:/path"), "c:/")
	assert.Equal(t, GetDirectoryPath("c:/path/"), "c:/")
	assert.Equal(t, GetDirectoryPath("//server"), "//server")
	assert.Equal(t, GetDirectoryPath("//server/"), "//server/")
	assert.Equal(t, GetDirectoryPath("//server/share"), "//server/")
	assert.Equal(t, GetDirectoryPath("//server/share/"), "//server/")
	assert.Equal(t, GetDirectoryPath("\\\\server"), "//server")
	assert.Equal(t, GetDirectoryPath("\\\\server\\"), "//server/")
	assert.Equal(t, GetDirectoryPath("\\\\server\\share"), "//server/")
	assert.Equal(t, GetDirectoryPath("\\\\server\\share\\"), "//server/")
	assert.Equal(t, GetDirectoryPath("file:///"), "file:///")
	assert.Equal(t, GetDirectoryPath("file:///path"), "file:///")
	assert.Equal(t, GetDirectoryPath("file:///path/"), "file:///")
	assert.Equal(t, GetDirectoryPath("file:///c:"), "file:///c:")
	assert.Equal(t, GetDirectoryPath("file:///c:d"), "file:///")
	assert.Equal(t, GetDirectoryPath("file:///c:/"), "file:///c:/")
	assert.Equal(t, GetDirectoryPath("file:///c:/path"), "file:///c:/")
	assert.Equal(t, GetDirectoryPath("file:///c:/path/"), "file:///c:/")
	assert.Equal(t, GetDirectoryPath("file://server"), "file://server")
	assert.Equal(t, GetDirectoryPath("file://server/"), "file://server/")
	assert.Equal(t, GetDirectoryPath("file://server/path"), "file://server/")
	assert.Equal(t, GetDirectoryPath("file://server/path/"), "file://server/")
	assert.Equal(t, GetDirectoryPath("http://server"), "http://server")
	assert.Equal(t, GetDirectoryPath("http://server/"), "http://server/")
	assert.Equal(t, GetDirectoryPath("http://server/path"), "http://server/")
	assert.Equal(t, GetDirectoryPath("http://server/path/"), "http://server/")
}

// !!!
// getBaseFileName
// getAnyExtensionFromPath

func TestGetPathComponents(t *testing.T) {
	t.Parallel()
	assert.DeepEqual(t, GetPathComponents("", ""), []string{""})
	assert.DeepEqual(t, GetPathComponents("a", ""), []string{"", "a"})
	assert.DeepEqual(t, GetPathComponents("./a", ""), []string{"", ".", "a"})
	assert.DeepEqual(t, GetPathComponents("/", ""), []string{"/"})
	assert.DeepEqual(t, GetPathComponents("/a", ""), []string{"/", "a"})
	assert.DeepEqual(t, GetPathComponents("/a/", ""), []string{"/", "a"})
	assert.DeepEqual(t, GetPathComponents("c:", ""), []string{"c:"})
	assert.DeepEqual(t, GetPathComponents("c:d", ""), []string{"", "c:d"})
	assert.DeepEqual(t, GetPathComponents("c:/", ""), []string{"c:/"})
	assert.DeepEqual(t, GetPathComponents("c:/path", ""), []string{"c:/", "path"})
	assert.DeepEqual(t, GetPathComponents("//server", ""), []string{"//server"})
	assert.DeepEqual(t, GetPathComponents("//server/", ""), []string{"//server/"})
	assert.DeepEqual(t, GetPathComponents("//server/share", ""), []string{"//server/", "share"})
	assert.DeepEqual(t, GetPathComponents("file:///", ""), []string{"file:///"})
	assert.DeepEqual(t, GetPathComponents("file:///path", ""), []string{"file:///", "path"})
	assert.DeepEqual(t, GetPathComponents("file:///c:", ""), []string{"file:///c:"})
	assert.DeepEqual(t, GetPathComponents("file:///c:d", ""), []string{"file:///", "c:d"})
	assert.DeepEqual(t, GetPathComponents("file:///c:/", ""), []string{"file:///c:/"})
	assert.DeepEqual(t, GetPathComponents("file:///c:/path", ""), []string{"file:///c:/", "path"})
	assert.DeepEqual(t, GetPathComponents("file://server", ""), []string{"file://server"})
	assert.DeepEqual(t, GetPathComponents("file://server/", ""), []string{"file://server/"})
	assert.DeepEqual(t, GetPathComponents("file://server/path", ""), []string{"file://server/", "path"})
	assert.DeepEqual(t, GetPathComponents("http://server", ""), []string{"http://server"})
	assert.DeepEqual(t, GetPathComponents("http://server/", ""), []string{"http://server/"})
	assert.DeepEqual(t, GetPathComponents("http://server/path", ""), []string{"http://server/", "path"})
}

func TestReducePathComponents(t *testing.T) {
	t.Parallel()
	assert.DeepEqual(t, reducePathComponents([]string{""}), []string{""})
	assert.DeepEqual(t, reducePathComponents([]string{"", "."}), []string{""})
	assert.DeepEqual(t, reducePathComponents([]string{"", ".", "a"}), []string{"", "a"})
	assert.DeepEqual(t, reducePathComponents([]string{"", "a", "."}), []string{"", "a"})
	assert.DeepEqual(t, reducePathComponents([]string{"", ".."}), []string{"", ".."})
	assert.DeepEqual(t, reducePathComponents([]string{"", "..", ".."}), []string{"", "..", ".."})
	assert.DeepEqual(t, reducePathComponents([]string{"", "..", ".", ".."}), []string{"", "..", ".."})
	assert.DeepEqual(t, reducePathComponents([]string{"", "a", ".."}), []string{""})
	assert.DeepEqual(t, reducePathComponents([]string{"", "..", "a"}), []string{"", "..", "a"})
	assert.DeepEqual(t, reducePathComponents([]string{"/"}), []string{"/"})
	assert.DeepEqual(t, reducePathComponents([]string{"/", "."}), []string{"/"})
	assert.DeepEqual(t, reducePathComponents([]string{"/", ".."}), []string{"/"})
	assert.DeepEqual(t, reducePathComponents([]string{"/", "a", ".."}), []string{"/"})
}

func TestCombinePaths(t *testing.T) {
	t.Parallel()
	// Non-rooted
	assert.Equal(t, CombinePaths("path", "to", "file.ext"), "path/to/file.ext")
	assert.Equal(t, CombinePaths("path", "dir", "..", "to", "file.ext"), "path/dir/../to/file.ext")
	// POSIX
	assert.Equal(t, CombinePaths("/path", "to", "file.ext"), "/path/to/file.ext")
	assert.Equal(t, CombinePaths("/path", "/to", "file.ext"), "/to/file.ext")
	// DOS
	assert.Equal(t, CombinePaths("c:/path", "to", "file.ext"), "c:/path/to/file.ext")
	assert.Equal(t, CombinePaths("c:/path", "c:/to", "file.ext"), "c:/to/file.ext")
	// URL
	assert.Equal(t, CombinePaths("file:///path", "to", "file.ext"), "file:///path/to/file.ext")
	assert.Equal(t, CombinePaths("file:///path", "file:///to", "file.ext"), "file:///to/file.ext")

	assert.Equal(t, CombinePaths("/", "/node_modules/@types"), "/node_modules/@types")
	assert.Equal(t, CombinePaths("/a/..", ""), "/a/..")
	assert.Equal(t, CombinePaths("/a/..", "b"), "/a/../b")
	assert.Equal(t, CombinePaths("/a/..", "b/"), "/a/../b/")
	assert.Equal(t, CombinePaths("/a/..", "/"), "/")
	assert.Equal(t, CombinePaths("/a/..", "/b"), "/b")
}

func BenchmarkCombinePaths(b *testing.B) {
	tests := [][]string{
		{"path", "to", "file.ext"},
		{"path", "dir", "..", "to", "file.ext"},
		{"/path", "to", "file.ext"},
		{"/path", "/to", "file.ext"},
		{"c:/path", "to", "file.ext"},
		{"file:///path", "to", "file.ext"},
	}

	for _, test := range tests {
		name := shortenName(strings.Join(test, "/"))
		b.Run(name, func(b *testing.B) {
			first, rest := test[0], test[1:]
			b.ReportAllocs()
			for b.Loop() {
				CombinePaths(first, rest...)
			}
		})
	}
}

func TestResolvePath(t *testing.T) {
	t.Parallel()
	assert.Equal(t, ResolvePath(""), "")
	assert.Equal(t, ResolvePath("."), "")
	assert.Equal(t, ResolvePath("./"), "")
	assert.Equal(t, ResolvePath(".."), "..")
	assert.Equal(t, ResolvePath("../"), "../")
	assert.Equal(t, ResolvePath("/"), "/")
	assert.Equal(t, ResolvePath("/."), "/")
	assert.Equal(t, ResolvePath("/./"), "/")
	assert.Equal(t, ResolvePath("/../"), "/")
	assert.Equal(t, ResolvePath("/a"), "/a")
	assert.Equal(t, ResolvePath("/a/"), "/a/")
	assert.Equal(t, ResolvePath("/a/."), "/a")
	assert.Equal(t, ResolvePath("/a/./"), "/a/")
	assert.Equal(t, ResolvePath("/a/./b"), "/a/b")
	assert.Equal(t, ResolvePath("/a/./b/"), "/a/b/")
	assert.Equal(t, ResolvePath("/a/.."), "/")
	assert.Equal(t, ResolvePath("/a/../"), "/")
	assert.Equal(t, ResolvePath("/a/../b"), "/b")
	assert.Equal(t, ResolvePath("/a/../b/"), "/b/")
	assert.Equal(t, ResolvePath("/a/..", "b"), "/b")
	assert.Equal(t, ResolvePath("/a/..", "/"), "/")
	assert.Equal(t, ResolvePath("/a/..", "b/"), "/b/")
	assert.Equal(t, ResolvePath("/a/..", "/b"), "/b")
	assert.Equal(t, ResolvePath("/a/.", "b"), "/a/b")
	assert.Equal(t, ResolvePath("/a/.", "."), "/a")
	assert.Equal(t, ResolvePath("a", "b", "c"), "a/b/c")
	assert.Equal(t, ResolvePath("a", "b", "/c"), "/c")
	assert.Equal(t, ResolvePath("a", "b", "../c"), "a/c")
}

func TestGetNormalizedAbsolutePath(t *testing.T) {
	t.Parallel()

	assert.Equal(t, GetNormalizedAbsolutePath("/", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/.", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/./", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/../", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/.", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/foo.", ""), "/a/foo.")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/./", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/./b", ""), "/a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/./b/", ""), "/a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/..", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/../", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/../", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/../b", ""), "/b")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/../b/", ""), "/b")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/..", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/..", "/"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/..", "b/"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/..", "/b"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/.", "b"), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/.", "."), "/a")

	// Tests as above, but with backslashes.
	assert.Equal(t, GetNormalizedAbsolutePath("\\", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\.", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\.\\", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\..\\", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\.\\", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\.\\b", ""), "/a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\.\\b\\", ""), "/a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..\\", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..\\", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..\\b", ""), "/b")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..\\b\\", ""), "/b")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..", "\\"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..", "b\\"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\..", "\\b"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\.", "b"), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\.", "."), "/a")

	// Relative paths on an empty currentDirectory.
	assert.Equal(t, GetNormalizedAbsolutePath("", ""), "")
	assert.Equal(t, GetNormalizedAbsolutePath(".", ""), "")
	assert.Equal(t, GetNormalizedAbsolutePath("./", ""), "")
	// Strangely, these do not normalize to the empty string.
	assert.Equal(t, GetNormalizedAbsolutePath("..", ""), "..")
	assert.Equal(t, GetNormalizedAbsolutePath("../", ""), "..")

	// Interaction between relative paths and currentDirectory.
	assert.Equal(t, GetNormalizedAbsolutePath("", "/home"), "/home")
	assert.Equal(t, GetNormalizedAbsolutePath(".", "/home"), "/home")
	assert.Equal(t, GetNormalizedAbsolutePath("./", "/home"), "/home")
	assert.Equal(t, GetNormalizedAbsolutePath("..", "/home"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("../", "/home"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("a", "b"), "b/a")
	assert.Equal(t, GetNormalizedAbsolutePath("a", "b/c"), "b/c/a")

	// Base names starting or ending with a dot do not affect normalization.
	assert.Equal(t, GetNormalizedAbsolutePath(".a", ""), ".a")
	assert.Equal(t, GetNormalizedAbsolutePath("..a", ""), "..a")
	assert.Equal(t, GetNormalizedAbsolutePath("a.", ""), "a.")
	assert.Equal(t, GetNormalizedAbsolutePath("a..", ""), "a..")

	assert.Equal(t, GetNormalizedAbsolutePath("/base/./.a", ""), "/base/.a")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../.a", ""), "/.a")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/./..a", ""), "/base/..a")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../..a", ""), "/..a")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/./..a/b", ""), "/base/..a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../..a/b", ""), "/..a/b")

	assert.Equal(t, GetNormalizedAbsolutePath("/base/./a.", ""), "/base/a.")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../a.", ""), "/a.")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/./a..", ""), "/base/a..")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../a..", ""), "/a..")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/./a../b", ""), "/base/a../b")
	assert.Equal(t, GetNormalizedAbsolutePath("/base/../a../b", ""), "/a../b")

	assert.Equal(t, GetNormalizedAbsolutePath("a/..", ""), "")
	assert.Equal(t, GetNormalizedAbsolutePath("/a//", ""), "/a")
	assert.Equal(t, GetNormalizedAbsolutePath("//a", "a"), "//a/")
	assert.Equal(t, GetNormalizedAbsolutePath("/\\", ""), "//")
	assert.Equal(t, GetNormalizedAbsolutePath("a///", "a"), "a/a")
	assert.Equal(t, GetNormalizedAbsolutePath("/.//", ""), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("//\\\\", ""), "///")
	assert.Equal(t, GetNormalizedAbsolutePath(".//a", "."), "a")
	assert.Equal(t, GetNormalizedAbsolutePath("a/../..", ""), "..")
	assert.Equal(t, GetNormalizedAbsolutePath("../..", "\\a"), "/")
	assert.Equal(t, GetNormalizedAbsolutePath("a:", "b"), "a:/")
	assert.Equal(t, GetNormalizedAbsolutePath("a/../..", ".."), "../..")
	assert.Equal(t, GetNormalizedAbsolutePath("a/../..", "b"), "")
	assert.Equal(t, GetNormalizedAbsolutePath("a//../..", ".."), "../..")

	// Consecutive intermediate slashes are normalized to a single slash.
	assert.Equal(t, GetNormalizedAbsolutePath("a//b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a///b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a/b//c", ""), "a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("/a/b//c", ""), "/a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("//a/b//c", ""), "//a/b/c")

	// Backslashes are converted to slashes,
	// and then consecutive intermediate slashes are normalized to a single slash
	assert.Equal(t, GetNormalizedAbsolutePath("a\\\\b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a\\\\\\b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a\\b\\\\c", ""), "a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\b\\\\c", ""), "/a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("\\\\a\\b\\\\c", ""), "//a/b/c")

	// The same occurs for mixed slashes.
	assert.Equal(t, GetNormalizedAbsolutePath("a/\\b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a\\/b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a\\/\\b", ""), "a/b")
	assert.Equal(t, GetNormalizedAbsolutePath("a\\b//c", ""), "a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("\\a\\b\\\\c", ""), "/a/b/c")
	assert.Equal(t, GetNormalizedAbsolutePath("\\\\a\\b\\\\c", ""), "//a/b/c")
}

var getNormalizedAbsolutePathTests = map[string][][]string{
	"non-normalized inputs": {
		{"/.", ""},
		{"/./", ""},
		{"/../", ""},
		{"/a/", ""},
		{"/a/.", ""},
		{"/a/foo.", ""},
		{"/a/./", ""},
		{"/a/./b", ""},
		{"/a/./b/", ""},
		{"/a/..", ""},
		{"/a/../", ""},
		{"/a/../", ""},
		{"/a/../b", ""},
		{"/a/../b/", ""},
		{"/a/..", ""},
		{"/a/..", "/"},
		{"/a/..", "b/"},
		{"/a/..", "/b"},
		{"/a/.", "b"},
		{"/a/.", "."},
	},
	"normalized inputs": {
		{"/a/b", ""},
		{"/one/two/three", ""},
		{"/users/root/project/src/foo.ts", ""},
	},
	"normalized inputs (long)": {
		{"/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z", ""},
		{"/one/two/three/four/five/six/seven/eight/nine/ten/eleven/twelve/thirteen/fourteen/fifteen/sixteen/seventeen/eighteen/nineteen/twenty", ""},
		{"/users/root/project/src/foo/bar/baz/qux/quux/corge/grault/garply/waldo/fred/plugh/xyzzy/thud", ""},
		{"/lorem/ipsum/dolor/sit/amet/consectetur/adipiscing/elit/sed/do/eiusmod/tempor/incididunt/ut/labore/et/dolore/magna/aliqua/ut/enim/ad/minim/veniam", ""},
	},
}

func BenchmarkGetNormalizedAbsolutePath(b *testing.B) {
	funcs := map[string]func(string, string) string{
		"GetNormalizedAbsolutePath":       GetNormalizedAbsolutePath,
		"GetNormalizedAbsolutePath (old)": getNormalizedAbsolutePath_old,
	}
	for name, tests := range getNormalizedAbsolutePathTests {
		b.Run(name, func(b *testing.B) {
			for fnName, fn := range funcs {
				b.Run(fnName, func(b *testing.B) {
					b.ReportAllocs()
					for b.Loop() {
						for _, test := range tests {
							fn(test[0], test[1])
						}
					}
				})
			}
		})
	}
}

func FuzzGetNormalizedAbsolutePath(f *testing.F) {
	for _, tests := range getNormalizedAbsolutePathTests {
		for _, test := range tests {
			f.Add(test[0], test[1])
		}
	}

	f.Fuzz(func(t *testing.T, p string, dir string) {
		assert.Equal(t, GetNormalizedAbsolutePath(p, dir), getNormalizedAbsolutePath_old(p, dir), fmt.Sprintf("p=%q, dir=%q", p, dir))
	})
}

func TestGetRelativePathToDirectoryOrUrl(t *testing.T) {
	t.Parallel()
	// !!!
	// Based on tests for `getRelativePathFromDirectory`.

	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/", "/", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a", "/a", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a/", "/a", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a", "/", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "..")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a", "/b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a/b", "/b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a/b/c", "/b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a/b/c", "/b/c", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../../b/c")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("/a/b/c", "/a/b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "..")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("c:", "d:", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "d:/")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///", "file:///", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a", "file:///a", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a/", "file:///a", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a", "file:///", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "..")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a", "file:///b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a/b", "file:///b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a/b/c", "file:///b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../../b")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a/b/c", "file:///b/c", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "../../../b/c")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///a/b/c", "file:///a/b", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "..")
	assert.Equal(t, GetRelativePathToDirectoryOrUrl("file:///c:", "file:///d:", false /*isAbsolutePathAnUrl*/, ComparePathsOptions{}), "file:///d:/")
}

func TestToFileNameLowerCase(t *testing.T) {
	t.Parallel()
	assert.Equal(t, ToFileNameLowerCase("/user/UserName/projects/Project/file.ts"), "/user/username/projects/project/file.ts")
	assert.Equal(t, ToFileNameLowerCase("/user/UserName/projects/projectß/file.ts"), "/user/username/projects/projectß/file.ts")
	assert.Equal(t, ToFileNameLowerCase("/user/UserName/projects/İproject/file.ts"), "/user/username/projects/İproject/file.ts")
	assert.Equal(t, ToFileNameLowerCase("/user/UserName/projects/ı/file.ts"), "/user/username/projects/ı/file.ts")
}

var toFileNameLowerCaseTests = []string{
	"/path/to/file.ext",
	"/PATH/TO/FILE.EXT",
	"/path/to/FILE.EXT",
	"/user/UserName/projects/Project/file.ts",
	"/user/UserName/projects/projectß/file.ts",
	"/user/UserName/projects/İproject/file.ts",
	"/user/UserName/projects/ı/file.ts",
	strings.Repeat("FoO/", 100),
}

// See [toFileNameLowerCase] for more info.
//
// To avoid having to do string building for most common cases, also ignore
// a-z, 0-9, \u0131, \u00DF, \, /, ., : and space
var fileNameLowerCaseRegExp = regexp.MustCompile(`[^\x{0130}\x{0131}\x{00DF}a-z0-9\\/:\-_. ]+`)

func oldToFileNameLowerCase(fileName string) string {
	return fileNameLowerCaseRegExp.ReplaceAllStringFunc(fileName, strings.ToLower)
}

func BenchmarkToFileNameLowerCase(b *testing.B) {
	for _, test := range toFileNameLowerCaseTests {
		name := shortenName(test)
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				ToFileNameLowerCase(test)
			}
		})
	}
}

func FuzzToFileNameLowerCase(f *testing.F) {
	for _, test := range toFileNameLowerCaseTests {
		f.Add(test)
	}

	f.Fuzz(func(t *testing.T, p string) {
		assert.Equal(t, oldToFileNameLowerCase(p), ToFileNameLowerCase(p))
	})
}

func TestToPath(t *testing.T) {
	t.Parallel()
	assert.Equal(t, string(ToPath("file.ext", "path/to", false /*useCaseSensitiveFileNames*/)), "path/to/file.ext")
	assert.Equal(t, string(ToPath("file.ext", "/path/to", true /*useCaseSensitiveFileNames*/)), "/path/to/file.ext")
	assert.Equal(t, string(ToPath("/path/to/../file.ext", "path/to", true /*useCaseSensitiveFileNames*/)), "/path/file.ext")
}

var relativePathSegmentRegExp = regexp.MustCompile(`//|(?:^|/)\.\.?(?:$|/)`)

func oldHasRelativePathSegment(p string) bool {
	return relativePathSegmentRegExp.MatchString(p)
}

var hasRelativePathSegmentTests = []struct {
	p     string
	bench bool
}{
	{"//", false},
	{"foo/bar/baz", true},
	{"foo/./baz", false},
	{"foo/../baz", false},
	{"foo/bar/baz/.", false},
	{"./some/path", true},
	{"/foo//bar/", false},
	{"/foo/./bar/../../.", true},
	{strings.Repeat("foo/", 100) + "..", true},
}

func BenchmarkHasRelativePathSegment(b *testing.B) {
	for _, tt := range hasRelativePathSegmentTests {
		if !tt.bench {
			continue
		}
		name := shortenName(tt.p)
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				hasRelativePathSegment(tt.p)
			}
		})
	}
}

func FuzzHasRelativePathSegment(f *testing.F) {
	for _, tt := range hasRelativePathSegmentTests {
		f.Add(tt.p)
	}

	f.Fuzz(func(t *testing.T, p string) {
		assert.Equal(t, oldHasRelativePathSegment(p), hasRelativePathSegment(p))
	})
}

var pathIsRelativeTests = []struct {
	p          string
	isRelative bool
	benchmark  bool
}{
	// relative
	{".", true, false},
	{"..", true, false},
	{"./", true, false},
	{"../", true, false},
	{"./foo/bar", true, true},
	{"../foo/bar", true, true},
	{"../" + strings.Repeat("foo/", 100), true, true},
	// non-relative
	{"", false, false},
	{"foo", false, false},
	{"foo/bar", false, false},
	{"/foo/bar", false, false},
	{"c:/foo/bar", false, false},
}

func init() {
	old := pathIsRelativeTests

	for _, t := range old {
		t.p = strings.ReplaceAll(t.p, "/", "\\")
		pathIsRelativeTests = append(pathIsRelativeTests, t)
	}
}

func TestPathIsRelative(t *testing.T) {
	t.Parallel()
	for _, tt := range pathIsRelativeTests {
		name := shortenName(tt.p)
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, PathIsRelative(tt.p), tt.isRelative)
		})
	}
}

func BenchmarkPathIsRelative(b *testing.B) {
	for _, tt := range pathIsRelativeTests {
		if !tt.benchmark {
			continue
		}
		name := shortenName(tt.p)
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				PathIsRelative(tt.p)
			}
		})
	}
}

func shortenName(name string) string {
	if len(name) > 20 {
		return name[:20] + "...etc"
	}
	return name
}

func normalizePath_old(path string) string {
	path = NormalizeSlashes(path)
	// Most paths don't require normalization
	if !hasRelativePathSegment(path) {
		return path
	}
	// Some paths only require cleanup of `/./` or leading `./`
	simplified := strings.ReplaceAll(path, "/./", "/")
	simplified = strings.TrimPrefix(simplified, "./")
	if simplified != path && !hasRelativePathSegment(simplified) {
		path = simplified
		return path
	}
	// Other paths require full normalization
	normalized := GetPathFromPathComponents(reducePathComponents(GetPathComponents(path, "")))
	if normalized != "" && HasTrailingDirectorySeparator(path) {
		normalized = EnsureTrailingDirectorySeparator(normalized)
	}
	return normalized
}

func getNormalizedAbsolutePath_old(fileName string, currentDirectory string) string {
	return GetPathFromPathComponents(GetNormalizedPathComponents(fileName, currentDirectory))
}

func TestGetCommonParents(t *testing.T) {
	t.Parallel()

	opts := ComparePathsOptions{}

	t.Run("empty input", func(t *testing.T) {
		t.Parallel()
		var paths []string
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		assert.DeepEqual(t, got, ([]string)(nil))
	})

	t.Run("single path returns itself", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{paths[0]}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("paths shorter than minComponents are ignored", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y"}
		got, ignored := GetCommonParents(paths, 4, GetPathComponents, opts)
		assert.DeepEqual(t, ignored, map[string]struct{}{"/x/y": {}})
		expected := []string{"/a/b/c", "/a/b/f/g"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("three paths share /a/b", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/a/b"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("mixed with short path collapses to root when minComponents=1", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y/z"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("mixed with short path preserves both when minComponents=3", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y/z"}
		got, ignored := GetCommonParents(paths, 3, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/a/b", "/x/y/z"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("different volumes are returned individually", func(t *testing.T) {
		t.Parallel()
		paths := []string{"c:/a/b/c/d", "d:/a/b/c/d"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{paths[0], paths[1]}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("duplicate paths deduplicate result", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/b/c/d"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{paths[0]}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("paths with few components are returned as-is when minComponents met", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/x/y"}
		got, ignored := GetCommonParents(paths, 2, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/a/b/c/d", "/x/y"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("minComponents=2", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/c/d", "/a/z/c/e", "/a/aaa/f/g", "/x/y/z"}
		got, ignored := GetCommonParents(paths, 2, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/a", "/x/y/z"}
		assert.DeepEqual(t, got, expected)
	})

	t.Run("trailing separators are handled", func(t *testing.T) {
		t.Parallel()
		paths := []string{"/a/b/", "/a/b/c"}
		got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
		assert.Equal(t, len(ignored), 0)
		expected := []string{"/a/b"}
		assert.DeepEqual(t, got, expected)
	})
}
