use std::{fs::File, path::Path, process::Command};

use assert_cmd::prelude::*;
use assert_fs::fixture::{FileWriteStr, PathChild};
use filetime::FileTime;
use predicates::prelude::*;
use serde_json::Value;
use sha2::{Digest, Sha256};
use testresult::TestResult;

#[test]
fn corrupted() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;
    dir.child("b").write_str("banana")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    let db = dir.child(".brck");
    assert!(db.exists());
    let db_time = db.metadata()?.modified()?;
    let db_hash = sha256(db.path())?;

    let b = dir.child("b");
    let b_time = FileTime::from_last_modification_time(&b.metadata()?);
    b.write_str("cheese")?;
    filetime::set_file_mtime(b.path(), b_time)?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .failure()
        .stdout(predicate::str::contains("corrupted"))
        .stderr(predicate::str::contains("Error"));

    assert_eq!(db_time, db.metadata()?.modified()?);
    assert_eq!(db_hash, sha256(db.path())?);

    Ok(())
}

#[test]
fn quiet_by_default() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    Ok(())
}

#[test]
fn verbose() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--verbose")
        .assert()
        .success()
        .stdout(predicate::str::contains("added"))
        .stderr(predicate::str::is_empty());

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--verbose")
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--verbose")
        .arg("--verbose")
        .assert()
        .success()
        .stdout(predicate::str::contains("unchanged"))
        .stderr(predicate::str::is_empty());

    Ok(())
}

#[test]
fn json() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    let assert = cmd
        .current_dir(&dir)
        .arg("--verbose")
        .arg("--verbose")
        .arg("--json")
        .assert()
        .success()
        .stderr(predicate::str::is_empty());

    let out = &assert.get_output().stdout;
    let mut iter = serde_json::Deserializer::from_slice(out).into_iter::<Value>();
    let added = iter.next().unwrap()?;
    assert_eq!(
        added.get("type"),
        Some(Value::String("added".into())).as_ref()
    );
    assert_eq!(
        added.get("new").unwrap().get("sha256"),
        Some(Value::String(
            "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b".into()
        ))
        .as_ref()
    );
    assert!(iter.next().is_none());

    Ok(())
}

#[test]
fn summary() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;
    dir.child("b").write_str("banana")?;
    dir.child("c").write_str("cherry")?;
    dir.child("d").write_str("date")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    std::fs::remove_file(dir.child("b"))?;
    dir.child("c").write_str("cashew")?;
    dir.child("d").write_str("date")?;
    dir.child("e").write_str("elderberry")?;
    dir.child("f").write_str("fig")?;

    let mut cmd = Command::cargo_bin("brck")?;
    // Matching ^ and $ doesn't really work.
    let pred = predicate::str::is_match("Removed: *1")?
        .and(predicate::str::is_match("Added: *2")?) // e, f
        .and(predicate::str::is_match("Unchanged: *1")?) // a
        .and(predicate::str::is_match("Corrupted: *0")?) // -
        .and(predicate::str::is_match("Touched: *1")?) // d
        .and(predicate::str::is_match("Changed: *1")?) // c
        .and(predicate::str::is_match("Total: *6")?); // a-f
    cmd.current_dir(&dir)
        .arg("--summary")
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(pred);

    Ok(())
}

#[test]
fn dry_run_init() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains("dry run"));
    assert!(!dir.child(".brck").exists());

    Ok(())
}

#[test]
fn dry_run_update() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    let db = dir.child(".brck");
    assert!(db.exists());
    let db_time = db.metadata()?.modified()?;
    let db_hash = sha256(db.path())?;

    dir.child("b").write_str("banana")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains("dry run"));

    assert_eq!(db_time, db.metadata()?.modified()?);
    assert_eq!(db_hash, sha256(db.path())?);

    Ok(())
}

#[test]
fn deny() -> TestResult {
    let dir = assert_fs::TempDir::new()?;
    dir.child("a").write_str("apple")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    let db = dir.child(".brck");
    assert!(db.exists());
    let db_time = db.metadata()?.modified()?;
    let db_hash = sha256(db.path())?;

    dir.child("b").write_str("banana")?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .arg("--deny")
        .arg("added")
        .assert()
        .failure()
        .stdout(predicate::str::contains("added"))
        .stderr(predicate::str::contains("Error"));

    assert_eq!(db_time, db.metadata()?.modified()?);
    assert_eq!(db_hash, sha256(db.path())?);

    Ok(())
}

#[test]
fn empty_dir() -> TestResult {
    let dir = assert_fs::TempDir::new()?;

    let mut cmd = Command::cargo_bin("brck")?;
    cmd.current_dir(&dir)
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());

    assert!(dir.child(".brck").exists());

    Ok(())
}

#[test]
fn short_help() -> TestResult {
    let mut cmd = Command::cargo_bin("brck")?;
    cmd.arg("-h")
        .assert()
        .failure()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains("--help"));

    Ok(())
}

#[test]
fn long_help() -> TestResult {
    let mut cmd = Command::cargo_bin("brck")?;
    cmd.arg("--help")
        .assert()
        .failure()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains("Usage"));

    Ok(())
}

fn sha256<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, std::io::Error> {
    let mut file = File::open(path)?;
    let mut hasher = digest_io::IoWrapper(Sha256::new());
    std::io::copy(&mut file, &mut hasher)?;
    let hash = hasher.0.finalize();
    Ok(hash.as_slice().into())
}
