use crate::testing_prelude::*;
use di::{Ref as DiRef, singleton_as_self};

/// Test that `UploadCommand` succeeds with a valid transcoded source.
#[tokio::test]

async fn upload_command_succeeds_with_valid_source() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let host = build_upload_test_host(&transcode, &test_dir).await;
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    assert!(status.formats.is_some(), "should have format statuses");
    let formats = status.formats.as_ref().expect("checked above");
    assert!(!formats.is_empty(), "should have at least one format");

    Ok(())
}

/// Test that `UploadCommand` with `dry_run=true` does not call the API.
#[tokio::test]
async fn upload_command_dry_run_skips_api_call() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let host = HostBuilder::new()
        .with_mock_api(transcode.album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        })
        .with_options(UploadOptions {
            dry_run: true,
            ..UploadOptions::default()
        })
        .expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "dry run should succeed");
    assert!(
        status.formats.unwrap_or_default().is_empty(),
        "dry run should not record formats"
    );
    assert!(status.errors.is_none(), "dry run should have no errors");

    Ok(())
}

/// Test that torrent client injection succeeds with mock client.
#[tokio::test]
async fn upload_command_torrent_client_injection_succeeds() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let mut builder = HostBuilder::new();
    let _ = builder
        .with_mock_api(transcode.album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        })
        .with_options(QbitOptions::mock())
        .with_options(QbitUploadOptions::mock())
        .with_mock_torrent_client(MockQBittorrentClient::default());
    let host = builder.expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    assert!(
        status.errors.is_none(),
        "injection should succeed with mock client"
    );

    Ok(())
}

/// Test that torrent injection is skipped when `qbit_url` is not set.
#[tokio::test]
async fn upload_command_skips_injection_when_not_configured() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let host = build_upload_test_host(&transcode, &test_dir).await;
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    assert!(
        status.errors.is_none(),
        "no warnings when injection not configured"
    );

    Ok(())
}

/// Test that `UploadCommand` fails when torrent file is missing (no transcodes).
#[tokio::test]
async fn upload_command_missing_transcode_returns_error() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let test_dir = TestDirectory::new();

    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(UploadOptions {
            copy_torrent_to: Some(PathBuf::from("/nonexistent/path")),
            ..UploadOptions::default()
        })
        .with_options(SourceArg {
            source: AlbumConfig::TORRENT_ID.to_string(),
        })
        .expect_build();

    let command = host.services.get_required::<UploadCommand>();

    // Act
    let result = command.execute_cli().await;

    // Assert - now returns Err since missing torrent is a failure
    assert!(result.is_err());

    Ok(())
}

/// Test that `UploadCommand` fails when the torrent file is missing.
#[tokio::test]

async fn upload_command_missing_torrent_fails() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let test_dir = TestDirectory::new();

    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .expect_build();

    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");

    // Delete torrent files
    let output_dir = test_dir.output();
    delete_torrent_files(&output_dir);

    let command = host.services.get_required::<UploadCommand>();

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(!status.success, "upload should fail with missing torrent");
    assert!(status.errors.is_some(), "should have error messages");

    Ok(())
}

fn delete_torrent_files(dir: &PathBuf) {
    if let Ok(entries) = read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|ext| ext == "torrent") {
                let _ = remove_file(&path);
            }
            if path.is_dir() {
                delete_torrent_files(&path);
            }
        }
    }
}

/// Test that `UploadCommand` copies transcode to content directory when enabled.
#[tokio::test]

async fn upload_command_copies_to_content_dir() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let copy_target = TempDirectory::create("content_copy_target");
    let host = HostBuilder::new()
        .with_mock_api(transcode.album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            // First content dir is for copy destination, second is where source files are found
            content: vec![copy_target.to_path_buf(), SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            allow_existing: true, // Allow existing since we're using pre-generated transcodes
            ..TargetOptions::default()
        })
        .with_options(UploadOptions {
            copy_transcode_to_content_dir: true,
            ..UploadOptions::default()
        })
        .expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    let entries: Vec<_> = read_dir(&copy_target).expect("read dir").collect();
    assert!(!entries.is_empty(), "should have copied transcodes");

    Ok(())
}

/// Test that `UploadCommand` copies transcode to custom directory.
#[tokio::test]

async fn upload_command_copies_to_custom_dir() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let copy_target = TempDirectory::create("custom_copy_target");
    let host = HostBuilder::new()
        .with_mock_api(transcode.album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        })
        .with_options(UploadOptions {
            copy_transcode_to: Some(copy_target.to_path_buf()),
            ..UploadOptions::default()
        })
        .expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    let entries: Vec<_> = read_dir(&copy_target).expect("read dir").collect();
    assert!(!entries.is_empty(), "should have copied transcodes");

    Ok(())
}

/// Test that `UploadCommand` copies torrent file to specified directory.
#[tokio::test]

async fn upload_command_copies_torrent_file() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let torrent_target = TempDirectory::create("torrent_copy_target");
    let host = HostBuilder::new()
        .with_mock_api(transcode.album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        })
        .with_options(UploadOptions {
            copy_torrent_to: Some(torrent_target.to_path_buf()),
            ..UploadOptions::default()
        })
        .expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    let torrent_files: Vec<_> = read_dir(&*torrent_target)
        .expect("read dir")
        .filter_map(Result::ok)
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "torrent"))
        .collect();
    assert!(!torrent_files.is_empty(), "should have copied torrents");

    Ok(())
}

/// Test that `UploadCommand` handles API failure gracefully.
#[tokio::test]

async fn upload_command_api_failure_sets_error() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();

    // Create a mock client that fails on upload
    let torrent_path = SAMPLE_SOURCES_DIR.join(transcode.album.torrent_filename());
    let torrent_bytes = read(torrent_path).expect("torrent file should exist");

    let mock = MockGazelleClient::new()
        .with_get_torrent(Ok(TorrentResponse {
            group: Group {
                id: 123,
                name: transcode.album.album.to_owned(),
                year: transcode.album.year,
                category_name: "Music".to_owned(),
                music_info: Some(Credits {
                    artists: vec![Credit {
                        id: 1,
                        name: transcode.album.artist.to_owned(),
                    }],
                    ..Credits::default()
                }),
                ..Group::default()
            },
            torrent: Torrent {
                id: AlbumConfig::TORRENT_ID,
                format: Format::FLAC,
                encoding: Quality::Lossless,
                media: Media::WEB,
                file_path: transcode.album.dir_name(),
                ..Torrent::default()
            },
        }))
        .with_get_torrent_group(Ok(GroupResponse {
            group: Group {
                id: 123,
                name: transcode.album.album.to_owned(),
                year: transcode.album.year,
                category_name: "Music".to_owned(),
                ..Group::default()
            },
            torrents: vec![Torrent {
                id: AlbumConfig::TORRENT_ID,
                format: Format::FLAC,
                encoding: Quality::Lossless,
                media: Media::WEB,
                file_path: transcode.album.dir_name(),
                ..Torrent::default()
            }],
        }))
        .with_download_torrent(Ok(torrent_bytes))
        .with_upload_torrent(Err(GazelleError {
            operation: GazelleOperation::ReadFile,
            source: ErrorSource::Io(IoError::new(ErrorKind::NotFound, "Upload failed")),
        }));

    let mut builder = HostBuilder::new();

    #[allow(clippy::as_conversions)]
    let client: DiRef<GazelleClient> = DiRef::new(Box::new(mock) as GazelleClient);
    builder
        .services
        .add(singleton_as_self().from(move |_| client.clone()));

    let _ = builder
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        });
    let host = builder.expect_build();
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(!status.success, "upload should fail on API error");
    assert!(status.errors.is_some(), "should have errors");

    Ok(())
}

/// Test that `UploadCommand` captures torrent ID from API response.
#[tokio::test]

async fn upload_command_captures_response_ids() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let host = build_upload_test_host(&transcode, &test_dir).await;
    let (source, command) = get_source_and_command(&host).await;

    // Act
    let result = command.execute(&source).await;
    let status = UploadStatus::new(result);

    // Assert
    assert!(status.success, "upload should succeed");
    assert!(status.formats.is_some(), "should have format statuses");
    for format_status in status.formats.as_ref().expect("checked") {
        assert!(format_status.id > 0, "should have valid ID");
    }

    Ok(())
}

/// Test that upload succeeds when copy target already exists.
#[tokio::test]

async fn upload_command_skip_existing_copy_succeeds() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
    let test_dir = TestDirectory::new();
    let host = build_upload_test_host(&transcode, &test_dir).await;
    let (source, command) = get_source_and_command(&host).await;

    // Act - run twice
    let result1 = command.execute(&source).await;
    let status1 = UploadStatus::new(result1);
    assert!(status1.success, "first upload should succeed");

    let result2 = command.execute(&source).await;
    let status2 = UploadStatus::new(result2);

    // Assert
    assert!(status2.success, "second upload should succeed");

    Ok(())
}

/// Helper to build a host configured for upload tests with pre-generated transcodes.
async fn build_upload_test_host(transcode: &TranscodeConfig, test_dir: &TestDirectory) -> Host {
    HostBuilder::new()
        .with_mock_api(transcode.album.clone())
        .with_test_options(test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: SAMPLE_TRANSCODES_DIR.clone(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![transcode.target],
            ..TargetOptions::default()
        })
        .expect_build()
}

/// Helper to get source and upload command from a host.
async fn get_source_and_command(host: &Host) -> (Source, DiRef<UploadCommand>) {
    let provider = host.services.get_required::<SourceProvider>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");
    let command = host.services.get_required::<UploadCommand>();
    (source, command)
}
