//! Core fstab types: `Entry`, `Fstab`, `MountPoint`, `EntryBuilder`.
//!
//! These are the primary data structures used to represent an `/etc/fstab`
//! file and its individual entries.

use crate::error::{EntryBuilderError, MountPointError};
use crate::fstype::FsType;
use crate::options::Options;
use crate::spec::Spec;
use std::fmt;
use std::path::{Path, PathBuf};

/// Mount point — fstab(5) field 2.
///
/// An absolute path or the special value `none` for swap entries.
///
/// # Examples
///
/// ```
/// # use mount_fstab::types::MountPoint;
/// let mp = MountPoint::new("/").unwrap();
/// assert!(mp.is_root());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MountPoint(PathBuf);

impl MountPoint {
    /// Create a mount point from a path.
    ///
    /// The path must be absolute (start with `/`) or be the special value
    /// `none`.
    ///
    /// # Errors
    ///
    /// Returns [`MountPointError::Empty`] if the path is empty,
    /// [`MountPointError::NotAbsolute`] if the path is relative.
    pub fn new(path: impl Into<PathBuf>) -> Result<Self, MountPointError> {
        let path = path.into();
        let s = path.to_string_lossy();
        if s.is_empty() {
            return Err(MountPointError::Empty);
        }
        if s == "none" || s.starts_with('/') {
            Ok(MountPoint(path))
        } else {
            Err(MountPointError::NotAbsolute)
        }
    }

    /// Create the special `none` mount point for swap.
    #[must_use]
    pub fn swap() -> Self {
        MountPoint(PathBuf::from("none"))
    }

    /// Whether this is a swap mount point (`none`).
    #[must_use]
    pub fn is_swap(&self) -> bool {
        self.0.to_string_lossy() == "none"
    }

    /// Whether this is the root filesystem (`/`).
    ///
    /// Normalizes paths like `//` to `/` via `Path::components()`.
    #[must_use]
    pub fn is_root(&self) -> bool {
        let normalized: PathBuf = self.0.components().collect();
        normalized == Path::new("/")
    }

    /// View the mount point as a `Path`.
    #[must_use]
    pub fn as_path(&self) -> &Path {
        &self.0
    }
}

impl std::ops::Deref for MountPoint {
    type Target = Path;

    /// Dereference to the underlying `Path`.
    ///
    /// This allows `&MountPoint` to be used wherever `&Path` is expected.
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl fmt::Display for MountPoint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0.display())
    }
}

impl AsRef<Path> for MountPoint {
    fn as_ref(&self) -> &Path {
        &self.0
    }
}

/// Try to convert a string into a `MountPoint`.
///
/// Equivalent to [`MountPoint::new`].
impl TryFrom<&str> for MountPoint {
    type Error = MountPointError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        MountPoint::new(s)
    }
}

/// Try to convert a `String` into a `MountPoint`.
///
/// Equivalent to [`MountPoint::new`].
impl TryFrom<String> for MountPoint {
    type Error = MountPointError;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        MountPoint::new(s)
    }
}

/// A single fstab entry — corresponds to libmount `struct libmnt_fs`.
///
/// Each entry represents one line in `/etc/fstab`, consisting of six
/// whitespace-separated fields plus an optional preceding comment block.
///
/// # Examples
///
/// ```
/// # use mount_fstab::{Entry, Spec, MountPoint, FsType, Options};
/// let entry = Entry {
///     spec: Spec::parse("UUID=root").unwrap(),
///     file: MountPoint::new("/").unwrap(),
///     vfstype: FsType::parse("ext4").unwrap(),
///     options: Options::parse("defaults").unwrap(),
///     freq: 0,
///     passno: 1,
///     comment: None,
/// };
/// assert!(entry.is_root());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Entry {
    /// Filesystem source (device, UUID, label, etc.) — fstab(5) field 1.
    pub spec: Spec,
    /// Mount point path — fstab(5) field 2 (`fs_file`).
    pub file: MountPoint,
    /// Filesystem type — fstab(5) field 3 (`fs_vfstype`).
    pub vfstype: FsType,
    /// Mount options — fstab(5) field 4 (`fs_mntops`).
    pub options: Options,
    /// Dump frequency — fstab(5) field 5 (`fs_freq`). Default: 0.
    pub freq: u32,
    /// Filesystem check order — fstab(5) field 6 (`fs_passno`). Default: 0.
    pub passno: u32,
    /// Comment lines immediately above this entry.
    pub comment: Option<String>,
}

impl Entry {
    /// Create a new entry with the required fields.
    ///
    /// `freq` and `passno` default to 0, and `comment` defaults to `None`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Spec, MountPoint, FsType, Options};
    /// let entry = Entry::new(
    ///     Spec::parse("UUID=root").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::parse("defaults").unwrap(),
    /// );
    /// assert!(entry.file.is_root());
    /// assert_eq!(entry.freq, 0);
    /// assert_eq!(entry.passno, 0);
    /// assert!(entry.comment.is_none());
    /// ```
    #[must_use]
    pub fn new(spec: Spec, file: MountPoint, vfstype: FsType, options: Options) -> Self {
        Entry {
            spec,
            file,
            vfstype,
            options,
            freq: 0,
            passno: 0,
            comment: None,
        }
    }

    /// Create an [`EntryBuilder`] for ergonomic construction with optional fields.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Spec, MountPoint, FsType, Options};
    /// let entry = Entry::builder()
    ///     .spec(Spec::parse("UUID=root").unwrap())
    ///     .file(MountPoint::new("/").unwrap())
    ///     .vfstype(FsType::parse("ext4").unwrap())
    ///     .options(Options::parse("defaults,noatime").unwrap())
    ///     .freq(0)
    ///     .passno(1)
    ///     .comment("# Root filesystem")
    ///     .build()
    ///     .unwrap();
    /// assert!(entry.is_root());
    /// ```
    #[must_use]
    pub fn builder() -> EntryBuilder {
        EntryBuilder::default()
    }

    /// Whether this is a swap entry.
    #[must_use]
    pub fn is_swap(&self) -> bool {
        self.vfstype.is_swap()
    }

    /// Whether this is a bind mount.
    #[must_use]
    pub fn is_bind_mount(&self) -> bool {
        self.vfstype.is_bind()
    }

    /// Whether this is the root filesystem entry.
    ///
    /// Returns `true` only when the mount point is `/` **and** `passno == 1`,
    /// following the fstab(5) convention that the root filesystem must have
    /// `fs_passno` set to 1. An entry at `/` with `passno != 1` is not
    /// considered the root entry by this method.
    #[must_use]
    pub fn is_root(&self) -> bool {
        self.file.is_root() && self.passno == 1
    }

    /// Whether this is a network filesystem.
    #[must_use]
    pub fn is_network(&self) -> bool {
        self.vfstype.is_network() || self.options.is_netdev()
    }

    /// Get the comment lines before this entry, if any.
    #[must_use]
    pub fn comment(&self) -> Option<&str> {
        self.comment.as_deref()
    }
}

/// Builder for [`Entry`] with ergonomic construction of optional fields.
///
/// Created via [`Entry::builder()`]. All fields are optional except `spec`,
/// `file`, and `vfstype`, which are required by [`build`](EntryBuilder::build).
///
/// # Examples
///
/// ```
/// # use mount_fstab::{Entry, Spec, MountPoint, FsType, Options};
/// let entry = Entry::builder()
///     .spec(Spec::parse("/dev/sda1").unwrap())
///     .file(MountPoint::new("/").unwrap())
///     .vfstype(FsType::parse("ext4").unwrap())
///     .comment("# Root")
///     .build()
///     .unwrap();
/// ```
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EntryBuilder {
    spec: Option<Spec>,
    file: Option<MountPoint>,
    vfstype: Option<FsType>,
    options: Options,
    freq: u32,
    passno: u32,
    comment: Option<String>,
}

impl EntryBuilder {
    /// Set the filesystem spec (required).
    pub fn spec(mut self, spec: Spec) -> Self {
        self.spec = Some(spec);
        self
    }
    /// Set the mount point (required).
    pub fn file(mut self, file: MountPoint) -> Self {
        self.file = Some(file);
        self
    }
    /// Set the filesystem type (required).
    pub fn vfstype(mut self, vfstype: FsType) -> Self {
        self.vfstype = Some(vfstype);
        self
    }
    /// Set the mount options (default: empty).
    pub fn options(mut self, options: Options) -> Self {
        self.options = options;
        self
    }
    /// Set the dump frequency (default: 0).
    pub fn freq(mut self, freq: u32) -> Self {
        self.freq = freq;
        self
    }
    /// Set the filesystem check order (default: 0).
    pub fn passno(mut self, passno: u32) -> Self {
        self.passno = passno;
        self
    }
    /// Set the comment preceding this entry.
    pub fn comment(mut self, comment: impl Into<String>) -> Self {
        self.comment = Some(comment.into());
        self
    }

    /// Build the [`Entry`], consuming the builder.
    ///
    /// # Errors
    ///
    /// Returns [`EntryBuilderError`] if any required fields (`spec`, `file`,
    /// `vfstype`) are missing.
    pub fn build(self) -> Result<Entry, EntryBuilderError> {
        Ok(Entry {
            spec: self.spec.ok_or(EntryBuilderError::MissingSpec)?,
            file: self.file.ok_or(EntryBuilderError::MissingFile)?,
            vfstype: self.vfstype.ok_or(EntryBuilderError::MissingFsType)?,
            options: self.options,
            freq: self.freq,
            passno: self.passno,
            comment: self.comment,
        })
    }
}

/// A complete `/etc/fstab` file representation.
///
/// Corresponds to libmount `struct libmnt_table`. Contains the file's
/// entries along with any leading (intro) and trailing comments.
///
/// # Examples
///
/// ```
/// # use mount_fstab::Fstab;
/// let input = "UUID=root / ext4 defaults 0 1\n";
/// let fstab = Fstab::parse_str(input).unwrap();
/// assert_eq!(fstab.len(), 1);
/// ```
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Fstab {
    /// Comment block at the beginning of the file (before the first entry).
    pub intro_comment: Option<String>,
    /// All entries, in original file order.
    pub entries: Vec<Entry>,
    /// Comment block at the end of the file (after the last entry).
    pub trailing_comment: Option<String>,
}

impl Fstab {
    /// Create an empty fstab.
    #[must_use]
    pub fn new() -> Self {
        Fstab::default()
    }

    /// Number of entries.
    #[must_use]
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Whether the fstab has no entries.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Return a slice of all entries.
    #[must_use]
    pub fn entries(&self) -> &[Entry] {
        &self.entries
    }

    /// Create an [`Fstab`] from a vector of entries.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Fstab, Spec, MountPoint, FsType, Options};
    /// let entry = Entry::new(
    ///     Spec::parse("/dev/sda1").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::defaults(),
    /// );
    /// let fstab = Fstab::from_entries(vec![entry]);
    /// assert_eq!(fstab.len(), 1);
    /// ```
    #[must_use]
    pub fn from_entries(entries: Vec<Entry>) -> Self {
        Fstab {
            entries,
            intro_comment: None,
            trailing_comment: None,
        }
    }

    /// Consume the `Fstab` and return its entries.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Fstab, Spec, MountPoint, FsType, Options};
    /// let mut fstab = Fstab::new();
    /// fstab.add(Entry::new(
    ///     Spec::parse("/dev/sda1").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::defaults(),
    /// ));
    /// let entries = fstab.into_entries();
    /// assert_eq!(entries.len(), 1);
    /// ```
    #[must_use]
    pub fn into_entries(self) -> Vec<Entry> {
        self.entries
    }

    /// Add an entry to the end.
    ///
    /// Returns `&mut Self` for chaining.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Fstab, Spec, MountPoint, FsType, Options};
    /// let mut fstab = Fstab::new();
    /// let entry = Entry::new(
    ///     Spec::parse("UUID=root").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::defaults(),
    /// );
    /// fstab.add(entry);
    /// assert_eq!(fstab.len(), 1);
    /// ```
    pub fn add(&mut self, entry: Entry) -> &mut Self {
        self.entries.push(entry);
        self
    }

    /// Insert an entry at the given position.
    ///
    /// Returns `&mut Self` for chaining.
    ///
    /// # Errors
    ///
    /// Returns [`FstabError`](crate::error::FstabError) as
    /// `IndexOutOfBounds(index, len)` if `index > len`.
    pub fn insert(
        &mut self,
        index: usize,
        entry: Entry,
    ) -> Result<&mut Self, crate::error::FstabError> {
        if index > self.entries.len() {
            return Err(crate::error::FstabError::IndexOutOfBounds(
                index,
                self.entries.len(),
            ));
        }
        self.entries.insert(index, entry);
        Ok(self)
    }

    /// Remove an entry at the given position, returning it.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Fstab, Spec, MountPoint, FsType, Options};
    /// let mut fstab = Fstab::new();
    /// fstab.add(Entry::new(
    ///     Spec::parse("UUID=root").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::defaults(),
    /// ));
    /// let removed = fstab.remove(0);
    /// assert!(removed.is_some());
    /// assert!(fstab.is_empty());
    /// ```
    pub fn remove(&mut self, index: usize) -> Option<Entry> {
        if index < self.entries.len() {
            Some(self.entries.remove(index))
        } else {
            None
        }
    }

    /// Replace an entry at the given position, returning the old entry.
    pub fn replace(&mut self, index: usize, entry: Entry) -> Option<Entry> {
        if index < self.entries.len() {
            Some(std::mem::replace(&mut self.entries[index], entry))
        } else {
            None
        }
    }

    /// Remove all entries and comments.
    pub fn clear(&mut self) {
        self.entries.clear();
        self.intro_comment = None;
        self.trailing_comment = None;
    }

    /// Find entries by source string match (substring match on spec).
    #[must_use]
    pub fn find_by_source(&self, source: &str) -> Vec<&Entry> {
        self.entries
            .iter()
            .filter(|e| e.spec.to_string().contains(source))
            .collect()
    }

    /// Find an entry by mount point.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Entry, Fstab, Spec, MountPoint, FsType, Options};
    /// # use std::path::Path;
    /// let mut fstab = Fstab::new();
    /// fstab.add(Entry::new(
    ///     Spec::parse("UUID=root").unwrap(),
    ///     MountPoint::new("/").unwrap(),
    ///     FsType::parse("ext4").unwrap(),
    ///     Options::defaults(),
    /// ));
    /// assert!(fstab.find_by_mountpoint(Path::new("/")).is_some());
    /// assert!(fstab.find_by_mountpoint(Path::new("/nonexistent")).is_none());
    /// ```
    #[must_use]
    pub fn find_by_mountpoint(&self, mp: &Path) -> Option<&Entry> {
        self.entries.iter().find(|e| e.file.as_path() == mp)
    }

    /// Get the root filesystem entry (passno == 1 at `/`).
    #[must_use]
    pub fn root(&self) -> Option<&Entry> {
        self.entries
            .iter()
            .find(|e| e.passno == 1 && e.file.is_root())
    }
}

impl IntoIterator for Fstab {
    type Item = Entry;
    type IntoIter = std::vec::IntoIter<Entry>;

    /// Consume the `Fstab` and iterate over its entries.
    fn into_iter(self) -> Self::IntoIter {
        self.entries.into_iter()
    }
}

impl<'a> IntoIterator for &'a Fstab {
    type Item = &'a Entry;
    type IntoIter = std::slice::Iter<'a, Entry>;

    /// Iterate over references to entries.
    fn into_iter(self) -> Self::IntoIter {
        self.entries.iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::EntryBuilderError;
    use crate::fstype::FsType;
    use crate::options::Options;
    use crate::spec::Spec;
    use std::path::Path;

    // ── MountPoint tests ──

    #[test]
    fn mount_point_new_absolute_path() {
        let mp = MountPoint::new("/mnt/data").unwrap();
        assert_eq!(mp.as_path(), Path::new("/mnt/data"));
        assert!(!mp.is_swap());
        assert!(!mp.is_root());
    }

    #[test]
    fn mount_point_new_root() {
        let mp = MountPoint::new("/").unwrap();
        assert!(mp.is_root());
        assert!(!mp.is_swap());
    }

    #[test]
    fn mount_point_new_none() {
        let mp = MountPoint::new("none").unwrap();
        assert!(mp.is_swap());
        assert!(!mp.is_root());
    }

    #[test]
    fn mount_point_new_empty() {
        let err = MountPoint::new("").unwrap_err();
        assert_eq!(err, MountPointError::Empty);
    }

    #[test]
    fn mount_point_new_not_absolute() {
        let err = MountPoint::new("relative/path").unwrap_err();
        assert_eq!(err, MountPointError::NotAbsolute);
    }

    #[test]
    fn mount_point_swap_constructor() {
        let mp = MountPoint::swap();
        assert!(mp.is_swap());
        assert_eq!(mp.as_path(), Path::new("none"));
    }

    #[test]
    fn mount_point_deref() {
        let mp = MountPoint::new("/etc").unwrap();
        let path: &Path = &*mp;
        assert_eq!(path, Path::new("/etc"));
        // Can call Path methods directly
        assert!(mp.is_absolute());
        assert!(mp.parent() == Some(Path::new("/")));
    }

    // ── Entry tests ──

    #[test]
    fn entry_new_defaults() {
        let spec = Spec::Device("/dev/sda1".into());
        let file = MountPoint::new("/").unwrap();
        let fstype = FsType::new("ext4").unwrap();
        let opts = Options::defaults();
        let entry = Entry::new(spec.clone(), file.clone(), fstype.clone(), opts.clone());

        assert_eq!(entry.spec, spec);
        assert_eq!(entry.file, file);
        assert_eq!(entry.vfstype, fstype);
        assert_eq!(entry.options, opts);
        assert_eq!(entry.freq, 0);
        assert_eq!(entry.passno, 0);
        assert_eq!(entry.comment, None);
    }

    #[test]
    fn entry_is_swap() {
        let entry = Entry {
            spec: Spec::Keyword("none".into()),
            file: MountPoint::new("none").unwrap(),
            vfstype: FsType::swap(),
            options: Options::defaults(),
            freq: 0,
            passno: 0,
            comment: None,
        };
        assert!(entry.is_swap());
    }

    #[test]
    fn entry_is_bind_mount() {
        let entry = Entry {
            spec: Spec::Device("/dev/sda1".into()),
            file: MountPoint::new("/mnt/bind").unwrap(),
            vfstype: FsType::bind(),
            options: Options::defaults(),
            freq: 0,
            passno: 0,
            comment: None,
        };
        assert!(entry.is_bind_mount());
    }

    #[test]
    fn entry_is_root() {
        let entry = Entry {
            spec: Spec::Device("/dev/sda1".into()),
            file: MountPoint::new("/").unwrap(),
            vfstype: FsType::new("ext4").unwrap(),
            options: Options::defaults(),
            freq: 0,
            passno: 1,
            comment: None,
        };
        assert!(entry.is_root());
    }

    #[test]
    fn entry_is_root_requires_passno() {
        let entry = Entry {
            spec: Spec::Device("/dev/sda1".into()),
            file: MountPoint::new("/").unwrap(),
            vfstype: FsType::new("ext4").unwrap(),
            options: Options::defaults(),
            freq: 0,
            passno: 0,
            comment: None,
        };
        assert!(!entry.is_root());
    }

    #[test]
    fn entry_is_network_by_fstype() {
        let entry = Entry {
            spec: Spec::NetworkMount {
                host: "server".into(),
                path: "/export".into(),
            },
            file: MountPoint::new("/mnt/nfs").unwrap(),
            vfstype: FsType::new("nfs").unwrap(),
            options: Options::new(),
            freq: 0,
            passno: 0,
            comment: None,
        };
        assert!(entry.is_network());
    }

    #[test]
    fn entry_is_network_by_netdev_option() {
        let entry = Entry {
            spec: Spec::Device("/dev/sda1".into()),
            file: MountPoint::new("/mnt/data").unwrap(),
            vfstype: FsType::new("ext4").unwrap(),
            options: Options::parse("_netdev").unwrap(),
            freq: 0,
            passno: 0,
            comment: None,
        };
        assert!(entry.is_network());
    }

    #[test]
    fn entry_builder_minimal() {
        let entry = Entry::builder()
            .spec(Spec::parse("/dev/sda1").unwrap())
            .file(MountPoint::new("/").unwrap())
            .vfstype(FsType::parse("ext4").unwrap())
            .build()
            .unwrap();
        assert_eq!(entry.spec, Spec::Device("/dev/sda1".into()));
        assert!(entry.file.is_root());
        assert_eq!(entry.vfstype.as_str(), "ext4");
        assert!(entry.options.is_empty());
        assert_eq!(entry.freq, 0);
        assert_eq!(entry.passno, 0);
    }

    #[test]
    fn entry_builder_all_fields() {
        let entry = Entry::builder()
            .spec(Spec::parse("UUID=root").unwrap())
            .file(MountPoint::new("/").unwrap())
            .vfstype(FsType::parse("ext4").unwrap())
            .options(Options::parse("defaults,noatime").unwrap())
            .freq(0)
            .passno(1)
            .comment("# Root filesystem")
            .build()
            .unwrap();
        assert_eq!(entry.spec, Spec::Uuid("root".into()));
        assert!(entry.file.is_root());
        assert_eq!(entry.passno, 1);
        assert_eq!(entry.comment, Some("# Root filesystem".into()));
    }

    #[test]
    fn entry_builder_missing_spec() {
        let err = Entry::builder()
            .file(MountPoint::new("/").unwrap())
            .vfstype(FsType::parse("ext4").unwrap())
            .build()
            .unwrap_err();
        assert_eq!(err, EntryBuilderError::MissingSpec);
    }

    #[test]
    fn entry_builder_missing_file() {
        let err = Entry::builder()
            .spec(Spec::parse("/dev/sda1").unwrap())
            .vfstype(FsType::parse("ext4").unwrap())
            .build()
            .unwrap_err();
        assert_eq!(err, EntryBuilderError::MissingFile);
    }

    #[test]
    fn entry_builder_missing_vfstype() {
        let err = Entry::builder()
            .spec(Spec::parse("/dev/sda1").unwrap())
            .file(MountPoint::new("/").unwrap())
            .build()
            .unwrap_err();
        assert_eq!(err, EntryBuilderError::MissingFsType);
    }

    #[test]
    fn entry_comment_accessor() {
        let entry = Entry {
            comment: Some("# Test".into()),
            ..Entry::new(
                Spec::Device("/dev/sda1".into()),
                MountPoint::new("/").unwrap(),
                FsType::new("ext4").unwrap(),
                Options::defaults(),
            )
        };
        assert_eq!(entry.comment(), Some("# Test"));
    }

    // ── Fstab tests ──

    #[test]
    fn fstab_new_is_empty() {
        let fstab = Fstab::new();
        assert!(fstab.is_empty());
        assert_eq!(fstab.len(), 0);
    }

    #[test]
    fn fstab_add_entry() {
        let mut fstab = Fstab::new();
        let entry = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        fstab.add(entry);
        assert_eq!(fstab.len(), 1);
        assert!(!fstab.is_empty());
    }

    #[test]
    fn fstab_add_chaining() {
        let mut fstab = Fstab::new();
        let e1 = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        let e2 = Entry::new(
            Spec::Device("/dev/sda2".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        fstab.add(e1).add(e2);
        assert_eq!(fstab.len(), 2);
    }

    #[test]
    fn fstab_insert_valid() {
        let mut fstab = Fstab::new();
        let e1 = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        let e2 = Entry::new(
            Spec::Device("/dev/sda2".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        fstab.add(e1);
        let e3 = Entry::new(
            Spec::Device("/dev/sdb1".into()),
            MountPoint::new("/mnt/data").unwrap(),
            FsType::new("xfs").unwrap(),
            Options::defaults(),
        );
        // Insert at end with chaining
        assert!(fstab.insert(1, e3).is_ok());
        assert_eq!(fstab.len(), 2);
        // Insert at beginning
        assert!(fstab.insert(0, e2).is_ok());
        assert_eq!(fstab.len(), 3);
    }

    #[test]
    fn fstab_insert_out_of_bounds() {
        let mut fstab = Fstab::new();
        let entry = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        let result = fstab.insert(1, entry);
        assert!(result.is_err());
    }

    #[test]
    fn fstab_remove() {
        let mut fstab = Fstab::new();
        let entry = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        fstab.add(entry);
        let removed = fstab.remove(0);
        assert!(removed.is_some());
        assert!(fstab.is_empty());

        let none = fstab.remove(0);
        assert!(none.is_none());
    }

    #[test]
    fn fstab_replace() {
        let mut fstab = Fstab::new();
        let e1 = Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        );
        let e2 = Entry::new(
            Spec::Device("/dev/sdb1".into()),
            MountPoint::new("/mnt/data").unwrap(),
            FsType::new("xfs").unwrap(),
            Options::defaults(),
        );
        fstab.add(e1);
        let old = fstab.replace(0, e2);
        assert!(old.is_some());
        assert_eq!(fstab.len(), 1);

        let none = fstab.replace(5, old.unwrap());
        assert!(none.is_none());
    }

    #[test]
    fn fstab_clear() {
        let mut fstab = Fstab::new();
        fstab.intro_comment = Some("# intro".into());
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        fstab.trailing_comment = Some("# trailing".into());
        fstab.clear();
        assert!(fstab.is_empty());
        assert!(fstab.intro_comment.is_none());
        assert!(fstab.trailing_comment.is_none());
    }

    #[test]
    fn fstab_find_by_source() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        fstab.add(Entry::new(
            Spec::Uuid("abc-123".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        let results = fstab.find_by_source("sda1");
        assert_eq!(results.len(), 1);
        let results = fstab.find_by_source("ext4");
        assert_eq!(results.len(), 0);
    }

    #[test]
    fn fstab_find_by_source_label() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Label("ROOT".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        let results = fstab.find_by_source("LABEL=ROOT");
        assert_eq!(results.len(), 1);
    }

    #[test]
    fn fstab_find_by_mountpoint() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        fstab.add(Entry::new(
            Spec::Device("/dev/sda2".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        let found = fstab.find_by_mountpoint(Path::new("/home"));
        assert!(found.is_some());
        assert!(
            fstab
                .find_by_mountpoint(Path::new("/nonexistent"))
                .is_none()
        );
    }

    #[test]
    fn fstab_root() {
        let mut fstab = Fstab::new();
        let root_entry = Entry {
            spec: Spec::Device("/dev/sda1".into()),
            file: MountPoint::new("/").unwrap(),
            vfstype: FsType::new("ext4").unwrap(),
            options: Options::defaults(),
            freq: 0,
            passno: 1,
            comment: None,
        };
        fstab.add(Entry::new(
            Spec::Device("/dev/sda2".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        fstab.add(root_entry);
        let root = fstab.root();
        assert!(root.is_some());
        assert!(root.unwrap().is_root());
    }

    #[test]
    fn fstab_root_no_root_entry() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        assert!(fstab.root().is_none());
    }

    #[test]
    fn fstab_entries_slice() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        assert_eq!(fstab.entries().len(), 1);
    }

    #[test]
    fn fstab_into_iter() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));
        fstab.add(Entry::new(
            Spec::Device("/dev/sda2".into()),
            MountPoint::new("/home").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));

        let count = fstab.into_iter().count();
        assert_eq!(count, 2);
    }

    #[test]
    fn fstab_ref_into_iter() {
        let mut fstab = Fstab::new();
        fstab.add(Entry::new(
            Spec::Device("/dev/sda1".into()),
            MountPoint::new("/").unwrap(),
            FsType::new("ext4").unwrap(),
            Options::defaults(),
        ));

        let entries: Vec<&Entry> = (&fstab).into_iter().collect();
        assert_eq!(entries.len(), 1);
    }

    // ── Spec Display tests ──

    #[test]
    fn spec_display_device() {
        let spec = Spec::Device("/dev/sda1".into());
        assert_eq!(spec.to_string(), "/dev/sda1");
    }

    #[test]
    fn spec_display_label() {
        let spec = Spec::Label("ROOT".into());
        assert_eq!(spec.to_string(), "LABEL=ROOT");
    }

    #[test]
    fn spec_display_uuid() {
        let spec = Spec::Uuid("abc-123".into());
        assert_eq!(spec.to_string(), "UUID=abc-123");
    }

    #[test]
    fn spec_display_partlabel() {
        let spec = Spec::PartLabel("System".into());
        assert_eq!(spec.to_string(), "PARTLABEL=System");
    }

    #[test]
    fn spec_display_partuuid() {
        let spec = Spec::PartUuid("abc-def".into());
        assert_eq!(spec.to_string(), "PARTUUID=abc-def");
    }

    #[test]
    fn spec_display_id() {
        #[allow(deprecated)]
        let spec = Spec::Id("wwn-0x50014ee2".into());
        assert_eq!(spec.to_string(), "ID=wwn-0x50014ee2");
    }

    #[test]
    fn spec_display_network() {
        let spec = Spec::NetworkMount {
            host: "server".into(),
            path: "/export".into(),
        };
        assert_eq!(spec.to_string(), "server:/export");
    }

    #[test]
    fn spec_display_keyword() {
        let spec = Spec::Keyword("proc".into());
        assert_eq!(spec.to_string(), "proc");
    }
}
