use crate::profile::Profile;
use crate::source::Source;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StorageBudget {
pub available_mb: u64,
pub profile_id: String,
pub internal_mb: u64,
pub fits: Vec<String>,
pub excluded: Vec<String>,
pub total_mb: u64,
pub remaining_mb: u64,
}
#[must_use]
pub fn calculate(available_mb: u64, profile: &Profile, sources: &[Source]) -> StorageBudget {
let internal_mb = 100;
let mut remaining = available_mb.saturating_sub(internal_mb);
let mut fits = Vec::new();
let mut excluded = Vec::new();
let mut total = internal_mb;
let mut sorted: Vec<&Source> = sources
.iter()
.filter(|s| s.enabled && profile.domains.contains(&s.domain))
.collect();
sorted.sort_by_key(|s| s.size_mb);
for source in sorted {
if source.size_mb <= remaining {
remaining -= source.size_mb;
total += source.size_mb;
fits.push(source.name.clone());
} else {
excluded.push(source.name.clone());
}
}
StorageBudget {
available_mb,
profile_id: profile.id.clone(),
internal_mb,
fits,
excluded,
total_mb: total,
remaining_mb: remaining,
}
}
impl std::fmt::Display for StorageBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {}MB / {}MB used ({} sources fit, {} excluded)",
self.profile_id,
self.total_mb,
self.available_mb,
self.fits.len(),
self.excluded.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::Domain;
use crate::source::SourceKind;
#[test]
fn budget_display() {
let profile = Profile::survival();
let budget = calculate(5000, &profile, &[]);
let s = budget.to_string();
assert!(s.contains("survival"));
assert!(s.contains("5000"));
}
fn make_source(id: &str, name: &str, domain: Domain, size_mb: u64) -> Source {
Source::new(id, name, domain, SourceKind::Zim, "", size_mb)
}
#[test]
fn budget_empty() {
let profile = Profile::survival();
let budget = calculate(5000, &profile, &[]);
assert_eq!(budget.internal_mb, 100);
assert_eq!(budget.total_mb, 100);
assert!(budget.fits.is_empty());
}
#[test]
fn budget_fits_small_sources() {
let profile = Profile::survival();
let sources = vec![make_source(
"med",
"Medical Encyclopedia",
Domain::Medicine,
1500,
)];
let budget = calculate(5000, &profile, &sources);
assert_eq!(budget.fits.len(), 1);
assert!(budget.excluded.is_empty());
}
#[test]
fn budget_excludes_too_large() {
let profile = Profile::survival();
let sources = vec![make_source(
"wiki",
"Full Wikipedia",
Domain::Medicine,
50_000,
)];
let budget = calculate(5000, &profile, &sources);
assert!(budget.fits.is_empty());
assert_eq!(budget.excluded.len(), 1);
}
#[test]
fn budget_zero_available() {
let profile = Profile::survival();
let sources = vec![make_source("med", "Medical", Domain::Medicine, 100)];
let budget = calculate(0, &profile, &sources);
assert!(budget.fits.is_empty());
assert_eq!(budget.remaining_mb, 0);
}
#[test]
fn budget_disabled_sources_excluded() {
let profile = Profile::survival();
let sources = vec![
Source::new("med", "Medical", Domain::Medicine, SourceKind::Zim, "", 100)
.with_enabled(false),
];
let budget = calculate(5000, &profile, &sources);
assert!(budget.fits.is_empty());
assert!(budget.excluded.is_empty()); }
#[test]
fn budget_wrong_domain_excluded() {
let profile = Profile::developer(); let sources = vec![make_source("med", "Medical", Domain::Medicine, 100)];
let budget = calculate(5000, &profile, &sources);
assert!(budget.fits.is_empty());
}
#[test]
fn budget_multiple_sources_smallest_first() {
let profile = Profile::survival();
let sources = vec![
make_source("large", "Large", Domain::Medicine, 1500),
make_source("small", "Small", Domain::Medicine, 200),
make_source("medium", "Medium", Domain::Survival, 800),
];
let budget = calculate(5000, &profile, &sources);
assert_eq!(budget.fits.len(), 3);
assert!(budget.excluded.is_empty());
}
#[test]
fn budget_partial_fit() {
let profile = Profile::survival();
let sources = vec![
make_source("small", "Small", Domain::Medicine, 200),
make_source("huge", "Huge", Domain::Medicine, 5000),
];
let budget = calculate(500, &profile, &sources);
assert_eq!(budget.fits.len(), 1);
assert_eq!(budget.excluded.len(), 1);
assert_eq!(budget.fits[0], "Small");
}
}