use std::{collections::HashMap, path, thread, time};

use anyhow::Result;
use gitbutler_core::{
    deltas::{self, operations::Operation},
    projects::{self, ApiProject, ProjectId},
    reader,
    sessions::{self, SessionId},
};
use pretty_assertions::assert_eq;
use tempfile::TempDir;

use gitbutler_testsupport::{init_opts_bare, Case, Suite};

mod repository {
    use std::path::PathBuf;

    use anyhow::Result;
    use pretty_assertions::assert_eq;

    use gitbutler_testsupport::{Case, Suite};

    #[test]
    fn alternates_file_being_set() -> Result<()> {
        let suite = Suite::default();
        let Case {
            gb_repository,
            project_repository,
            ..
        } = &suite.new_case();

        let file_content = std::fs::read_to_string(
            gb_repository
                .git_repository_path()
                .join("objects/info/alternates"),
        )?;

        let file_content = PathBuf::from(file_content.trim());
        let project_path = project_repository.path().to_path_buf().join(".git/objects");

        assert_eq!(file_content, project_path);

        Ok(())
    }
}

fn new_test_remote_repository() -> Result<(git2::Repository, TempDir)> {
    let tmp = tempfile::tempdir()?;
    let path = tmp.path().to_str().unwrap().to_string();
    let repo_a = git2::Repository::init_opts(path, &init_opts_bare())?;
    Ok((repo_a, tmp))
}

#[test]
fn get_current_session_writer_should_use_existing_session() -> Result<()> {
    let suite = Suite::default();
    let Case { gb_repository, .. } = &suite.new_case();

    let current_session_1 = gb_repository.get_or_create_current_session()?;
    let current_session_2 = gb_repository.get_or_create_current_session()?;
    assert_eq!(current_session_1.id, current_session_2.id);

    Ok(())
}

#[test]
fn must_not_return_init_session() -> Result<()> {
    let suite = Suite::default();
    let Case { gb_repository, .. } = &suite.new_case();

    assert!(gb_repository.get_current_session()?.is_none());

    let iter = gb_repository.get_sessions_iterator()?;
    assert_eq!(iter.count(), 0);

    Ok(())
}

#[test]
fn must_not_flush_without_current_session() -> Result<()> {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case();

    let session = gb_repository.flush(project_repository, None)?;
    assert!(session.is_none());

    let iter = gb_repository.get_sessions_iterator()?;
    assert_eq!(iter.count(), 0);

    Ok(())
}

#[test]
fn non_empty_repository() -> Result<()> {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case_with_files(HashMap::from([(path::PathBuf::from("test.txt"), "test")]));

    gb_repository.get_or_create_current_session()?;
    gb_repository.flush(project_repository, None)?;

    Ok(())
}

#[test]
fn must_flush_current_session() -> Result<()> {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case();

    gb_repository.get_or_create_current_session()?;

    let session = gb_repository.flush(project_repository, None)?;
    assert!(session.is_some());

    let iter = gb_repository.get_sessions_iterator()?;
    assert_eq!(iter.count(), 1);

    Ok(())
}

#[test]
fn list_deltas_from_current_session() -> Result<()> {
    let suite = Suite::default();
    let Case { gb_repository, .. } = &suite.new_case();

    let current_session = gb_repository.get_or_create_current_session()?;
    let writer = deltas::Writer::new(gb_repository)?;
    writer.write(
        "test.txt",
        &vec![deltas::Delta {
            operations: vec![Operation::Insert((0, "Hello World".to_string()))],
            timestamp_ms: 0,
        }],
    )?;

    let session_reader = sessions::Reader::open(gb_repository, &current_session)?;
    let deltas_reader = deltas::Reader::new(&session_reader);
    let deltas = deltas_reader.read(None)?;

    assert_eq!(deltas.len(), 1);
    assert_eq!(
        deltas[&path::PathBuf::from("test.txt")][0].operations.len(),
        1
    );
    assert_eq!(
        deltas[&path::PathBuf::from("test.txt")][0].operations[0],
        Operation::Insert((0, "Hello World".to_string()))
    );

    Ok(())
}

#[test]
fn list_deltas_from_flushed_session() {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case();

    let writer = deltas::Writer::new(gb_repository).unwrap();
    writer
        .write(
            "test.txt",
            &vec![deltas::Delta {
                operations: vec![Operation::Insert((0, "Hello World".to_string()))],
                timestamp_ms: 0,
            }],
        )
        .unwrap();
    let session = gb_repository.flush(project_repository, None).unwrap();

    let session_reader = sessions::Reader::open(gb_repository, &session.unwrap()).unwrap();
    let deltas_reader = deltas::Reader::new(&session_reader);
    let deltas = deltas_reader.read(None).unwrap();

    assert_eq!(deltas.len(), 1);
    assert_eq!(
        deltas[&path::PathBuf::from("test.txt")][0].operations.len(),
        1
    );
    assert_eq!(
        deltas[&path::PathBuf::from("test.txt")][0].operations[0],
        Operation::Insert((0, "Hello World".to_string()))
    );
}

#[test]
fn list_files_from_current_session() {
    let suite = Suite::default();
    let Case { gb_repository, .. } = &suite.new_case_with_files(HashMap::from([(
        path::PathBuf::from("test.txt"),
        "Hello World",
    )]));

    let current = gb_repository.get_or_create_current_session().unwrap();
    let reader = sessions::Reader::open(gb_repository, &current).unwrap();
    let files = reader.files(None).unwrap();

    assert_eq!(files.len(), 1);
    assert_eq!(
        files[&path::PathBuf::from("test.txt")],
        reader::Content::UTF8("Hello World".to_string())
    );
}

#[test]
fn list_files_from_flushed_session() {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case_with_files(HashMap::from([(
        path::PathBuf::from("test.txt"),
        "Hello World",
    )]));

    gb_repository.get_or_create_current_session().unwrap();
    let session = gb_repository
        .flush(project_repository, None)
        .unwrap()
        .unwrap();
    let reader = sessions::Reader::open(gb_repository, &session).unwrap();
    let files = reader.files(None).unwrap();

    assert_eq!(files.len(), 1);
    assert_eq!(
        files[&path::PathBuf::from("test.txt")],
        reader::Content::UTF8("Hello World".to_string())
    );
}

#[tokio::test]
async fn remote_syncronization() {
    // first, crate a remote, pretending it's a cloud
    let (cloud, _tmp) = new_test_remote_repository().unwrap();
    let api_project = ApiProject {
        name: "test-sync".to_string(),
        description: None,
        repository_id: "123".to_string(),
        git_url: cloud.path().to_str().unwrap().to_string(),
        code_git_url: None,
        created_at: 0_i32.to_string(),
        updated_at: 0_i32.to_string(),
        sync: true,
    };

    let suite = Suite::default();
    let user = suite.sign_in();

    // create first local project, add files, deltas and flush a session
    let case_one = suite.new_case_with_files(HashMap::from([(
        path::PathBuf::from("test.txt"),
        "Hello World",
    )]));
    suite
        .projects
        .update(&projects::UpdateRequest {
            id: case_one.project.id,
            api: Some(api_project.clone()),
            ..Default::default()
        })
        .await
        .unwrap();
    let case_one = case_one.refresh(&suite);

    let writer = deltas::Writer::new(&case_one.gb_repository).unwrap();
    writer
        .write(
            "test.txt",
            &vec![deltas::Delta {
                operations: vec![Operation::Insert((0, "Hello World".to_string()))],
                timestamp_ms: 0,
            }],
        )
        .unwrap();
    let session_one = case_one
        .gb_repository
        .flush(&case_one.project_repository, Some(&user))
        .unwrap()
        .unwrap();
    case_one.gb_repository.push(Some(&user)).unwrap();

    // create second local project, fetch it and make sure session is there
    let case_two = suite.new_case();
    suite
        .projects
        .update(&projects::UpdateRequest {
            id: case_two.project.id,
            api: Some(api_project.clone()),
            ..Default::default()
        })
        .await
        .unwrap();
    let case_two = case_two.refresh(&suite);

    case_two.gb_repository.fetch(Some(&user)).unwrap();

    // now it should have the session from the first local project synced
    let sessions_two = case_two
        .gb_repository
        .get_sessions_iterator()
        .unwrap()
        .map(Result::unwrap)
        .collect::<Vec<_>>();
    assert_eq!(sessions_two.len(), 1);
    assert_eq!(sessions_two[0].id, session_one.id);

    let session_reader = sessions::Reader::open(&case_two.gb_repository, &sessions_two[0]).unwrap();
    let deltas_reader = deltas::Reader::new(&session_reader);
    let deltas = deltas_reader.read(None).unwrap();
    let files = session_reader.files(None).unwrap();
    assert_eq!(deltas.len(), 1);
    assert_eq!(files.len(), 1);
    assert_eq!(
        files[&path::PathBuf::from("test.txt")],
        reader::Content::UTF8("Hello World".to_string())
    );
    assert_eq!(
        deltas[&path::PathBuf::from("test.txt")],
        vec![deltas::Delta {
            operations: vec![Operation::Insert((0, "Hello World".to_string()))],
            timestamp_ms: 0,
        }]
    );
}

#[tokio::test]
async fn remote_sync_order() {
    // first, crate a remote, pretending it's a cloud
    let (cloud, _tmp) = new_test_remote_repository().unwrap();
    let api_project = projects::ApiProject {
        name: "test-sync".to_string(),
        description: None,
        repository_id: "123".to_string(),
        git_url: cloud.path().to_str().unwrap().to_string(),
        code_git_url: None,
        created_at: 0_i32.to_string(),
        updated_at: 0_i32.to_string(),
        sync: true,
    };

    let suite = Suite::default();

    let case_one = suite.new_case();
    suite
        .projects
        .update(&projects::UpdateRequest {
            id: case_one.project.id,
            api: Some(api_project.clone()),
            ..Default::default()
        })
        .await
        .unwrap();
    let case_one = case_one.refresh(&suite);

    let case_two = suite.new_case();
    suite
        .projects
        .update(&projects::UpdateRequest {
            id: case_two.project.id,
            api: Some(api_project.clone()),
            ..Default::default()
        })
        .await
        .unwrap();
    let case_two = case_two.refresh(&suite);

    let user = suite.sign_in();

    // create session in the first project
    case_one
        .gb_repository
        .get_or_create_current_session()
        .unwrap();
    let session_one_first = case_one
        .gb_repository
        .flush(&case_one.project_repository, Some(&user))
        .unwrap()
        .unwrap();
    case_one.gb_repository.push(Some(&user)).unwrap();

    thread::sleep(time::Duration::from_secs(1));

    // create session in the second project
    case_two
        .gb_repository
        .get_or_create_current_session()
        .unwrap();
    let session_two_first = case_two
        .gb_repository
        .flush(&case_two.project_repository, Some(&user))
        .unwrap()
        .unwrap();
    case_two.gb_repository.push(Some(&user)).unwrap();

    thread::sleep(time::Duration::from_secs(1));

    // create second session in the first project
    case_one
        .gb_repository
        .get_or_create_current_session()
        .unwrap();
    let session_one_second = case_one
        .gb_repository
        .flush(&case_one.project_repository, Some(&user))
        .unwrap()
        .unwrap();
    case_one.gb_repository.push(Some(&user)).unwrap();

    thread::sleep(time::Duration::from_secs(1));

    // create second session in the second project
    case_two
        .gb_repository
        .get_or_create_current_session()
        .unwrap();
    let session_two_second = case_two
        .gb_repository
        .flush(&case_two.project_repository, Some(&user))
        .unwrap()
        .unwrap();
    case_two.gb_repository.push(Some(&user)).unwrap();

    case_one.gb_repository.fetch(Some(&user)).unwrap();
    let sessions_one = case_one
        .gb_repository
        .get_sessions_iterator()
        .unwrap()
        .map(Result::unwrap)
        .collect::<Vec<_>>();

    case_two.gb_repository.fetch(Some(&user)).unwrap();
    let sessions_two = case_two
        .gb_repository
        .get_sessions_iterator()
        .unwrap()
        .map(Result::unwrap)
        .collect::<Vec<_>>();

    // make sure the sessions are the same on both repos
    assert_eq!(sessions_one.len(), 4);
    assert_eq!(sessions_two, sessions_one);

    assert_eq!(sessions_one[0].id, session_two_second.id);
    assert_eq!(sessions_one[1].id, session_one_second.id);
    assert_eq!(sessions_one[2].id, session_two_first.id);
    assert_eq!(sessions_one[3].id, session_one_first.id);
}

#[test]
fn gitbutler_file() {
    let suite = Suite::default();
    let Case {
        gb_repository,
        project_repository,
        ..
    } = &suite.new_case();

    let session = gb_repository.get_or_create_current_session().unwrap();

    let gitbutler_file_path = project_repository.path().join(".git/gitbutler.json");
    assert!(gitbutler_file_path.exists());

    let file_content: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(&gitbutler_file_path).unwrap()).unwrap();
    let sid: SessionId = file_content["sessionId"].as_str().unwrap().parse().unwrap();
    assert_eq!(sid, session.id);

    let pid: ProjectId = file_content["repositoryId"]
        .as_str()
        .unwrap()
        .parse()
        .unwrap();
    assert_eq!(pid, project_repository.project().id);
}