Skip to main content

hexz_core/format/
version.rs

1//! Format version management and compatibility checking.
2//!
3//! This module defines the versioning strategy for Hexz archive files (`.hxz`),
4//! enabling safe evolution of the on-disk format while maintaining backward and
5//! forward compatibility guarantees. Version negotiation ensures that readers can
6//! detect incompatible archives and provide actionable error messages.
7//!
8//! # Versioning Strategy
9//!
10//! Hexz uses a monotonic integer versioning scheme where each version number
11//! represents a distinct on-disk format:
12//!
13//! - **Version 1**: Initial format with two-level index, LZ4/Zstd compression,
14//!   optional encryption, thin provisioning support
15//! - **Future versions**: Reserved for incompatible format changes (e.g., new
16//!   index structures, alternative serialization formats, block metadata extensions)
17//!
18//! ## Semantic Versioning Alignment
19//!
20//! Format versions are independent of Hexz software versions (semver). A software
21//! release may support multiple format versions for backward compatibility. The
22//! mapping is defined as:
23//!
24//! | Format Version | Hexz Software Versions | Key Changes |
25//! |----------------|--------------------------|-------------|
26//! | 1              | 0.1.0+                   | Initial release |
27//!
28//! # Compatibility Model
29//!
30//! ## Backward Compatibility (Reading Old Archives)
31//!
32//! Hexz readers maintain compatibility with archives created by older software:
33//!
34//! - **`MIN_SUPPORTED_VERSION`**: Oldest format version we can read (currently 1)
35//! - **Upgrade Path**: Archives older than `MIN_SUPPORTED_VERSION` must be migrated
36//!   using the `hexz-migrate` tool (see Migration section below)
37//!
38//! ## Forward Compatibility (Reading New Archives)
39//!
40//! Hexz readers handle archives created by newer software:
41//!
42//! - **`MAX_SUPPORTED_VERSION`**: Newest format version we can read (currently 1)
43//! - **Degraded Mode**: Future versions may enable partial reads with warnings if
44//!   minor features are unrecognized (not yet implemented)
45//! - **Strict Rejection**: Archives with `version > MAX_SUPPORTED_VERSION` are
46//!   rejected with an actionable error message
47//!
48//! # Version Negotiation Workflow
49//!
50//! When opening a archive file:
51//!
52//! ```text
53//! 1. Read header magic bytes (validate "HEXZ" signature)
54//! 2. Read header.version field
55//! 3. Call check_version(header.version)
56//! 4. If VersionCompatibility::Incompatible:
57//!       - Generate human-readable error via compatibility_message()
58//!       - Suggest upgrade/migration path
59//!       - Abort operation
60//! 5. If VersionCompatibility::Full:
61//!       - Proceed with normal operations
62//! 6. If VersionCompatibility::Degraded (future):
63//!       - Log warning about unsupported features
64//!       - Proceed with limited functionality
65//! ```
66//!
67//! # Adding New Format Versions
68//!
69//! To introduce a breaking format change:
70//!
71//! ## Step 1: Increment Version Constant
72//!
73//! ```rust,ignore
74//! pub const CURRENT_VERSION: u32 = 2;  // Changed from 1
75//! pub const MAX_SUPPORTED_VERSION: u32 = 2;
76//! ```
77//!
78//! ## Step 2: Update Serialization Logic
79//!
80//! Modify [`crate::format::header::Header`] or [`crate::format::index::MasterIndex`]
81//! to include new fields or change existing structures. Use serde attributes to
82//! maintain compatibility:
83//!
84//! ```rust,no_run
85//! # use serde::{Serialize, Deserialize};
86//! #[derive(Serialize, Deserialize)]
87//! pub struct Header {
88//!     // Existing fields...
89//!     #[serde(skip_serializing_if = "Option::is_none")]
90//!     pub new_feature: Option<String>,  // Version 2+ only
91//! }
92//! ```
93//!
94//! ## Step 3: Conditional Deserialization
95//!
96//! Add version-aware deserialization in reader code:
97//!
98//! ```rust,no_run
99//! # fn read_index_v1<R>(_: R) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
100//! # fn read_index_v2<R>(_: R) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
101//! # fn example<R>(header: hexz_core::format::header::Header, reader: R) -> Result<(), Box<dyn std::error::Error>> {
102//! match header.version {
103//!     1 => {
104//!         // Legacy path for version 1
105//!         read_index_v1(reader)?
106//!     }
107//!     2 => {
108//!         // New path for version 2
109//!         read_index_v2(reader)?
110//!     }
111//!     _ => unreachable!("check_version already validated"),
112//! }
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! ## Step 4: Update Tests
118//!
119//! Add test fixtures for the new format version:
120//!
121//! ```rust,no_run
122//! # fn load_fixture(_: &str) -> hexz_core::Archive { todo!() }
123//! #[test]
124//! fn test_read_v2_archive() {
125//!     let archive = load_fixture("testdata/v2_archive.hxz");
126//!     assert_eq!(archive.header.version, 2);
127//!     // Verify new features work correctly
128//! }
129//! ```
130//!
131//! ## Step 5: Document Migration Path
132//!
133//! Update the migration guide with conversion instructions (see Migration section).
134//!
135//! # Migration Between Versions
136//!
137//! The `hexz-migrate` tool converts archives between format versions:
138//!
139//! ```bash
140//! # Upgrade old archive to current format
141//! hexz-migrate upgrade --input old_v1.hxz --output new_v2.hxz
142//!
143//! # Downgrade for compatibility (if supported)
144//! hexz-migrate downgrade --input new_v2.hxz --output legacy_v1.hxz \
145//!     --target-version 1
146//! ```
147//!
148//! ## Migration Algorithm
149//!
150//! The migration tool performs a streaming rewrite:
151//!
152//! 1. Open source archive with reader for version N
153//! 2. Create destination archive with writer for version M
154//! 3. Stream all blocks through decompression/re-compression
155//! 4. Rebuild index in target format
156//! 5. Preserve metadata (encryption keys, parent references, etc.)
157//!
158//! ## Lossy Migrations
159//!
160//! Downgrading may lose features unsupported in the target version:
161//!
162//! - Version 2 → 1: Hypothetical new features would be discarded with warnings
163//!
164//! # Performance Considerations
165//!
166//! Version checking is performed once per archive open operation:
167//!
168//! - **Overhead**: Negligible (~100ns for integer comparison)
169//! - **Caching**: Version is cached in the archive reader struct
170//! - **Hot Path**: Not in block read/write paths
171//!
172//! # Examples
173//!
174//! ## Basic Version Check
175//!
176//! ```
177//! use hexz_core::format::version::{check_version, VersionCompatibility};
178//!
179//! let archive_version = 1;
180//! let compat = check_version(archive_version);
181//!
182//! match compat {
183//!     VersionCompatibility::Full => {
184//!         println!("Archive is fully compatible");
185//!     }
186//!     VersionCompatibility::Incompatible => {
187//!         eprintln!("Cannot read archive (incompatible version)");
188//!     }
189//!     VersionCompatibility::Degraded => {
190//!         println!("Archive readable with warnings");
191//!     }
192//! }
193//! ```
194//!
195//! ## User-Facing Error Messages
196//!
197//! ```
198//! use hexz_core::format::version::compatibility_message;
199//!
200//! let version = 999;  // Future version
201//! let message = compatibility_message(version);
202//! println!("{}", message);
203//! // Output: "Version 999 is too new (max supported: 1). Please upgrade Hexz."
204//! ```
205//!
206//! ## Reader Implementation
207//!
208//! ```rust,no_run
209//! # use hexz_core::format::version::{check_version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION};
210//! # use hexz_common::Error;
211//! # use hexz_core::format::header::Header;
212//! # use std::path::Path;
213//! # struct Archive { header: Header }
214//! # fn read_header(_: &Path) -> Result<Header, Error> { todo!() }
215//! fn open_archive(path: &Path) -> Result<Archive, Error> {
216//!     let header = read_header(path)?;
217//!
218//!     if !check_version(header.version).is_compatible() {
219//!         return Err(Error::Format(format!(
220//!             "Incompatible version {}. Supported range: [{}, {}]",
221//!             header.version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
222//!         )));
223//!     }
224//!
225//!     // Proceed with version-aware deserialization
226//!     Ok(Archive { header })
227//! }
228//! ```
229
230use std::fmt;
231
232/// Current format version written by this build of Hexz.
233///
234/// This constant defines the format version number written to the `version` field
235/// of new archive headers. It is incremented when the on-disk format changes in
236/// a way that requires readers to adapt their parsing logic.
237///
238/// # Incrementing Policy
239///
240/// Increment `CURRENT_VERSION` when:
241/// - Header fields are added/removed/reordered
242/// - Index structure changes (e.g., new page entry fields)
243/// - Serialization format changes (e.g., switch from bincode to another codec)
244/// - Block metadata semantics change
245///
246/// Do NOT increment for:
247/// - New compression algorithms (use header.compression field)
248/// - New encryption modes (use header.encryption field)
249/// - Performance optimizations that don't affect format
250///
251/// # Current Version
252///
253/// **Version 1** (initial release):
254/// - Two-level index (master index + pages)
255/// - bincode serialization
256/// - LZ4/Zstd compression support
257/// - Optional AES-256-GCM encryption
258/// - Thin provisioning via parent archives
259/// - Dual streams (disk + memory)
260pub const CURRENT_VERSION: u32 = 1;
261
262/// Minimum format version readable by this build.
263///
264/// Archives with `version < MIN_SUPPORTED_VERSION` are rejected with an
265/// [`VersionCompatibility::Incompatible`] error. Users must migrate such
266/// archives using `hexz-migrate upgrade` before reading.
267///
268/// # Rationale
269///
270/// Maintaining backward compatibility indefinitely is untenable as format
271/// complexity grows. This constant defines the "support horizon" for old
272/// archives. When incrementing, ensure migration tooling exists.
273///
274/// # Current Policy
275///
276/// - **Hexz 0.1.x - 0.9.x**: Support version 1 only
277/// - **Hexz 1.0+**: May drop support for version 1 (TBD based on adoption)
278pub const MIN_SUPPORTED_VERSION: u32 = 1;
279
280/// Maximum format version readable by this build.
281///
282/// Archives with `version > MAX_SUPPORTED_VERSION` are rejected unless
283/// degraded mode is enabled (not yet implemented). This prevents crashes
284/// when reading archives created by future Hexz versions.
285///
286/// # Forward Compatibility
287///
288/// Currently, Hexz uses strict versioning: any version mismatch results
289/// in incompatibility. Future releases may implement graceful degradation
290/// for minor version increments (e.g., ignore unknown header fields).
291///
292/// # Upgrade Path
293///
294/// If you encounter a archive with `version > MAX_SUPPORTED_VERSION`,
295/// upgrade Hexz to a newer release that supports that version.
296pub const MAX_SUPPORTED_VERSION: u32 = 1;
297
298/// Result of archive version compatibility analysis.
299///
300/// Returned by [`check_version`] to indicate whether a archive with a given
301/// format version can be read by this build of Hexz. This enum enables
302/// graceful handling of version mismatches with appropriate error messages.
303///
304/// # Variants
305///
306/// - **Full**: Version is within [`MIN_SUPPORTED_VERSION`]..=[`MAX_SUPPORTED_VERSION`]
307///   and all features are supported. Proceed with normal operations.
308///
309/// - **Degraded**: Version is newer than [`MAX_SUPPORTED_VERSION`] but forward
310///   compatibility rules allow partial reads with warnings. Unsupported features
311///   are ignored or substituted with defaults. (Not yet implemented; currently
312///   treated as Incompatible.)
313///
314/// - **Incompatible**: Version is outside the supported range and cannot be read.
315///   The user must upgrade Hexz (for newer archives) or migrate the archive
316///   (for older archives).
317///
318/// # Examples
319///
320/// ```
321/// use hexz_core::format::version::{check_version, VersionCompatibility};
322///
323/// let result = check_version(1);
324/// assert_eq!(result, VersionCompatibility::Full);
325///
326/// let result = check_version(999);
327/// assert_eq!(result, VersionCompatibility::Incompatible);
328/// assert!(!result.is_compatible());
329/// ```
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum VersionCompatibility {
332    /// Fully supported version with all features available.
333    Full,
334    /// Newer version with partial support (warnings emitted, some features unavailable).
335    ///
336    /// This variant is reserved for future forward compatibility modes. Currently,
337    /// all versions outside the supported range are marked as Incompatible.
338    Degraded,
339    /// Incompatible version that cannot be read (too old or too new).
340    Incompatible,
341}
342
343impl VersionCompatibility {
344    /// Tests whether the archive can be read with this compatibility status.
345    ///
346    /// Returns `true` for [`Full`](VersionCompatibility::Full) and
347    /// [`Degraded`](VersionCompatibility::Degraded) compatibility, indicating
348    /// that read operations can proceed (possibly with warnings). Returns `false`
349    /// for [`Incompatible`](VersionCompatibility::Incompatible), indicating that
350    /// the archive must be rejected.
351    ///
352    /// # Returns
353    ///
354    /// - `true`: Archive can be opened (possibly with limited functionality)
355    /// - `false`: Archive cannot be opened (hard error)
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use hexz_core::format::version::{check_version, VersionCompatibility};
361    ///
362    /// let compat = check_version(1);
363    /// if compat.is_compatible() {
364    ///     println!("Archive can be read");
365    /// } else {
366    ///     eprintln!("Archive is incompatible");
367    /// }
368    /// ```
369    pub const fn is_compatible(&self) -> bool {
370        match self {
371            Self::Full | Self::Degraded => true,
372            Self::Incompatible => false,
373        }
374    }
375}
376
377/// Determines compatibility status of a archive format version.
378///
379/// This function implements the version negotiation logic by comparing the
380/// provided version number against the supported range defined by
381/// [`MIN_SUPPORTED_VERSION`] and [`MAX_SUPPORTED_VERSION`].
382///
383/// # Parameters
384///
385/// - `version`: The format version number read from a archive header
386///
387/// # Returns
388///
389/// A [`VersionCompatibility`] value indicating whether the archive can be read:
390///
391/// - [`Full`](VersionCompatibility::Full): Version is within supported range
392/// - [`Incompatible`](VersionCompatibility::Incompatible): Version is too old or too new
393/// - [`Degraded`](VersionCompatibility::Degraded): Currently unused (reserved for future)
394///
395/// # Version Ranges
396///
397/// | Condition | Result | Reason |
398/// |-----------|--------|--------|
399/// | `version < MIN_SUPPORTED_VERSION` | Incompatible | Too old, needs migration |
400/// | `MIN_SUPPORTED_VERSION <= version <= MAX_SUPPORTED_VERSION` | Full | Within supported range |
401/// | `version > MAX_SUPPORTED_VERSION` | Incompatible | Too new, upgrade Hexz |
402///
403/// # Future Extensions
404///
405/// In future versions, this function may return `Degraded` for archives with
406/// minor version mismatches, enabling partial reads with warnings. For example:
407///
408/// ```rust,no_run
409/// # use hexz_core::format::version::{VersionCompatibility, CURRENT_VERSION};
410/// # struct V { major: u32, minor: u32 }
411/// # let version = V { major: 1, minor: 1 };
412/// # let current_version = V { major: 1, minor: 0 };
413/// // Hypothetical future behavior with major.minor versioning
414/// let _compat = if version.major == current_version.major && version.minor > current_version.minor {
415///     VersionCompatibility::Degraded  // Newer minor version, try best-effort read
416/// } else {
417///     VersionCompatibility::Full
418/// };
419/// ```
420///
421/// # Performance
422///
423/// This function performs two integer comparisons and has negligible overhead
424/// (~100ns). It is called once per archive open operation and does not affect
425/// hot path performance.
426///
427/// # Examples
428///
429/// ```
430/// use hexz_core::format::version::{
431///     check_version, VersionCompatibility,
432///     MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
433/// };
434///
435/// // Version within supported range
436/// let compat = check_version(1);
437/// assert_eq!(compat, VersionCompatibility::Full);
438///
439/// // Version too old (hypothetical example if MIN_SUPPORTED_VERSION > 1)
440/// let compat = check_version(0);
441/// assert_eq!(compat, VersionCompatibility::Incompatible);
442///
443/// // Version too new
444/// let compat = check_version(MAX_SUPPORTED_VERSION + 1);
445/// assert_eq!(compat, VersionCompatibility::Incompatible);
446/// ```
447///
448/// ## Error Handling
449///
450/// ```rust,no_run
451/// # use hexz_core::format::version::{check_version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION};
452/// # use hexz_common::{Error, Result};
453/// # use hexz_core::format::header::Header;
454/// # struct Archive;
455/// # impl Archive { fn new(_: &Header) -> Self { Archive } }
456/// fn open_archive(header: &Header) -> Result<Archive> {
457///     let compat = check_version(header.version);
458///     if !compat.is_compatible() {
459///         return Err(Error::Format(format!(
460///             "Incompatible version {}. Supported range: [{}, {}]",
461///             header.version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
462///         )));
463///     }
464///     // Proceed with read operations
465///     Ok(Archive::new(header))
466/// }
467/// ```
468pub const fn check_version(version: u32) -> VersionCompatibility {
469    if version < MIN_SUPPORTED_VERSION {
470        VersionCompatibility::Incompatible
471    } else if version > MAX_SUPPORTED_VERSION {
472        // For now, strict versioning. Future: Check major/minor for degraded mode.
473        VersionCompatibility::Incompatible
474    } else {
475        VersionCompatibility::Full
476    }
477}
478
479/// Generates a user-facing message describing version compatibility status.
480///
481/// This function produces human-readable error messages and recommendations
482/// for handling version mismatches. The messages are designed to be displayed
483/// directly to end users in error logs or CLI output.
484///
485/// # Parameters
486///
487/// - `version`: The format version number from a archive header
488///
489/// # Returns
490///
491/// A `String` containing:
492/// - **Full compatibility**: Confirmation message
493/// - **Degraded compatibility**: Warning about potential feature limitations
494/// - **Incompatibility**: Error message with actionable remediation steps
495///
496/// # Message Content
497///
498/// ## Fully Supported Version
499///
500/// ```text
501/// "Version 1 is fully supported."
502/// ```
503///
504/// ## Too Old (version < `MIN_SUPPORTED_VERSION`)
505///
506/// ```text
507/// "Version 0 is too old (min supported: 1). Please upgrade the archive."
508/// ```
509///
510/// Remediation: Use `hexz-migrate upgrade` to convert the archive.
511///
512/// ## Too New (version > `MAX_SUPPORTED_VERSION`)
513///
514/// ```text
515/// "Version 2 is too new (max supported: 1). Please upgrade Hexz."
516/// ```
517///
518/// Remediation: Install a newer version of the Hexz toolchain.
519///
520/// ## Degraded Compatibility (Future)
521///
522/// ```text
523/// "Version 2 is newer than supported (1), features may be missing."
524/// ```
525///
526/// Remediation: Upgrade Hexz for full feature support, or proceed with warnings.
527///
528/// # Usage in Error Handling
529///
530/// This function is typically called when displaying compatibility errors to users:
531///
532/// ```rust,no_run
533/// # use hexz_core::format::version::{check_version, compatibility_message};
534/// # use hexz_core::format::header::Header;
535/// fn validate_archive(header: &Header) -> Result<(), String> {
536///     let compat = check_version(header.version);
537///     if !compat.is_compatible() {
538///         return Err(compatibility_message(header.version));
539///     }
540///     Ok(())
541/// }
542/// ```
543///
544/// # Examples
545///
546/// ```
547/// use hexz_core::format::version::{compatibility_message, MAX_SUPPORTED_VERSION};
548///
549/// // Supported version
550/// let msg = compatibility_message(1);
551/// assert!(msg.contains("fully supported"));
552///
553/// // Version too new
554/// let msg = compatibility_message(MAX_SUPPORTED_VERSION + 1);
555/// assert!(msg.contains("too new"));
556/// assert!(msg.contains("upgrade Hexz"));
557///
558/// // Version too old (hypothetical if MIN_SUPPORTED_VERSION > 1)
559/// let msg = compatibility_message(0);
560/// assert!(msg.contains("too old"));
561/// assert!(msg.contains("upgrade the archive"));
562/// ```
563///
564/// ## CLI Integration
565///
566/// ```bash
567/// $ hexz open old_archive.hxz
568/// Error: Version 0 is too old (min supported: 1). Please upgrade the archive.
569///
570/// Run: hexz-migrate upgrade old_archive.hxz new_archive.hxz
571/// ```
572pub fn compatibility_message(version: u32) -> String {
573    match check_version(version) {
574        VersionCompatibility::Full => format!("Version {version} is fully supported."),
575        VersionCompatibility::Degraded => format!(
576            "Version {version} is newer than supported ({MAX_SUPPORTED_VERSION}), features may be missing."
577        ),
578        VersionCompatibility::Incompatible => {
579            if version < MIN_SUPPORTED_VERSION {
580                format!(
581                    "Version {version} is too old (min supported: {MIN_SUPPORTED_VERSION}). Please upgrade the archive."
582                )
583            } else {
584                format!(
585                    "Version {version} is too new (max supported: {MAX_SUPPORTED_VERSION}). Please upgrade Hexz."
586                )
587            }
588        }
589    }
590}
591
592impl fmt::Display for VersionCompatibility {
593    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594        match self {
595            Self::Full => write!(f, "full"),
596            Self::Degraded => write!(f, "degraded"),
597            Self::Incompatible => write!(f, "incompatible"),
598        }
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    #[allow(clippy::assertions_on_constants)]
608    fn test_version_constants_are_consistent() {
609        // MIN_SUPPORTED_VERSION must be <= MAX_SUPPORTED_VERSION
610        assert!(
611            MIN_SUPPORTED_VERSION <= MAX_SUPPORTED_VERSION,
612            "MIN_SUPPORTED_VERSION ({MIN_SUPPORTED_VERSION}) must be <= MAX_SUPPORTED_VERSION ({MAX_SUPPORTED_VERSION})"
613        );
614
615        // CURRENT_VERSION must be within supported range
616        assert!(
617            CURRENT_VERSION >= MIN_SUPPORTED_VERSION,
618            "CURRENT_VERSION ({CURRENT_VERSION}) must be >= MIN_SUPPORTED_VERSION ({MIN_SUPPORTED_VERSION})"
619        );
620        assert!(
621            CURRENT_VERSION <= MAX_SUPPORTED_VERSION,
622            "CURRENT_VERSION ({CURRENT_VERSION}) must be <= MAX_SUPPORTED_VERSION ({MAX_SUPPORTED_VERSION})"
623        );
624    }
625
626    #[test]
627    fn test_check_version_current_is_fully_supported() {
628        let compat = check_version(CURRENT_VERSION);
629        assert_eq!(compat, VersionCompatibility::Full);
630        assert!(compat.is_compatible());
631    }
632
633    #[test]
634    fn test_check_version_within_range_is_full() {
635        // Test all versions in the supported range
636        for version in MIN_SUPPORTED_VERSION..=MAX_SUPPORTED_VERSION {
637            let compat = check_version(version);
638            assert_eq!(
639                compat,
640                VersionCompatibility::Full,
641                "Version {version} should be fully compatible"
642            );
643            assert!(compat.is_compatible());
644        }
645    }
646
647    #[test]
648    fn test_check_version_too_old_is_incompatible() {
649        if MIN_SUPPORTED_VERSION > 0 {
650            let old_version = MIN_SUPPORTED_VERSION - 1;
651            let compat = check_version(old_version);
652            assert_eq!(compat, VersionCompatibility::Incompatible);
653            assert!(!compat.is_compatible());
654        }
655
656        // Always test version 0 as too old
657        let compat = check_version(0);
658        assert_eq!(compat, VersionCompatibility::Incompatible);
659        assert!(!compat.is_compatible());
660    }
661
662    #[test]
663    fn test_check_version_too_new_is_incompatible() {
664        let new_version = MAX_SUPPORTED_VERSION + 1;
665        let compat = check_version(new_version);
666        assert_eq!(compat, VersionCompatibility::Incompatible);
667        assert!(!compat.is_compatible());
668
669        // Test very high version number
670        let compat = check_version(9999);
671        assert_eq!(compat, VersionCompatibility::Incompatible);
672        assert!(!compat.is_compatible());
673    }
674
675    #[test]
676    fn test_version_compatibility_is_compatible() {
677        // Full is compatible
678        assert!(VersionCompatibility::Full.is_compatible());
679
680        // Degraded is compatible (with warnings)
681        assert!(VersionCompatibility::Degraded.is_compatible());
682
683        // Incompatible is not compatible
684        assert!(!VersionCompatibility::Incompatible.is_compatible());
685    }
686
687    #[test]
688    fn test_version_compatibility_display() {
689        assert_eq!(format!("{}", VersionCompatibility::Full), "full");
690        assert_eq!(format!("{}", VersionCompatibility::Degraded), "degraded");
691        assert_eq!(
692            format!("{}", VersionCompatibility::Incompatible),
693            "incompatible"
694        );
695    }
696
697    #[test]
698    fn test_compatibility_message_for_supported_version() {
699        let msg = compatibility_message(CURRENT_VERSION);
700        assert!(msg.contains("fully supported"), "Message: {msg}");
701        assert!(msg.contains(&CURRENT_VERSION.to_string()), "Message: {msg}");
702    }
703
704    #[test]
705    fn test_compatibility_message_for_too_old_version() {
706        if MIN_SUPPORTED_VERSION > 0 {
707            let old_version = MIN_SUPPORTED_VERSION - 1;
708            let msg = compatibility_message(old_version);
709            assert!(msg.contains("too old"), "Message: {msg}");
710            assert!(
711                msg.contains(&MIN_SUPPORTED_VERSION.to_string()),
712                "Message: {msg}"
713            );
714            assert!(msg.contains("upgrade the archive"), "Message: {msg}");
715        }
716    }
717
718    #[test]
719    fn test_compatibility_message_for_too_new_version() {
720        let new_version = MAX_SUPPORTED_VERSION + 1;
721        let msg = compatibility_message(new_version);
722        assert!(msg.contains("too new"), "Message: {msg}");
723        assert!(
724            msg.contains(&MAX_SUPPORTED_VERSION.to_string()),
725            "Message: {msg}"
726        );
727        assert!(msg.contains("upgrade Hexz"), "Message: {msg}");
728    }
729
730    #[test]
731    fn test_compatibility_message_for_degraded() {
732        // Since we don't currently return Degraded, we can't directly test it
733        // But we can test the message format by checking what would happen
734        let msg = match VersionCompatibility::Degraded {
735            VersionCompatibility::Degraded => format!(
736                "Version {} is newer than supported ({}), features may be missing.",
737                99, MAX_SUPPORTED_VERSION
738            ),
739            _ => String::new(),
740        };
741        assert!(msg.contains("newer than supported"));
742        assert!(msg.contains("features may be missing"));
743    }
744
745    #[test]
746    fn test_version_compatibility_enum_properties() {
747        // Test Debug trait
748        let full = VersionCompatibility::Full;
749        assert!(format!("{full:?}").contains("Full"));
750
751        // Test Clone and Copy
752        let degraded = VersionCompatibility::Degraded;
753        let degraded_copy = degraded;
754        let degraded_clone = degraded;
755        assert_eq!(degraded, degraded_copy);
756        assert_eq!(degraded, degraded_clone);
757
758        // Test PartialEq and Eq
759        assert_eq!(VersionCompatibility::Full, VersionCompatibility::Full);
760        assert_ne!(
761            VersionCompatibility::Full,
762            VersionCompatibility::Incompatible
763        );
764    }
765
766    #[test]
767    fn test_check_version_boundary_conditions() {
768        // Test MIN_SUPPORTED_VERSION boundary
769        let compat = check_version(MIN_SUPPORTED_VERSION);
770        assert_eq!(compat, VersionCompatibility::Full);
771
772        // Test MAX_SUPPORTED_VERSION boundary
773        let compat = check_version(MAX_SUPPORTED_VERSION);
774        assert_eq!(compat, VersionCompatibility::Full);
775
776        // Test just below MIN
777        if MIN_SUPPORTED_VERSION > 0 {
778            let compat = check_version(MIN_SUPPORTED_VERSION - 1);
779            assert_eq!(compat, VersionCompatibility::Incompatible);
780        }
781
782        // Test just above MAX
783        let compat = check_version(MAX_SUPPORTED_VERSION + 1);
784        assert_eq!(compat, VersionCompatibility::Incompatible);
785    }
786
787    #[test]
788    fn test_multiple_version_checks() {
789        // Verify check_version is consistent across multiple calls
790        let version = CURRENT_VERSION;
791        let result1 = check_version(version);
792        let result2 = check_version(version);
793        assert_eq!(result1, result2);
794    }
795
796    #[test]
797    fn test_compatibility_message_contains_version_number() {
798        // Test that messages include the actual version number
799        for version in [0u32, 1, 2, 99, 999] {
800            let msg = compatibility_message(version);
801            assert!(
802                msg.contains(&version.to_string()),
803                "Message for version {version} should contain the version number: {msg}"
804            );
805        }
806    }
807
808    #[test]
809    #[allow(clippy::assertions_on_constants)]
810    fn test_version_constants_are_reasonable() {
811        // Ensure version numbers are in a reasonable range
812        assert!(CURRENT_VERSION > 0, "CURRENT_VERSION must be positive");
813        assert!(
814            CURRENT_VERSION < 1000,
815            "CURRENT_VERSION seems unreasonably high"
816        );
817        assert!(
818            MIN_SUPPORTED_VERSION > 0,
819            "MIN_SUPPORTED_VERSION must be positive"
820        );
821        assert!(
822            MAX_SUPPORTED_VERSION < 1000,
823            "MAX_SUPPORTED_VERSION seems unreasonably high"
824        );
825    }
826}