use super::{Error as ResolutionError, Forge, ShaDescription, VersionRegistry};
use crate::domain::action::identity::{ActionId, CommitDate, CommitSha, Version};
use crate::domain::action::resolved::Commit;
use crate::domain::action::uses_ref::RefType;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash as _, Hasher as _};
const DEFAULT_DATE: &str = "2026-01-01T00:00:00Z";
#[derive(Clone, Default)]
pub struct FakeRegistry {
tags: HashMap<String, Vec<String>>,
sha_tags: HashMap<(String, String), Vec<String>>,
fixed_sha: Option<String>,
lookup_result: Option<Result<Commit, ResolutionError>>,
error: Option<ResolutionError>,
action_errors: HashMap<String, ResolutionError>,
describe_error: Option<ResolutionError>,
empty_dates: bool,
}
impl FakeRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_all_tags(mut self, id: &str, tags: Vec<&str>) -> Self {
self.tags
.insert(id.to_owned(), tags.into_iter().map(String::from).collect());
self
}
#[must_use]
pub fn with_sha_tags(mut self, id: &str, sha: &str, tags: Vec<&str>) -> Self {
self.sha_tags.insert(
(id.to_owned(), sha.to_owned()),
tags.into_iter().map(String::from).collect(),
);
self
}
#[must_use]
pub fn with_fixed_sha(mut self, sha: &str) -> Self {
self.fixed_sha = Some(sha.to_owned());
self
}
#[must_use]
pub fn with_lookup_result(mut self, result: Result<Commit, ResolutionError>) -> Self {
self.lookup_result = Some(result);
self
}
#[must_use]
pub fn failing_auth(mut self) -> Self {
self.error = Some(ResolutionError::AuthRequired {
forge: Forge::GitHub,
});
self
}
#[must_use]
pub fn failing_action(mut self, id: &str, error: ResolutionError) -> Self {
self.action_errors.insert(id.to_owned(), error);
self
}
#[must_use]
pub fn failing_describe(mut self, error: ResolutionError) -> Self {
self.describe_error = Some(error);
self
}
#[must_use]
pub fn with_empty_dates(mut self) -> Self {
self.empty_dates = true;
self
}
#[must_use]
#[expect(
clippy::string_slice,
reason = "slices a 48-char hex string built here, so the 40-byte boundary is always valid"
)]
pub fn fake_sha(id: &str, version: &str) -> String {
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
version.hash(&mut hasher);
let h1 = hasher.finish();
h1.hash(&mut hasher);
let h2 = hasher.finish();
h2.hash(&mut hasher);
let h3 = hasher.finish();
let full = format!("{h1:016x}{h2:016x}{h3:016x}");
full[..40].to_owned()
}
fn date(&self) -> CommitDate {
CommitDate::from(if self.empty_dates { "" } else { DEFAULT_DATE })
}
fn error_for(&self, id: &ActionId) -> Option<ResolutionError> {
self.error
.clone()
.or_else(|| self.action_errors.get(id.as_str()).cloned())
}
}
impl VersionRegistry for FakeRegistry {
fn lookup_sha(&self, id: &ActionId, version: &Version) -> Result<Commit, ResolutionError> {
if let Some(result) = self.lookup_result.clone() {
return result;
}
if let Some(error) = self.error_for(id) {
return Err(error);
}
let sha = self
.fixed_sha
.clone()
.unwrap_or_else(|| Self::fake_sha(id.as_str(), version.as_str()));
Ok(Commit {
sha: CommitSha::from(sha),
repository: id.base_repo(),
ref_type: Some(RefType::Tag),
date: self.date(),
})
}
fn all_tags(&self, id: &ActionId) -> Result<Vec<Version>, ResolutionError> {
if let Some(error) = self.error_for(id) {
return Err(error);
}
Ok(self
.tags
.get(id.as_str())
.cloned()
.unwrap_or_default()
.into_iter()
.map(Version::from)
.collect())
}
fn describe_sha(
&self,
id: &ActionId,
sha: &CommitSha,
) -> Result<ShaDescription, ResolutionError> {
if let Some(error) = self.describe_error.clone() {
return Err(error);
}
if let Some(error) = self.error_for(id) {
return Err(error);
}
let key = (id.as_str().to_owned(), sha.as_str().to_owned());
let tags = self
.sha_tags
.get(&key)
.cloned()
.unwrap_or_default()
.into_iter()
.map(Version::from)
.collect();
Ok(ShaDescription {
tags,
repository: id.base_repo(),
date: self.date(),
})
}
}