//! Integration tests for the s3bolt copy engine using MockS3.
//!
//! No AWS credentials or network access required.

use std::sync::Arc;

use s3bolt::config::{ConcurrencyConfig, CopyConfig, FilterConfig};
use s3bolt::engine::orchestrator;
use s3bolt::progress::reporter::ProgressState;
use s3bolt::s3::mock::MockS3;
use s3bolt::s3::ops::S3Operations;
use s3bolt::types::S3Uri;

fn make_config(source: &str, dest: &str) -> CopyConfig {
    CopyConfig {
        source: S3Uri::parse(source).unwrap(),
        destination: S3Uri::parse(dest).unwrap(),
        recursive: true,
        sync_mode: false,
        dry_run: false,
        verify: false,
        filters: FilterConfig::default(),
        concurrency: ConcurrencyConfig {
            max_concurrent: 16,
            adaptive: false,
            min_concurrent: 4,
            listing_buffer_size: 100,
        },
        checkpoint_path: None,
        resume: false,
        storage_class: None,
        sse: None,
        preserve_metadata: false,
        source_profile: None,
        dest_profile: None,
    }
}

#[tokio::test]
async fn copy_single_object() {
    let mock = MockS3::new();
    mock.put_object("src-bucket", "data/file.parquet", 1024);

    let config = make_config("s3://src-bucket/data/", "s3://dst-bucket/data/");
    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.total_objects, 1);
    assert_eq!(manifest.copied_objects, 1);
    assert_eq!(manifest.failed_objects, 0);
    assert_eq!(manifest.skipped_objects, 0);
    assert!(mock.exists("dst-bucket", "data/file.parquet"));
}

#[tokio::test]
async fn copy_multiple_objects() {
    let mock = MockS3::new();
    mock.put_objects("src", "prefix/", 50, 2048);

    let config = make_config("s3://src/prefix/", "s3://dst/prefix/");
    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.total_objects, 50);
    assert_eq!(manifest.copied_objects, 50);
    assert_eq!(manifest.failed_objects, 0);
    assert_eq!(mock.count_objects("dst", "prefix/"), 50);
    assert_eq!(mock.copies().len(), 50);
}

#[tokio::test]
async fn dry_run_copies_nothing() {
    let mock = MockS3::new();
    mock.put_objects("src", "data/", 10, 512);

    let mut config = make_config("s3://src/data/", "s3://dst/data/");
    config.dry_run = true;

    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.total_objects, 10);
    assert_eq!(manifest.copied_objects, 0);
    assert_eq!(manifest.skipped_objects, 10);
    assert_eq!(mock.count_objects("dst", "data/"), 0); // nothing copied
    assert!(mock.copies().is_empty());
}

#[tokio::test]
async fn filter_include_glob() {
    let mock = MockS3::new();
    mock.put_object("src", "data/file1.parquet", 100);
    mock.put_object("src", "data/file2.csv", 200);
    mock.put_object("src", "data/file3.parquet", 300);

    let mut config = make_config("s3://src/data/", "s3://dst/data/");
    config.filters.include_globs = vec!["**/*.parquet".to_owned()];

    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.total_objects, 3);
    assert_eq!(manifest.copied_objects, 2); // only .parquet files
    assert_eq!(manifest.skipped_objects, 1); // .csv filtered out
}

#[tokio::test]
async fn filter_exclude_glob() {
    let mock = MockS3::new();
    mock.put_object("src", "data/keep.txt", 100);
    mock.put_object("src", "data/_tmp/skip.txt", 200);

    let mut config = make_config("s3://src/data/", "s3://dst/data/");
    config.filters.exclude_globs = vec!["**/_tmp/**".to_owned()];

    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.copied_objects, 1);
    assert_eq!(manifest.skipped_objects, 1);
    assert!(mock.exists("dst", "data/keep.txt"));
    assert!(!mock.exists("dst", "data/_tmp/skip.txt"));
}

#[tokio::test]
async fn filter_by_size() {
    let mock = MockS3::new();
    mock.put_object("src", "data/small.txt", 50);
    mock.put_object("src", "data/medium.txt", 500);
    mock.put_object("src", "data/large.txt", 5000);

    let mut config = make_config("s3://src/data/", "s3://dst/data/");
    config.filters.min_size = Some(100);
    config.filters.max_size = Some(1000);

    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.copied_objects, 1); // only medium.txt
    assert_eq!(manifest.skipped_objects, 2);
}

#[tokio::test]
async fn checkpoint_resume() {
    let dir = std::env::temp_dir().join("s3bolt_test_resume");
    let ckpt_path = dir.join("test.ckpt");
    let _ = std::fs::create_dir_all(&dir);
    let _ = std::fs::remove_file(&ckpt_path);

    let mock = MockS3::new();
    mock.put_objects("src", "data/", 10, 256);

    // First run: copy all 10 objects (checkpoint records them).
    {
        let mut config = make_config("s3://src/data/", "s3://dst/data/");
        config.checkpoint_path = Some(ckpt_path.clone());

        let progress = Arc::new(ProgressState::default());
        let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

        let manifest = orchestrator::run_with_s3(config, progress, s3)
            .await
            .unwrap();
        assert_eq!(manifest.copied_objects, 10);
    }

    // Second run with resume: all 10 should be skipped.
    {
        let mut config = make_config("s3://src/data/", "s3://dst/data/");
        config.checkpoint_path = Some(ckpt_path.clone());
        config.resume = true;

        let progress = Arc::new(ProgressState::default());
        let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

        let manifest = orchestrator::run_with_s3(config, progress, s3)
            .await
            .unwrap();
        assert_eq!(manifest.copied_objects, 0);
        assert_eq!(manifest.skipped_objects, 10);
    }

    let _ = std::fs::remove_file(&ckpt_path);
}

#[tokio::test]
async fn empty_source_copies_nothing() {
    let mock = MockS3::new();
    // No objects in source.

    let config = make_config("s3://src/data/", "s3://dst/data/");
    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.total_objects, 0);
    assert_eq!(manifest.copied_objects, 0);
    assert_eq!(manifest.failed_objects, 0);
}

#[tokio::test]
async fn progress_state_is_updated() {
    let mock = MockS3::new();
    mock.put_objects("src", "data/", 5, 1000);

    let config = make_config("s3://src/data/", "s3://dst/data/");
    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let _ = orchestrator::run_with_s3(config, progress.clone(), s3)
        .await
        .unwrap();

    use std::sync::atomic::Ordering;
    assert_eq!(progress.objects_discovered.load(Ordering::Relaxed), 5);
    assert_eq!(progress.bytes_discovered.load(Ordering::Relaxed), 5000);
    assert_eq!(progress.objects_completed.load(Ordering::Relaxed), 5);
    assert_eq!(progress.bytes_completed.load(Ordering::Relaxed), 5000);
}

#[tokio::test]
async fn cross_bucket_copy() {
    let mock = MockS3::new();
    mock.put_object("bucket-a", "exports/report.csv", 4096);

    let config = make_config("s3://bucket-a/exports/", "s3://bucket-b/imports/");
    let progress = Arc::new(ProgressState::default());
    let s3: Arc<dyn S3Operations> = Arc::new(mock.clone());

    let manifest = orchestrator::run_with_s3(config, progress, s3)
        .await
        .unwrap();

    assert_eq!(manifest.copied_objects, 1);
    assert!(mock.exists("bucket-b", "imports/report.csv"));
}
