use crate::builders::{CrateBuilder, VersionBuilder};
use crate::util::{RequestHelper, TestApp};
use insta::{assert_json_snapshot, assert_snapshot};

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let c1 = CrateBuilder::new("c1", user.id)
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c2", user.id)
        .version(VersionBuilder::new("1.0.0").dependency(&c1, None))
        .version(
            VersionBuilder::new("1.1.0")
                .dependency(&c1, None)
                .dependency(&c1, Some("foo")),
        )
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });

    // c1 has no dependent crates.
    let response = anon
        .get::<()>("/api/v1/crates/c2/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies_when_old_version_doesnt_depend_but_new_does() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let c1 = CrateBuilder::new("c1", user.id)
        .version("1.1.0")
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c2", user.id)
        .version("1.0.0")
        .version(VersionBuilder::new("2.0.0").dependency(&c1, None))
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies_when_old_version_depended_but_new_doesnt() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let c1 = CrateBuilder::new("c1", user.id)
        .version("1.0.0")
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c2", user.id)
        .version(VersionBuilder::new("1.0.0").dependency(&c1, None))
        .version("2.0.0")
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn prerelease_versions_not_included_in_reverse_dependencies() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let c1 = CrateBuilder::new("c1", user.id)
        .version("1.0.0")
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c2", user.id)
        .version("1.1.0-pre")
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c3", user.id)
        .version(VersionBuilder::new("1.0.0").dependency(&c1, None))
        .version("1.1.0-pre")
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn yanked_versions_not_included_in_reverse_dependencies() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let c1 = CrateBuilder::new("c1", user.id)
        .version("1.0.0")
        .expect_build(&mut conn)
        .await;

    CrateBuilder::new("c2", user.id)
        .version("1.0.0")
        .version(VersionBuilder::new("2.0.0").dependency(&c1, None))
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });

    use crates_io::schema::versions;
    use diesel::{ExpressionMethods, QueryDsl};
    use diesel_async::RunQueryDsl;

    diesel::update(versions::table.filter(versions::num.eq("2.0.0")))
        .set(versions::yanked.eq(true))
        .execute(&mut conn)
        .await
        .unwrap();

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies_includes_published_by_user_when_present() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    use crates_io::schema::versions;
    use diesel::{ExpressionMethods, update};
    use diesel_async::RunQueryDsl;

    let c1 = CrateBuilder::new("c1", user.id)
        .version("1.0.0")
        .expect_build(&mut conn)
        .await;
    CrateBuilder::new("c2", user.id)
        .downloads(100)
        .version(VersionBuilder::new("2.0.0").dependency(&c1, None))
        .expect_build(&mut conn)
        .await;

    // Make c2's version (and,incidentally, c1's, but that doesn't matter) mimic a version
    // published before we started recording who published versions
    let none: Option<i32> = None;
    update(versions::table)
        .set(versions::published_by.eq(none))
        .execute(&mut conn)
        .await
        .unwrap();

    // c3's version will have the published by info recorded
    CrateBuilder::new("c3", user.id)
        .downloads(10)
        .version(VersionBuilder::new("3.0.0").dependency(&c1, None))
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies_query_supports_u64_version_number_parts() {
    let (app, anon, user) = TestApp::init().with_user().await;
    let mut conn = app.db_conn().await;
    let user = user.as_model();

    let large_but_valid_version_number = format!("1.0.{}", u64::MAX);

    let c1 = CrateBuilder::new("c1", user.id)
        .expect_build(&mut conn)
        .await;

    // The crate that depends on c1...
    CrateBuilder::new("c2", user.id)
        // ...has a patch version at the limits of what the semver crate supports
        .version(VersionBuilder::new(&large_but_valid_version_number).dependency(&c1, None))
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get::<()>("/api/v1/crates/c1/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"200 OK");
    assert_json_snapshot!(response.json(), {
        ".versions[].created_at" => "[datetime]",
        ".versions[].updated_at" => "[datetime]",
        ".versions[].published_by.created_at" => "[datetime]",
    });
}

#[tokio::test(flavor = "multi_thread")]
async fn reverse_dependencies_blocks_high_page_numbers() {
    let (app, anon, user) = TestApp::init()
        .with_config(|config| {
            config.max_allowed_page_offset = 1;
        })
        .with_user()
        .await;

    let mut conn = app.db_conn().await;
    let user = user.as_model();

    CrateBuilder::new("c1", user.id)
        .expect_build(&mut conn)
        .await;

    let response = anon
        .get_with_query::<()>(
            "/api/v1/crates/c1/reverse_dependencies",
            "page=2&per_page=1",
        )
        .await;
    assert_snapshot!(response.status(), @"400 Bad Request");
    assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"Page 2 is unavailable for performance reasons. Please take a look at https://crates.io/data-access for alternatives."}]}"#);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_unknown_crate() {
    let (_, anon) = TestApp::init().empty().await;

    let response = anon
        .get::<()>("/api/v1/crates/unknown/reverse_dependencies")
        .await;
    assert_snapshot!(response.status(), @"404 Not Found");
    assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"crate `unknown` does not exist"}]}"#);
}
