#[macro_use]
extern crate cargotest;
extern crate hamcrest;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use std::collections::HashMap;
use std::fs::{self, File};
use std::io::prelude::*;
use std::str;

use cargotest::cargo_process;
use cargotest::support::git;
use cargotest::support::paths;
use cargotest::support::registry::{Package, cksum};
use cargotest::support::{project, execs, ProjectBuilder};
use hamcrest::assert_that;

fn setup() {
    let root = paths::root();
    t!(fs::create_dir(&root.join(".cargo")));
    t!(t!(File::create(root.join(".cargo/config"))).write_all(br#"
        [source.crates-io]
        replace-with = 'my-awesome-local-registry'

        [source.my-awesome-local-registry]
        directory = 'index'
    "#));
}

struct VendorPackage {
    p: Option<ProjectBuilder>,
    cksum: Checksum,
}

#[derive(Serialize)]
struct Checksum {
    package: Option<String>,
    files: HashMap<String, String>,
}

impl VendorPackage {
    fn new(name: &str) -> VendorPackage {
        VendorPackage {
            p: Some(project(&format!("index/{}", name))),
            cksum: Checksum {
                package: Some(String::new()),
                files: HashMap::new(),
            },
        }
    }

    fn file(&mut self, name: &str, contents: &str) -> &mut VendorPackage {
        self.p = Some(self.p.take().unwrap().file(name, contents));
        self.cksum.files.insert(name.to_string(), cksum(contents.as_bytes()));
        self
    }

    fn disable_checksum(&mut self) -> &mut VendorPackage {
        self.cksum.package = None;
        self
    }

    fn build(&mut self) {
        let p = self.p.take().unwrap();
        let json = serde_json::to_string(&self.cksum).unwrap();
        let p = p.file(".cargo-checksum.json", &json);
        let _ = p.build();
    }
}

#[test]
fn simple() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", r#"
            extern crate foo;

            pub fn bar() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(p.cargo("build"),
                execs().with_status(0).with_stderr("\
[COMPILING] foo v0.1.0
[COMPILING] bar v0.1.0 ([..]bar)
[FINISHED] [..]
"));
}

#[test]
fn simple_install() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .build();

    VendorPackage::new("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/main.rs", r#"
            extern crate foo;

            pub fn main() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(cargo_process().arg("install").arg("bar"),
                execs().with_status(0).with_stderr(
"  Installing bar v0.1.0
   Compiling foo v0.1.0
   Compiling bar v0.1.0
    Finished release [optimized] target(s) in [..] secs
  Installing [..]bar[..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
"));
}

#[test]
fn simple_install_fail() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .build();

    VendorPackage::new("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
            baz = "9.8.7"
        "#)
        .file("src/main.rs", r#"
            extern crate foo;

            pub fn main() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(cargo_process().arg("install").arg("bar"),
                execs().with_status(101).with_stderr(
"  Installing bar v0.1.0
error: failed to compile `bar v0.1.0`, intermediate artifacts can be found at `[..]`

Caused by:
  no matching package named `baz` found
location searched: registry `https://github.com/rust-lang/crates.io-index`
required by package `bar v0.1.0`
"));
}

#[test]
fn install_without_feature_dep() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .build();

    VendorPackage::new("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
            baz = { version = "9.8.7", optional = true }

            [features]
            wantbaz = ["baz"]
        "#)
        .file("src/main.rs", r#"
            extern crate foo;

            pub fn main() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(cargo_process().arg("install").arg("bar"),
                execs().with_status(0).with_stderr(
"  Installing bar v0.1.0
   Compiling foo v0.1.0
   Compiling bar v0.1.0
    Finished release [optimized] target(s) in [..] secs
  Installing [..]bar[..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
"));
}

#[test]
fn not_there() {
    setup();

    let _ = project("index").build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", r#"
            extern crate foo;

            pub fn bar() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(p.cargo("build"),
                execs().with_status(101).with_stderr("\
error: no matching package named `foo` found
location searched: [..]
required by package `bar v0.1.0 ([..])`
"));
}

#[test]
fn multiple() {
    setup();

    VendorPackage::new("foo-0.1.0")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .file(".cargo-checksum", "")
        .build();

    VendorPackage::new("foo-0.2.0")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.2.0"
            authors = []
        "#)
        .file("src/lib.rs", "pub fn foo() {}")
        .file(".cargo-checksum", "")
        .build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", r#"
            extern crate foo;

            pub fn bar() {
                foo::foo();
            }
        "#)
        .build();

    assert_that(p.cargo("build"),
                execs().with_status(0).with_stderr("\
[COMPILING] foo v0.1.0
[COMPILING] bar v0.1.0 ([..]bar)
[FINISHED] [..]
"));
}

#[test]
fn crates_io_then_directory() {
    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", r#"
            extern crate foo;

            pub fn bar() {
                foo::foo();
            }
        "#)
        .build();

    let cksum = Package::new("foo", "0.1.0")
                        .file("src/lib.rs", "pub fn foo() -> u32 { 0 }")
                        .publish();

    assert_that(p.cargo("build"),
                execs().with_status(0).with_stderr("\
[UPDATING] registry `[..]`
[DOWNLOADING] foo v0.1.0 ([..])
[COMPILING] foo v0.1.0
[COMPILING] bar v0.1.0 ([..]bar)
[FINISHED] [..]
"));

    setup();

    let mut v = VendorPackage::new("foo");
    v.file("Cargo.toml", r#"
        [package]
        name = "foo"
        version = "0.1.0"
        authors = []
    "#);
    v.file("src/lib.rs", "pub fn foo() -> u32 { 1 }");
    v.cksum.package = Some(cksum);
    v.build();

    assert_that(p.cargo("build"),
                execs().with_status(0).with_stderr("\
[COMPILING] foo v0.1.0
[COMPILING] bar v0.1.0 ([..]bar)
[FINISHED] [..]
"));
}

#[test]
fn crates_io_then_bad_checksum() {
    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", "")
        .build();

    Package::new("foo", "0.1.0").publish();

    assert_that(p.cargo("build"),
                execs().with_status(0));
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .build();

    assert_that(p.cargo("build"),
                execs().with_status(101).with_stderr("\
error: checksum for `foo v0.1.0` changed between lock files

this could be indicative of a few possible errors:

    * the lock file is corrupt
    * a replacement source in use (e.g. a mirror) returned a different checksum
    * the source itself may be corrupt in one way or another

unable to verify that `foo v0.1.0` is the same as when the lockfile was generated

"));
}

#[test]
fn bad_file_checksum() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .build();

    let mut f = t!(File::create(paths::root().join("index/foo/src/lib.rs")));
    t!(f.write_all(b"fn foo() -> u32 { 0 }"));

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", "")
        .build();

    assert_that(p.cargo("build"),
                execs().with_status(101).with_stderr("\
error: the listed checksum of `[..]lib.rs` has changed:
expected: [..]
actual:   [..]

directory sources are not intended to be edited, if modifications are \
required then it is recommended that [replace] is used with a forked copy of \
the source
"));
}

#[test]
fn only_dot_files_ok() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .build();
    VendorPackage::new("bar")
        .file(".foo", "")
        .build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", "")
        .build();

    assert_that(p.cargo("build"), execs().with_status(0));
}

#[test]
fn random_files_ok() {
    setup();

    VendorPackage::new("foo")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.1.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .build();
    VendorPackage::new("bar")
        .file("foo", "")
        .file("../test", "")
        .build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "bar"
            version = "0.1.0"
            authors = []

            [dependencies]
            foo = "0.1.0"
        "#)
        .file("src/lib.rs", "")
        .build();

    assert_that(p.cargo("build"), execs().with_status(0));
}

#[test]
fn git_lock_file_doesnt_change() {

    let git = git::new("git", |p| {
        p.file("Cargo.toml", r#"
            [project]
            name = "git"
            version = "0.5.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
    }).unwrap();

    VendorPackage::new("git")
        .file("Cargo.toml", r#"
            [package]
            name = "git"
            version = "0.5.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .disable_checksum()
        .build();

    let p = project("bar")
        .file("Cargo.toml", &format!(r#"
            [package]
            name = "foo"
            version = "0.0.1"
            authors = []

            [dependencies]
            git = {{ git = '{0}' }}
        "#, git.url()))
        .file("src/lib.rs", "")
        .build();

    assert_that(p.cargo("build"), execs().with_status(0));

    let mut lock1 = String::new();
    t!(t!(File::open(p.root().join("Cargo.lock"))).read_to_string(&mut lock1));

    let root = paths::root();
    t!(fs::create_dir(&root.join(".cargo")));
    t!(t!(File::create(root.join(".cargo/config"))).write_all(&format!(r#"
        [source.my-git-repo]
        git = '{}'
        replace-with = 'my-awesome-local-registry'

        [source.my-awesome-local-registry]
        directory = 'index'
    "#, git.url()).as_bytes()));

    assert_that(p.cargo("build"),
                execs().with_status(0)
                       .with_stderr("\
[COMPILING] [..]
[COMPILING] [..]
[FINISHED] [..]
"));

    let mut lock2 = String::new();
    t!(t!(File::open(p.root().join("Cargo.lock"))).read_to_string(&mut lock2));
    assert!(lock1 == lock2, "lock files changed");
}

#[test]
fn git_override_requires_lockfile() {
    VendorPackage::new("git")
        .file("Cargo.toml", r#"
            [package]
            name = "git"
            version = "0.5.0"
            authors = []
        "#)
        .file("src/lib.rs", "")
        .disable_checksum()
        .build();

    let p = project("bar")
        .file("Cargo.toml", r#"
            [package]
            name = "foo"
            version = "0.0.1"
            authors = []

            [dependencies]
            git = { git = 'https://example.com/' }
        "#)
        .file("src/lib.rs", "")
        .build();

    let root = paths::root();
    t!(fs::create_dir(&root.join(".cargo")));
    t!(t!(File::create(root.join(".cargo/config"))).write_all(br#"
        [source.my-git-repo]
        git = 'https://example.com/'
        replace-with = 'my-awesome-local-registry'

        [source.my-awesome-local-registry]
        directory = 'index'
    "#));

    assert_that(p.cargo("build"),
                execs().with_status(101)
                       .with_stderr("\
error: failed to load source for a dependency on `git`

Caused by:
  Unable to update [..]

Caused by:
  the source my-git-repo requires a lock file to be present first before it can be
used against vendored source code

remove the source replacement configuration, generate a lock file, and then
restore the source replacement configuration to continue the build

"));
}
