use crate::file;
use crate::repo::{self, pijul};
use crate::testing::{
    repo_path, setup_test_repo, setup_test_repo_in_subdir, DEFAULT_IGNORE_FILE,
    INITIAL_LOG_LEN,
};

use assert_matches::assert_matches;
use libpijul::key::SKey;
use tempfile::tempdir;
use test_log::test;

use std::collections::BTreeSet;
use std::sync::Arc;

#[test]
fn test_load_repo() {
    // _________________________________________________________________________
    // Case: Handle a non-existent path gracefully

    let dir = tempdir().unwrap();
    let result =
        repo::load(&dir.path().join("howdoyouopensomethingthatdoesntexist"));
    assert!(result.is_err());
    assert_matches!(result.unwrap_err(), repo::LoadError::DoesntExist(_));

    // _________________________________________________________________________
    // Case: Handle a path without a pijul repo gracefully

    let dir = tempdir().unwrap();
    let result = repo::load(dir.path());
    assert!(result.is_err());
    assert_matches!(result.unwrap_err(), repo::LoadError::NotPijulRepo(_));

    // _________________________________________________________________________
    // Case: Handle an inaccessible path due to permissions gracefully

    #[cfg(unix)]
    {
        let subdir = "inaccessible";
        let repo = setup_test_repo_in_subdir(subdir);
        let repo_path = repo_path(&repo);
        // Make the dir inaccessible
        std::fs::set_permissions(repo.rootdir.path(), <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o000)).unwrap();
        assert!(std::fs::exists(&repo_path).is_err());

        let result = repo::load(&repo.rootdir.path().join(subdir));
        assert!(result.is_err());
        assert_matches!(
            result.unwrap_err(),
            repo::LoadError::Inaccessible(_, _)
        );
    }

    // _________________________________________________________________________
    // Case: A valid path with a repo

    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (_internal, state) = repo::load(&repo_path).unwrap();
    let repo::State {
        dir_name,
        channel,
        other_channels,
        untracked_files,
        changed_files,
        short_log,
        remotes: _,
    } = state.unwrap();
    assert_eq!(
        &dir_name,
        &repo_path
            .components()
            .next_back()
            .unwrap()
            .as_os_str()
            .to_string_lossy()
    );
    assert_eq!(&channel, libpijul::DEFAULT_CHANNEL);
    assert_eq!(other_channels, BTreeSet::<String>::new());
    assert!(untracked_files.is_empty());
    assert!(changed_files.is_empty());
    assert_eq!(short_log.len(), 2);
}

#[test]
fn test_current_channel() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);

    let mut repo = pijul::Repository::find_root(Some(&repo_path)).unwrap();
    assert_eq!(
        repo::current_channel(&repo).unwrap(),
        libpijul::DEFAULT_CHANNEL
    );

    let new_channel_name = "NEW CHANNEL!".to_string();
    repo::new_channel(&mut repo, new_channel_name.clone()).unwrap();
    assert_eq!(repo::current_channel(&repo).unwrap(), new_channel_name);
}

#[test]
fn test_record() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (internal, _state) = repo::load(&repo_path).unwrap();

    let log = repo::get_log(&internal.repo, None, None, None).unwrap();
    assert_eq!(log.len(), INITIAL_LOG_LEN);

    // Make a change to a tracked file
    let file_path = DEFAULT_IGNORE_FILE;
    let full_file_path = repo_path.join(DEFAULT_IGNORE_FILE);
    std::fs::write(&full_file_path, "some content").unwrap();

    // Record it
    let skey = Arc::new(SKey::generate(None));
    let msg = "some message";
    let to_record = crate::to_record::State::default();
    repo::record(
        &internal,
        msg.to_string(),
        None,
        skey,
        "ID".to_string(),
        to_record,
    )
    .unwrap();

    let log = repo::get_log(&internal.repo, None, None, None).unwrap();
    assert_eq!(log.len(), INITIAL_LOG_LEN + 1);
    let repo::LogEntry {
        hash: _,
        message,
        description,
        file_paths,
        timestamp: _,
    } = log.first().unwrap();
    assert_eq!(message, msg);
    assert_eq!(description, &None);
    assert_eq!(file_paths.len(), 1);
    assert_eq!(file_paths.first().unwrap(), file_path);
}

#[test]
fn test_diff_and_changed_files() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (internal, _state) = repo::load(&repo_path).unwrap();

    let diff = repo::get_diff(&internal.repo).unwrap();
    assert!(diff.changes.is_empty());

    // Make a change to a tracked file
    std::fs::write(repo_path.join(DEFAULT_IGNORE_FILE), "some content")
        .unwrap();

    let diff = repo::get_diff(&internal.repo).unwrap();
    assert_eq!(diff.changes.len(), 1);
    assert_matches!(
        diff.changes.first().unwrap(),
        libpijul::change::BaseHunk::Replacement { .. }
    );

    let changed_files = repo::changed_files(&internal.repo, &diff).unwrap();
    assert_eq!(changed_files.len(), 1);
    let path = file::Path {
        raw: DEFAULT_IGNORE_FILE.to_string(),
        is_dir: false,
    };
    let changed_file_diff = changed_files.get(&path).unwrap();
    assert_eq!(changed_file_diff.len(), 1);
    assert_matches!(
        changed_file_diff.first().unwrap(),
        repo::ChangedFileDiff::Replacement { .. }
    );
}

#[test]
fn test_untracked_files() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (internal, _state) = repo::load(&repo_path).unwrap();

    let untracked_files = repo::untracked_files(&internal.repo).unwrap();
    assert!(untracked_files.is_empty());

    // Write a new file
    let new_file = "new_file";
    std::fs::write(repo_path.join(new_file), "some content").unwrap();

    let untracked_files = repo::untracked_files(&internal.repo).unwrap();
    assert_eq!(untracked_files.len(), 1);
    assert_eq!(untracked_files.first().unwrap().raw, new_file);
}

#[test]
fn test_add_and_rm() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (mut internal, _state) = repo::load(&repo_path).unwrap();

    let diff = repo::get_diff(&internal.repo).unwrap();
    assert!(diff.changes.is_empty());

    // Write a new file
    let new_file = "new_file";
    std::fs::write(repo_path.join(new_file), "some content").unwrap();

    let recursive = false;
    repo::add(&mut internal.repo, new_file, recursive).unwrap();

    let diff = repo::get_diff(&internal.repo).unwrap();
    assert_eq!(diff.changes.len(), 1);
    assert_matches!(
        diff.changes.first().unwrap(),
        libpijul::change::BaseHunk::FileAdd { .. }
    );

    repo::rm(&mut internal.repo, new_file).unwrap();

    let adiff = repo::get_diff(&internal.repo).unwrap();
    assert!(adiff.changes.is_empty());

    // Create a dir with a file inside it
    let new_dir = "a";
    std::fs::create_dir(repo_path.join(new_dir)).unwrap();
    let new_file = "new_file";
    std::fs::write(repo_path.join(new_dir).join(new_file), "some content")
        .unwrap();

    let recursive = true;
    repo::add(&mut internal.repo, new_dir, recursive).unwrap();

    let diff = repo::get_diff(&internal.repo).unwrap();
    assert_eq!(diff.changes.len(), 2);
}

#[test]
fn test_log() {
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);
    let (internal, _state) = repo::load(&repo_path).unwrap();

    let mut log = repo::get_log(&internal.repo, None, None, None).unwrap();
    assert_eq!(2, INITIAL_LOG_LEN);
    assert_eq!(log.len(), INITIAL_LOG_LEN);
    let change_1 = log.pop().unwrap();
    let change_0 = log.pop().unwrap();

    let limit = 1;
    let mut log =
        repo::get_log(&internal.repo, None, None, Some(limit)).unwrap();
    assert_eq!(log.len(), limit);
    assert_eq!(log.pop().unwrap(), change_0);

    let offset = 1;
    let mut log =
        repo::get_log(&internal.repo, None, Some(offset), None).unwrap();
    assert_eq!(log.len(), INITIAL_LOG_LEN - 1);
    assert_eq!(log.pop().unwrap(), change_1);
}