direct_play_nice 0.1.0-beta.2

CLI program that converts video files to direct-play-compatible formats.
Documentation
//! Tests that verify Sonarr/Radarr environment parsing and output-path resolution behavior.

#[cfg(test)]
mod tests {
    use crate::servarr::*;
    use std::ffi::CString;
    use std::sync::Mutex;
    use tempfile::tempdir;

    static ENV_MUTEX: Mutex<()> = Mutex::new(());

    #[test]
    fn append_suffix_preserves_extension() {
        let base = PathBuf::from("Episode.mkv");
        let suffixed = append_suffix(&base, ".tmp");
        assert_eq!(suffixed, PathBuf::from("Episode.tmp.mkv"));
    }

    #[test]
    fn resolve_output_path_match_input() {
        let base = PathBuf::from("Movie.mkv");
        let resolved = resolve_output_path(&base, "match-input", "").unwrap();
        assert_eq!(resolved, base);
    }

    #[test]
    fn resolve_output_path_custom_extension() {
        let base = PathBuf::from("Movie.mkv");
        let resolved = resolve_output_path(&base, "mp4", "").unwrap();
        assert_eq!(resolved, PathBuf::from("Movie.mp4"));
    }

    #[test]
    fn resolve_output_path_with_suffix_and_extension() {
        let base = PathBuf::from("Movie.mkv");
        let resolved = resolve_output_path(&base, "mp4", ".fixed").unwrap();
        assert_eq!(resolved, PathBuf::from("Movie.fixed.mp4"));
    }

    #[test]
    fn resolve_output_path_with_suffix_and_match_input() {
        let base = PathBuf::from("Episode.mkv");
        let resolved = resolve_output_path(&base, "match-input", "fixed").unwrap();
        assert_eq!(resolved, PathBuf::from("Episode.fixed.mkv"));
    }

    #[test]
    fn resolve_media_path_uses_sonarr_fallback() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("sonarr_episodefile_path");
        env::remove_var("sonarr_episodefile_paths");
        env::set_var("sonarr_series_path", "/tmp/show");
        env::set_var("sonarr_episodefile_relativepath", "Season 1/episode.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Sonarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(
            resolved,
            Path::new("/tmp/show").join("Season 1/episode.mkv")
        );

        env::remove_var("sonarr_episodefile_relativepath");
        env::remove_var("sonarr_series_path");
    }

    #[test]
    fn resolve_media_path_uses_sonarr_source_only() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("sonarr_episodefile_path");
        env::remove_var("sonarr_episodefile_relativepath");
        env::remove_var("sonarr_series_path");
        env::set_var("sonarr_episodefile_sourcepath", "/tmp/source/episode.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Sonarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(resolved, PathBuf::from("/tmp/source/episode.mkv"));

        env::remove_var("sonarr_episodefile_sourcepath");
    }

    #[test]
    fn resolve_media_path_uses_sonarr_episodefile_paths() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("sonarr_episodefile_path");
        env::set_var("sonarr_episodefile_paths", "/tmp/show/Season 1/episode.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Sonarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(resolved, PathBuf::from("/tmp/show/Season 1/episode.mkv"));

        env::remove_var("sonarr_episodefile_paths");
    }

    #[test]
    fn resolve_media_paths_handles_multiple_sonarr_entries() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("sonarr_episodefile_path");
        env::set_var(
            "sonarr_episodefile_paths",
            "/tmp/show/Season 1/episode1.mkv|/tmp/show/Season 1/episode2.mkv",
        );

        let resolved = resolve_media_paths(IntegrationKind::Sonarr).unwrap();
        assert_eq!(resolved.len(), 2);
        assert_eq!(
            resolved[0],
            PathBuf::from("/tmp/show/Season 1/episode1.mkv")
        );
        assert_eq!(
            resolved[1],
            PathBuf::from("/tmp/show/Season 1/episode2.mkv")
        );

        env::remove_var("sonarr_episodefile_paths");
    }

    #[test]
    fn resolve_media_path_uses_radarr_fallback() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("radarr_moviefile_path");
        env::remove_var("radarr_moviefile_paths");
        env::set_var("radarr_movie_path", "/tmp/movie");
        env::set_var("radarr_moviefile_relativepath", "movie.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Radarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(resolved, Path::new("/tmp/movie").join("movie.mkv"));

        env::remove_var("radarr_moviefile_relativepath");
        env::remove_var("radarr_movie_path");
    }

    #[test]
    fn resolve_media_path_uses_radarr_source_only() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("radarr_moviefile_path");
        env::remove_var("radarr_moviefile_relativepath");
        env::remove_var("radarr_movie_path");
        env::set_var("radarr_moviefile_sourcepath", "/tmp/source/movie.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Radarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(resolved, PathBuf::from("/tmp/source/movie.mkv"));

        env::remove_var("radarr_moviefile_sourcepath");
    }

    #[test]
    fn resolve_media_path_uses_radarr_moviefile_paths() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("radarr_moviefile_path");
        env::set_var("radarr_moviefile_paths", "/tmp/movie/movie.mkv");

        let resolved = resolve_media_paths(IntegrationKind::Radarr)
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        assert_eq!(resolved, PathBuf::from("/tmp/movie/movie.mkv"));

        env::remove_var("radarr_moviefile_paths");
    }

    #[test]
    fn resolve_media_paths_handles_multiple_radarr_entries() {
        let _guard = ENV_MUTEX.lock().unwrap();
        env::remove_var("radarr_moviefile_path");
        env::set_var(
            "radarr_moviefile_paths",
            "/tmp/movie/part1.mkv|/tmp/movie/part2.mkv",
        );

        let resolved = resolve_media_paths(IntegrationKind::Radarr).unwrap();
        assert_eq!(resolved.len(), 2);
        assert_eq!(resolved[0], PathBuf::from("/tmp/movie/part1.mkv"));
        assert_eq!(resolved[1], PathBuf::from("/tmp/movie/part2.mkv"));

        env::remove_var("radarr_moviefile_paths");
    }

    #[test]
    fn sonarr_default_suffix_is_fixed() {
        let _guard = ENV_MUTEX.lock().unwrap();
        let tmp = tempdir().unwrap();
        let input = tmp.path().join("Episode.mkv");
        std::fs::write(&input, b"dummy").unwrap();

        env::set_var("sonarr_eventtype", "Download");
        env::set_var("sonarr_episodefile_path", &input);

        let view = ArgsView {
            has_input: false,
            has_output: false,
            desired_extension: "mp4",
            desired_suffix: "",
        };

        let prep = prepare_from_env(view).unwrap();
        let IntegrationPreparation::Replace(plan) = prep else {
            panic!("expected replace plan");
        };
        let file_name = plan
            .final_output_path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap();
        assert_eq!(file_name, "Episode.fixed.mp4");

        env::remove_var("sonarr_eventtype");
        env::remove_var("sonarr_episodefile_path");
    }

    #[test]
    fn radarr_default_suffix_is_empty() {
        let _guard = ENV_MUTEX.lock().unwrap();
        let tmp = tempdir().unwrap();
        let input = tmp.path().join("Movie.mkv");
        std::fs::write(&input, b"dummy").unwrap();

        env::set_var("radarr_eventtype", "Download");
        env::set_var("radarr_moviefile_path", &input);

        let view = ArgsView {
            has_input: false,
            has_output: false,
            desired_extension: "mp4",
            desired_suffix: "",
        };

        let prep = prepare_from_env(view).unwrap();
        let IntegrationPreparation::Replace(plan) = prep else {
            panic!("expected replace plan");
        };
        let file_name = plan
            .final_output_path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap();
        assert_eq!(file_name, "Movie.mp4");

        env::remove_var("radarr_eventtype");
        env::remove_var("radarr_moviefile_path");
    }

    #[test]
    fn finalize_success_restores_original_when_promote_fails() {
        let tmp = tempdir().unwrap();
        let input = tmp.path().join("Episode.mkv");
        let backup = tmp.path().join("Episode.backup.mkv");
        let temp_output = tmp.path().join("Episode.tmp.mp4");
        let final_output = tmp.path().join("missing").join("Episode.mp4");

        std::fs::write(&input, b"original").unwrap();
        std::fs::write(&temp_output, b"converted").unwrap();

        let plan = ReplacePlan {
            kind: IntegrationKind::Sonarr,
            event_type: "Download".to_string(),
            display_name: None,
            is_upgrade: None,
            input_path: input.clone(),
            final_output_path: final_output,
            temp_output_path: temp_output.clone(),
            backup_path: backup.clone(),
            input_cstring: CString::new(input.to_string_lossy().to_string()).unwrap(),
            temp_output_cstring: CString::new(temp_output.to_string_lossy().to_string()).unwrap(),
        };

        let err = plan
            .finalize_success()
            .expect_err("promotion should fail when target parent directory is missing");
        assert!(err.to_string().contains("could not promote"));
        assert!(input.exists(), "original input should be restored");
        assert!(!backup.exists(), "backup should not remain after restore");
    }
}