use std::sync::Arc;

use uuid::Uuid;

use domain::{
    errors::DomainError,
    models::Movie,
    ports::{MetadataClient, MovieCommand, MovieQuery},
    testing::{
        FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository,
        NoopEventPublisher, NoopObjectStorage,
    },
    value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear},
};

use crate::{
    diary::commands::SyncPosterCommand,
    movies::{deps::SyncPosterDeps, sync_poster},
};

fn default_deps() -> SyncPosterDeps {
    SyncPosterDeps {
        movie_command: InMemoryMovieRepository::new(),
        movie_query: InMemoryMovieRepository::new(),
        movie_profile: InMemoryMovieProfileRepository::new(),
        metadata: Arc::new(domain::testing::FakeMetadataClient),
        poster_fetcher: Arc::new(domain::testing::FakePosterFetcher),
        object_storage: Arc::new(NoopObjectStorage),
        event_publisher: NoopEventPublisher::new(),
        search_command: Arc::new(FakeSearchCommand),
    }
}

#[tokio::test]
async fn fails_when_movie_not_found() {
    let deps = default_deps();

    let result = sync_poster::execute(
        &deps,
        SyncPosterCommand {
            movie_id: Uuid::new_v4(),
        },
    )
    .await;

    assert!(result.is_err());
}

#[tokio::test]
async fn fails_when_no_external_id() {
    let movies = InMemoryMovieRepository::new();
    let movie = Movie::new(
        None,
        MovieTitle::new("Test".into()).unwrap(),
        ReleaseYear::new(2024).unwrap(),
        None,
        None,
    );
    let movie_id = movie.id().value();
    movies.upsert_movie(&movie).await.unwrap();

    let deps = SyncPosterDeps {
        movie_command: Arc::clone(&movies) as _,
        movie_query: Arc::clone(&movies) as _,
        ..default_deps()
    };

    let result = sync_poster::execute(&deps, SyncPosterCommand { movie_id }).await;

    assert!(result.is_err());
}

struct FakeMetaWithPoster;

#[async_trait::async_trait]
impl MetadataClient for FakeMetaWithPoster {
    async fn fetch_movie_metadata(
        &self,
        _: &domain::models::MetadataSearchCriteria,
    ) -> Result<Movie, DomainError> {
        unimplemented!()
    }
    async fn get_poster_url(
        &self,
        _: &ExternalMetadataId,
    ) -> Result<Option<PosterUrl>, DomainError> {
        Ok(Some(
            PosterUrl::new("https://example.com/poster.jpg".into()).unwrap(),
        ))
    }
}

#[tokio::test]
async fn syncs_poster_for_movie_with_external_id() {
    let movies = InMemoryMovieRepository::new();
    let ext_id = ExternalMetadataId::new("tmdb:999".into()).unwrap();
    let movie = Movie::new(
        Some(ext_id),
        MovieTitle::new("Poster Movie".into()).unwrap(),
        ReleaseYear::new(2024).unwrap(),
        None,
        None,
    );
    let movie_id = movie.id().value();
    movies.upsert_movie(&movie).await.unwrap();

    let deps = SyncPosterDeps {
        movie_command: Arc::clone(&movies) as _,
        movie_query: Arc::clone(&movies) as _,
        metadata: Arc::new(FakeMetaWithPoster) as _,
        ..default_deps()
    };

    sync_poster::execute(&deps, SyncPosterCommand { movie_id })
        .await
        .unwrap();

    let updated = movies
        .get_movie_by_id(&domain::value_objects::MovieId::from_uuid(movie_id))
        .await
        .unwrap()
        .unwrap();
    assert!(updated.poster_path().is_some());
}
