use std::collections::HashMap; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use chrono::Utc; use uuid::Uuid; use crate::{ errors::DomainError, models::{ AnnotatedRow, DiaryEntry, DiaryFilter, ExternalPersonId, FeedEntry, FieldMapping, FileFormat, ImportError, ImportRow, Movie, MovieProfile, MovieStats, ParsedFile, Person, PersonCredits, PersonId, Review, ReviewHistory, RowResult, SearchQuery, SearchResults, UserStats, UserTrends, collections::{PageParams, Paginated}, }, ports::{ AuthService, DiaryRepository, DocumentParser, FeedSortBy, FollowingFilter, GeneratedToken, MetadataClient, MetadataSearchCriteria, MovieEnrichmentClient, PasswordHasher, PersonQuery, PosterFetcherClient, SearchCommand, SearchPort, StatsRepository, }, value_objects::{ExternalMetadataId, MovieId, PasswordHash, PosterUrl, UserId}, }; // ── FakeAuthService ─────────────────────────────────────────────────────────── pub struct FakeAuthService; #[async_trait] impl AuthService for FakeAuthService { async fn generate_token(&self, user_id: &UserId) -> Result { Ok(GeneratedToken { token: user_id.value().to_string(), expires_at: Utc::now() + chrono::Duration::hours(24), }) } async fn validate_token(&self, token: &str) -> Result { Uuid::parse_str(token) .map(UserId::from_uuid) .map_err(|_| DomainError::Unauthorized("invalid token".into())) } } // ── FakePasswordHasher ──────────────────────────────────────────────────────── pub struct FakePasswordHasher; #[async_trait] impl PasswordHasher for FakePasswordHasher { async fn hash(&self, plain_password: &str) -> Result { PasswordHash::new(format!("hashed:{plain_password}")) } async fn verify(&self, plain_password: &str, hash: &PasswordHash) -> Result { Ok(hash.value() == format!("hashed:{plain_password}")) } } // ── FakeMetadataClient ──────────────────────────────────────────────────────── pub struct FakeMetadataClient; #[async_trait] impl MetadataClient for FakeMetadataClient { async fn fetch_movie_metadata( &self, _criteria: &MetadataSearchCriteria, ) -> Result { Err(DomainError::InfrastructureError( "fake metadata client".into(), )) } async fn get_poster_url( &self, _external_metadata_id: &ExternalMetadataId, ) -> Result, DomainError> { Ok(None) } } // ── FakeDiaryRepository ─────────────────────────────────────────────────────── pub struct FakeDiaryRepository { histories: Mutex)>>, } impl FakeDiaryRepository { pub fn new() -> Arc { Arc::new(Self { histories: Mutex::new(HashMap::new()), }) } pub fn seed_history(&self, movie: Movie, reviews: Vec) { self.histories .lock() .unwrap() .insert(movie.id().value(), (movie, reviews)); } } #[async_trait] impl DiaryRepository for FakeDiaryRepository { async fn query_diary( &self, _filter: &DiaryFilter, ) -> Result, DomainError> { Ok(Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }) } async fn query_activity_feed( &self, _page: &PageParams, ) -> Result, DomainError> { Ok(Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }) } async fn query_activity_feed_filtered( &self, _page: &PageParams, _sort_by: &FeedSortBy, _search: Option<&str>, _following: Option<&FollowingFilter>, ) -> Result, DomainError> { Ok(Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }) } async fn get_review_history(&self, movie_id: &MovieId) -> Result { let histories = self.histories.lock().unwrap(); let (movie, reviews) = histories .get(&movie_id.value()) .ok_or_else(|| DomainError::NotFound(format!("movie {}", movie_id.value())))?; Ok(ReviewHistory::new(movie.clone(), reviews.clone())) } async fn get_user_history(&self, _user_id: &UserId) -> Result, DomainError> { Ok(vec![]) } async fn get_movie_stats(&self, _movie_id: &MovieId) -> Result { Ok(MovieStats { total_count: 0, avg_rating: None, federated_count: 0, rating_histogram: [0; 5], }) } async fn get_movie_social_feed( &self, _movie_id: &MovieId, _page: &PageParams, ) -> Result, DomainError> { Ok(Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }) } async fn count_local_posts(&self) -> Result { Ok(0) } } // ── FakeStatsRepository ───────────────────────────────────────────────────── pub struct FakeStatsRepository; #[async_trait] impl StatsRepository for FakeStatsRepository { async fn get_user_stats(&self, _: &UserId) -> Result { Ok(UserStats { total_movies: 0, avg_rating: None, favorite_director: None, most_active_month: None, }) } async fn get_user_trends(&self, _: &UserId) -> Result { Ok(UserTrends { monthly_ratings: vec![], top_directors: vec![], max_director_count: 0, }) } } // ── FakePersonQuery ───────────────────────────────────────────────────────── pub struct FakePersonQuery; #[async_trait] impl PersonQuery for FakePersonQuery { async fn get_by_id(&self, _: &PersonId) -> Result, DomainError> { Ok(None) } async fn get_by_external_id( &self, _: &ExternalPersonId, ) -> Result, DomainError> { Ok(None) } async fn get_credits(&self, id: &PersonId) -> Result { let dummy = Person::basic( id.clone(), ExternalPersonId::new("tmdb:0"), "Unknown".into(), None, None, ); Ok(PersonCredits { person: dummy, cast: vec![], crew: vec![], }) } async fn list_orphaned_persons(&self) -> Result, DomainError> { Ok(vec![]) } async fn list_page(&self, _: u32, _: u32) -> Result, DomainError> { Ok(vec![]) } } // ── FakeSearchPort ────────────────────────────────────────────────────────── pub struct FakeSearchPort; #[async_trait] impl SearchPort for FakeSearchPort { async fn search(&self, _: &SearchQuery) -> Result { Ok(SearchResults { movies: Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }, people: Paginated { items: vec![], total_count: 0, limit: 10, offset: 0, }, }) } } // ── FakeSearchCommand ─────────────────────────────────────────────────────── pub struct FakeSearchCommand; #[async_trait] impl SearchCommand for FakeSearchCommand { async fn index(&self, _: crate::models::IndexableDocument) -> Result<(), DomainError> { Ok(()) } async fn remove(&self, _: crate::models::EntityType, _: &str) -> Result<(), DomainError> { Ok(()) } } // ── FakeDocumentParser ────────────────────────────────────────────────────── pub struct FakeDocumentParser; impl DocumentParser for FakeDocumentParser { fn parse(&self, _: &[u8], _: FileFormat) -> Result { Ok(ParsedFile { columns: vec!["title".into()], rows: vec![vec!["Test Movie".into()]], }) } fn apply_mapping(&self, _: &ParsedFile, _: &[FieldMapping]) -> Vec { vec![AnnotatedRow { result: RowResult::Valid(ImportRow { title: Some("Test Movie".into()), ..ImportRow::default() }), is_duplicate: false, }] } } // ── FakePosterFetcher ─────────────────────────────────────────────────────── pub struct FakePosterFetcher; #[async_trait] impl PosterFetcherClient for FakePosterFetcher { async fn fetch_poster_bytes(&self, _: &PosterUrl) -> Result, DomainError> { Ok(vec![1, 2, 3]) } } // ── FakeMovieEnrichmentClient ─────────────────────────────────────────────── pub struct FakeMovieEnrichmentClient; #[async_trait] impl MovieEnrichmentClient for FakeMovieEnrichmentClient { async fn fetch_profile( &self, movie_id: MovieId, _external_metadata_id: &str, ) -> Result { Ok(MovieProfile { movie_id, tmdb_id: 0, imdb_id: None, overview: None, tagline: None, runtime_minutes: None, budget_usd: None, revenue_usd: None, vote_average: None, vote_count: None, original_language: None, collection_name: None, genres: vec![], keywords: vec![], cast: vec![], crew: vec![], enriched_at: Utc::now(), }) } }