//! Workspace-root walk: locate the workspace root from a starting
//! path, distinguishing **explicit** mode (a `.haz/` directory was
//! found) from **implicit** mode (only a `.git` entry was found).
//!
//! Implements rules `DISC-001` and `DISC-002` from
//! `docs/spec/03-discovery.md`. Symlink semantics for marker entries
//! (`DISC-020`) are inherited from the [`Filesystem`] trait.
//!
//! The entry point is [`crate::discovery::Discovery::find_workspace_root`].

use std::path::{Path, PathBuf};

use haz_domain::path::{WorkspaceRootPath, WorkspaceRootPathError};
use snafu::Snafu;

use haz_vfs::{EntryKind, Filesystem, FsError};

use crate::discovery::Discovery;

/// Result of a successful workspace-root walk.
///
/// Each variant carries two views of the workspace root:
///
/// - `lexical`: a [`WorkspaceRootPath`] (absolute, lexically clean,
///   no `..`). This is the byte-form used to interpolate paths in
///   diagnostics and to render strings to the user.
/// - `canonical`: an [`F::CanonicalPath`](Filesystem::CanonicalPath)
///   returned by the underlying [`Filesystem::canonicalize`] call.
///   This is the value the host filesystem uses to compare
///   "descendant of the workspace root" (`DISC-022`); it carries
///   the implementation-specific canonicality witness.
///
/// The two values are byte-equal at construction time (the walk
/// canonicalises before validating against [`WorkspaceRootPath`]).
/// They are kept separate because they capture distinct invariants:
/// [`WorkspaceRootPath`] is a pure-domain lexical guarantee that
/// any caller can reconstruct; `F::CanonicalPath` is an
/// implementation-coupled host-canonicality guarantee that is only
/// obtainable via the FS that produced it.
///
/// The variant tag encodes the SPEC's explicit/implicit mode
/// distinction.
pub enum WorkspaceRoot<F: Filesystem> {
    /// A `.haz/` directory was found at this ancestor (`DISC-001`).
    /// Workspace configuration loads from
    /// `<root>/.haz/workspace.yml` (`DISC-004`).
    Explicit {
        /// Lexical view of the workspace-root path.
        lexical: WorkspaceRootPath,
        /// Host-canonical view of the workspace-root path,
        /// produced by [`Filesystem::canonicalize`].
        canonical: F::CanonicalPath,
    },
    /// No `.haz/` was found; a `.git` entry (directory or regular
    /// file) was found at this ancestor (`DISC-001`). The workspace
    /// contains a single project rooted at the workspace root, with
    /// configuration in `<root>/haz.yml` (`DISC-003`).
    Implicit {
        /// Lexical view of the workspace-root path.
        lexical: WorkspaceRootPath,
        /// Host-canonical view of the workspace-root path,
        /// produced by [`Filesystem::canonicalize`].
        canonical: F::CanonicalPath,
    },
}

impl<F: Filesystem> WorkspaceRoot<F> {
    /// Borrow the workspace-root path's lexical view regardless of
    /// mode.
    #[must_use]
    pub fn lexical(&self) -> &WorkspaceRootPath {
        match self {
            WorkspaceRoot::Explicit { lexical, .. } | WorkspaceRoot::Implicit { lexical, .. } => {
                lexical
            }
        }
    }

    /// Borrow the workspace-root path's host-canonical view
    /// regardless of mode.
    #[must_use]
    pub fn canonical(&self) -> &F::CanonicalPath {
        match self {
            WorkspaceRoot::Explicit { canonical, .. }
            | WorkspaceRoot::Implicit { canonical, .. } => canonical,
        }
    }
}

// Manual trait impls below: deriving Debug/Clone/PartialEq/Eq/Hash
// would add bounds on `F` (the filesystem type itself), but the
// data only depends on `F::CanonicalPath`, whose bounds the
// [`Filesystem`] trait already supplies. Writing the impls by hand
// keeps `F` unconstrained beyond `Filesystem`.

impl<F: Filesystem> std::fmt::Debug for WorkspaceRoot<F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Explicit { lexical, canonical } => f
                .debug_struct("WorkspaceRoot::Explicit")
                .field("lexical", lexical)
                .field("canonical", canonical)
                .finish(),
            Self::Implicit { lexical, canonical } => f
                .debug_struct("WorkspaceRoot::Implicit")
                .field("lexical", lexical)
                .field("canonical", canonical)
                .finish(),
        }
    }
}

impl<F: Filesystem> Clone for WorkspaceRoot<F> {
    fn clone(&self) -> Self {
        match self {
            Self::Explicit { lexical, canonical } => Self::Explicit {
                lexical: lexical.clone(),
                canonical: canonical.clone(),
            },
            Self::Implicit { lexical, canonical } => Self::Implicit {
                lexical: lexical.clone(),
                canonical: canonical.clone(),
            },
        }
    }
}

impl<F: Filesystem> PartialEq for WorkspaceRoot<F> {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (
                Self::Explicit {
                    lexical: l1,
                    canonical: c1,
                },
                Self::Explicit {
                    lexical: l2,
                    canonical: c2,
                },
            )
            | (
                Self::Implicit {
                    lexical: l1,
                    canonical: c1,
                },
                Self::Implicit {
                    lexical: l2,
                    canonical: c2,
                },
            ) => l1 == l2 && c1 == c2,
            _ => false,
        }
    }
}

impl<F: Filesystem> Eq for WorkspaceRoot<F> {}

impl<F: Filesystem> std::hash::Hash for WorkspaceRoot<F> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        match self {
            Self::Explicit { lexical, canonical } => {
                state.write_u8(0);
                lexical.hash(state);
                canonical.hash(state);
            }
            Self::Implicit { lexical, canonical } => {
                state.write_u8(1);
                lexical.hash(state);
                canonical.hash(state);
            }
        }
    }
}

/// Errors produced by
/// [`Discovery::find_workspace_root`](crate::discovery::Discovery::find_workspace_root).
///
/// Diagnostic codes follow the `haz::dsc::NNN` namespace; they are
/// kept in the variant docstrings so callers can route on them
/// without round-tripping through [`Display`](std::fmt::Display)
/// text.
#[derive(Debug, Snafu)]
pub enum RootError {
    /// The walk reached the filesystem root without finding either
    /// a `.haz/` directory (after symlink resolution) or a `.git`
    /// entry that is a directory or a regular file.
    ///
    /// Diagnostic code: `haz::dsc::001`.
    #[snafu(display("no workspace root found from start: {}", start.display()))]
    NoWorkspaceRoot {
        /// Starting path the walk was launched from.
        start: PathBuf,
    },

    /// The starting path was not absolute.
    ///
    /// Diagnostic code: `haz::dsc::002`.
    #[snafu(display("expected absolute starting path, got: {}", path.display()))]
    NotAbsolute {
        /// Relative path that was rejected.
        path: PathBuf,
    },

    /// A filesystem error other than [`FsError::NotFound`] surfaced
    /// while probing a marker entry or canonicalising the chosen
    /// workspace root. Common sources: [`FsError::SymlinkLoop`],
    /// [`FsError::UnknownEntryKind`], [`FsError::Io`].
    ///
    /// Diagnostic code: `haz::dsc::003`.
    #[snafu(display(
        "filesystem error during workspace root walk at: {}: {source}",
        path.display()
    ))]
    Fs {
        /// Path being probed when the error surfaced.
        path: PathBuf,
        /// Underlying filesystem error.
        source: FsError,
    },

    /// [`Filesystem::canonicalize`] returned a path that fails the
    /// [`WorkspaceRootPath`] validator (i.e. is not absolute,
    /// contains `..`, or is not lexically clean). This indicates a
    /// contract violation by the underlying filesystem
    /// implementation.
    ///
    /// Diagnostic code: `haz::dsc::004`.
    #[snafu(display(
        "canonicalised path is not a valid workspace root path: {}: {source}",
        path.display()
    ))]
    InvalidCanonicalPath {
        /// Path returned by `canonicalize`.
        path: PathBuf,
        /// Underlying validator error.
        source: WorkspaceRootPathError,
    },
}

impl<F: Filesystem> Discovery<'_, F> {
    /// Locate the workspace root from a starting path.
    ///
    /// Walks `start`'s ancestors innermost-first per `DISC-001`. At
    /// each ancestor:
    ///
    /// - probes `<ancestor>/.haz` and counts it as the explicit-mode
    ///   marker iff [`Filesystem::metadata`] (which follows symlinks)
    ///   reports [`EntryKind::Dir`];
    /// - probes `<ancestor>/.git` and counts it as the implicit-mode
    ///   marker iff [`Filesystem::metadata`] reports
    ///   [`EntryKind::Dir`] or [`EntryKind::File`].
    ///
    /// The first ancestor at which any marker is present terminates
    /// the walk. If both markers are present at the same ancestor,
    /// the explicit marker wins (`DISC-001`). If the walk reaches
    /// the filesystem root without finding either marker,
    /// [`RootError::NoWorkspaceRoot`] is returned (`DISC-002`).
    ///
    /// `start` MUST be absolute. Cleanliness (no `.`/`..` components,
    /// no empty separators, etc.) is ultimately enforced by
    /// [`WorkspaceRootPath`]'s validator on the canonicalised result;
    /// pre-canonicalisation, the [`Filesystem`] trait's contract on
    /// individual probes also rejects unclean inputs.
    ///
    /// # Errors
    ///
    /// - [`RootError::NotAbsolute`] when `start` is relative.
    /// - [`RootError::Fs`] when probing a marker, walking ancestors,
    ///   or canonicalising the chosen ancestor surfaces an
    ///   [`FsError`] other than [`FsError::NotFound`].
    /// - [`RootError::InvalidCanonicalPath`] when
    ///   [`Filesystem::canonicalize`] returns a path that fails the
    ///   [`WorkspaceRootPath`] validator.
    /// - [`RootError::NoWorkspaceRoot`] when no marker is found from
    ///   `start` up to the filesystem root.
    pub fn find_workspace_root(&self, start: &Path) -> Result<WorkspaceRoot<F>, RootError> {
        if !start.is_absolute() {
            return Err(RootError::NotAbsolute {
                path: start.to_path_buf(),
            });
        }

        for ancestor in start.ancestors() {
            let haz_marker = probe_haz_marker(ancestor, self.fs)?;
            let git_marker = probe_git_marker(ancestor, self.fs)?;

            if !(haz_marker || git_marker) {
                continue;
            }

            let canonical = self
                .fs
                .canonicalize(ancestor)
                .map_err(|source| RootError::Fs {
                    path: ancestor.to_path_buf(),
                    source,
                })?;
            let lexical_pb: PathBuf = canonical.as_ref().to_path_buf();
            let lexical = WorkspaceRootPath::try_new(lexical_pb.clone()).map_err(|source| {
                RootError::InvalidCanonicalPath {
                    path: lexical_pb,
                    source,
                }
            })?;

            return Ok(if haz_marker {
                WorkspaceRoot::Explicit { lexical, canonical }
            } else {
                WorkspaceRoot::Implicit { lexical, canonical }
            });
        }

        Err(RootError::NoWorkspaceRoot {
            start: start.to_path_buf(),
        })
    }
}

fn probe_haz_marker(ancestor: &Path, fs: &impl Filesystem) -> Result<bool, RootError> {
    let path = ancestor.join(".haz");
    match fs.metadata(&path) {
        Ok(m) => Ok(m.kind == EntryKind::Dir),
        Err(FsError::NotFound { .. }) => Ok(false),
        Err(source) => Err(RootError::Fs { path, source }),
    }
}

fn probe_git_marker(ancestor: &Path, fs: &impl Filesystem) -> Result<bool, RootError> {
    let path = ancestor.join(".git");
    match fs.metadata(&path) {
        Ok(m) => Ok(matches!(m.kind, EntryKind::Dir | EntryKind::File)),
        Err(FsError::NotFound { .. }) => Ok(false),
        Err(source) => Err(RootError::Fs { path, source }),
    }
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use haz_domain::path::WorkspaceRootPath;

    use super::{RootError, WorkspaceRoot};
    use crate::discovery::Discovery;
    use haz_vfs_testing::MemFilesystem;

    use haz_vfs::Filesystem;

    fn explicit(fs: &MemFilesystem, p: &str) -> WorkspaceRoot<MemFilesystem> {
        let lexical = WorkspaceRootPath::try_new(PathBuf::from(p)).unwrap();
        let canonical = fs.canonicalize(Path::new(p)).unwrap();
        WorkspaceRoot::Explicit { lexical, canonical }
    }

    fn implicit(fs: &MemFilesystem, p: &str) -> WorkspaceRoot<MemFilesystem> {
        let lexical = WorkspaceRootPath::try_new(PathBuf::from(p)).unwrap();
        let canonical = fs.canonicalize(Path::new(p)).unwrap();
        WorkspaceRoot::Implicit { lexical, canonical }
    }

    #[test]
    fn disc_001_finds_explicit_at_starting_directory() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b/.haz").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_001_finds_implicit_at_starting_directory_with_git_dir() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b/.git").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_001_finds_implicit_at_starting_directory_with_git_file() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b").unwrap();
        fs.add_file("/a/b/.git", b"gitdir: /elsewhere\n".to_vec())
            .unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_001_explicit_wins_over_git_at_same_ancestor() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b/.haz").unwrap();
        fs.add_dir("/a/b/.git").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_001_walks_up_until_haz_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/.haz").unwrap();
        fs.add_dir("/a/b/c").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b/c"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/a"));
    }

    #[test]
    fn disc_001_walks_up_until_git_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/.git").unwrap();
        fs.add_dir("/a/b/c").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b/c"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a"));
    }

    #[test]
    fn disc_001_innermost_haz_wins_over_outer_haz() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/.haz").unwrap();
        fs.add_dir("/a/b/.haz").unwrap();
        fs.add_dir("/a/b/c").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b/c"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_001_inner_git_blocks_outer_haz() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/.haz").unwrap();
        fs.add_dir("/a/b").unwrap();
        fs.add_file("/a/b/.git", b"gitdir: /elsewhere\n".to_vec())
            .unwrap();
        fs.add_dir("/a/b/c").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b/c"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_002_no_marker_returns_no_workspace_root() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b/c").unwrap();
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b/c"))
            .unwrap_err();
        assert!(matches!(err, RootError::NoWorkspaceRoot { .. }));
    }

    #[test]
    fn disc_001_relative_start_returns_not_absolute() {
        let fs = MemFilesystem::new();
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("relative/path"))
            .unwrap_err();
        assert!(matches!(err, RootError::NotAbsolute { .. }));
    }

    #[test]
    fn disc_001_haz_as_regular_file_is_not_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b").unwrap();
        fs.add_file("/a/b/.haz", b"not a marker".to_vec()).unwrap();
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap_err();
        assert!(matches!(err, RootError::NoWorkspaceRoot { .. }));
    }

    #[test]
    fn disc_020_haz_as_symlink_to_dir_is_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/target").unwrap();
        fs.add_dir("/a/b").unwrap();
        fs.add_symlink("/a/b/.haz", "/target").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_020_haz_as_symlink_to_file_is_not_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b").unwrap();
        fs.add_file("/file", b"content".to_vec()).unwrap();
        fs.add_symlink("/a/b/.haz", "/file").unwrap();
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap_err();
        assert!(matches!(err, RootError::NoWorkspaceRoot { .. }));
    }

    #[test]
    fn disc_020_git_as_symlink_to_dir_is_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/target").unwrap();
        fs.add_dir("/a/b").unwrap();
        fs.add_symlink("/a/b/.git", "/target").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a/b"));
    }

    #[test]
    fn disc_020_git_as_symlink_to_file_is_marker() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b").unwrap();
        fs.add_file("/gitlink", b"gitdir: /elsewhere\n".to_vec())
            .unwrap();
        fs.add_symlink("/a/b/.git", "/gitlink").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/a/b"));
    }

    #[test]
    fn haz_as_fifo_is_not_marker_and_walk_continues() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/.haz").unwrap();
        fs.add_dir("/a/b").unwrap();
        fs.add_fifo("/a/b/.haz").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/"));
    }

    #[test]
    fn git_as_fifo_is_not_marker_and_walk_continues() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/.git").unwrap();
        fs.add_dir("/a/b").unwrap();
        fs.add_fifo("/a/b/.git").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/"));
    }

    #[test]
    fn haz_symlink_loop_surfaces_as_fs_error() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/a/b").unwrap();
        fs.add_symlink("/a/b/.haz", "/a/b/.haz").unwrap();
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("/a/b"))
            .unwrap_err();
        match err {
            RootError::Fs { source, .. } => {
                assert!(matches!(source, haz_vfs::FsError::SymlinkLoop { .. }));
            }
            other => panic!("expected RootError::Fs, got {other:?}"),
        }
    }

    #[test]
    fn canonicalises_root_through_symlink_in_ancestor() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/real/dir/.haz").unwrap();
        fs.add_symlink("/link", "/real/dir").unwrap();
        fs.add_dir("/real/dir/sub").unwrap();
        // Start path enters via the symlink; the chosen ancestor is
        // /link, but `canonicalize` resolves it to /real/dir.
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/link/sub"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/real/dir"));
    }

    #[test]
    fn finds_explicit_at_filesystem_root() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/.haz").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/"));
    }

    #[test]
    fn walks_up_to_filesystem_root_with_haz() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/.haz").unwrap();
        fs.add_dir("/sub/deeper").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/sub/deeper"))
            .unwrap();
        assert_eq!(r, explicit(&fs, "/"));
    }

    #[test]
    fn walk_does_not_climb_past_inner_git_to_outer_explicit_far_apart() {
        let mut fs = MemFilesystem::new();
        fs.add_dir("/outer/.haz").unwrap();
        fs.add_dir("/outer/inner").unwrap();
        fs.add_dir("/outer/inner/.git").unwrap();
        fs.add_dir("/outer/inner/sub/leaf").unwrap();
        let r = Discovery::new(&fs)
            .find_workspace_root(Path::new("/outer/inner/sub/leaf"))
            .unwrap();
        assert_eq!(r, implicit(&fs, "/outer/inner"));
    }
}

#[cfg(all(test, unix))]
mod std_smoke_tests {
    use std::fs as std_fs;
    use std::path::Path;

    use tempfile::TempDir;

    use super::WorkspaceRoot;
    use crate::discovery::Discovery;
    use haz_vfs::StdFilesystem;

    #[test]
    fn disc_001_std_finds_explicit_in_tempdir() {
        let tmp = TempDir::new().unwrap();
        let real = std_fs::canonicalize(tmp.path()).unwrap();
        let nested = real.join("sub").join("leaf");
        std_fs::create_dir_all(&nested).unwrap();
        std_fs::create_dir(real.join(".haz")).unwrap();

        let fs = StdFilesystem;
        let r = Discovery::new(&fs).find_workspace_root(&nested).unwrap();
        match r {
            WorkspaceRoot::Explicit { lexical, .. } => {
                assert_eq!(lexical.as_path(), real.as_path());
            }
            WorkspaceRoot::Implicit { .. } => panic!("expected Explicit"),
        }
    }

    #[test]
    fn disc_001_std_finds_implicit_with_git_file_gitlink() {
        let tmp = TempDir::new().unwrap();
        let real = std_fs::canonicalize(tmp.path()).unwrap();
        let nested = real.join("inner");
        std_fs::create_dir(&nested).unwrap();
        // Create a regular-file `.git` (gitlink form, e.g. submodule).
        std_fs::write(real.join(".git"), "gitdir: /elsewhere\n").unwrap();

        let fs = StdFilesystem;
        let r = Discovery::new(&fs).find_workspace_root(&nested).unwrap();
        match r {
            WorkspaceRoot::Implicit { lexical, .. } => {
                assert_eq!(lexical.as_path(), real.as_path());
            }
            WorkspaceRoot::Explicit { .. } => panic!("expected Implicit"),
        }
    }

    #[test]
    fn disc_001_std_relative_start_returns_not_absolute() {
        let fs = StdFilesystem;
        let err = Discovery::new(&fs)
            .find_workspace_root(Path::new("relative/path"))
            .unwrap_err();
        assert!(matches!(err, super::RootError::NotAbsolute { .. }));
    }
}
