use crate::{
    diff, file, init, reindex_selection, repo, repo_got_change_diffs,
    selection, update, ManagingRepoMsg, Msg, State,
};
use inflorescence_model::model::{
    ManagingRepoSubState, ReadyState, RecordChanges, SubState,
};
use inflorescence_model::{action, model};
use inflorescence_view::view;
use libflorescence::prelude::Timestamp;
use libflorescence::testing::{
    repo_path, setup_test_repo, setup_test_user_id, TestRepo, TestUserId,
    DEFAULT_IGNORE_FILE, INITIAL_LOG_LEN,
};

use assert_matches::assert_matches;
use derivative::Derivative;
use iced::widget::text_editor;
use iced_utils::task::{self, await_next_msg};
use iced_utils::Task;
use test_log::test;
use tokio::fs;

use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

/// Test app [`init`] function
#[test(tokio::test)]
async fn test_init() {
    let _id = setup_test_user_id();
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);

    let (state, mut tasks) = init(Some(repo_path.to_path_buf()));

    assert_matches!(state.model.sub, model::SubState::ManagingRepo(_));
    if let model::SubState::ManagingRepo(sub) = state.model.sub {
        assert_eq!(sub.repo_path, repo_path);
    }

    // Must receive 3 messages:
    // - Msg::NoOp from opening a window
    // - Msg::LoadedId
    // - Msg::FromRepo(repo::MsgOut::Init)
    let msg_0 = task::await_next_msg(&mut tasks).await;
    let msg_1 = task::await_next_msg(&mut tasks).await;
    let msg_2 = task::await_next_msg(&mut tasks).await;

    let mut window_opened = false;
    let mut loaded_id = false;
    let mut inited_repo = false;

    for msg in [msg_0, msg_1, msg_2] {
        if !window_opened && matches!(msg, Msg::NoOp) {
            window_opened = true;
            continue;
        }
        if !loaded_id
            && matches!(
                msg,
                Msg::ManagingRepo(ManagingRepoMsg::LoadedIdentities(_))
            )
        {
            loaded_id = true;
            continue;
        }
        if !inited_repo
            && matches!(
                msg,
                Msg::ManagingRepo(ManagingRepoMsg::FromRepo(
                    repo::MsgOut::Init(_)
                ))
            )
        {
            inited_repo = true;
            continue;
        }
    }

    let all = [window_opened, loaded_id, inited_repo];
    if !all.iter().all(|success| *success) {
        panic!("Some task didn't complete {all:#?}")
    }
}

/// Test making of records
#[test(tokio::test)]
async fn test_making_records() {
    let TestState {
        mut state,
        mut tasks,
        mut fs_watch_task,
        id: _,
        repo,
    } = setup_state().await;
    let repo_path = repo_path(&repo);

    {
        let ready_state = get_ready_state(&state);
        assert_eq!(ready_state.repo.short_log.len(), INITIAL_LOG_LEN);
    }

    let to_path = |raw: &str| file::Path {
        raw: raw.to_string(),
        is_dir: false,
    };

    // _________________________________________________________________________
    // Case: start -> edit -> save

    // Add an untracked file
    let file_to_record = "funky.rs";
    fs::write(repo_path.join(file_to_record), "music")
        .await
        .unwrap();
    // Wait for the FS watch to pick it up
    let msg = await_next_msg(&mut fs_watch_task).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
            path
        )) if path == &repo_path.join(file_to_record)
    );
    // Update the state with it to send it to repo
    let _task = update(&mut state, msg);
    // Wait for response from repo
    let msg = await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
            invalidate_logs: true,
            ..
        }))
    );
    let _task = update(&mut state, msg);

    // Select it
    update(
        &mut state,
        Msg::View(view::Msg::UnfilteredSelection(
            selection::UnfilteredMsg::Select(
                selection::Select::UntrackedFile {
                    ix: 0,
                    path: to_path(file_to_record),
                },
            ),
        )),
    );

    // Selection triggers `LoadedSrcFile`
    let _msg = task::await_next_msg(&mut tasks).await;

    // Add it to tracked files
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
            model::SubMenu::Add { recursive: false },
        ))),
    );
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
    );

    // Wait for it to be added
    let msg = task::await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile { result:_, path })) if path == file_to_record
    );

    // Start a record
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
    }

    // Edit the record msg
    let record_msg = "Added funky music";
    let _task = update(
        &mut state,
        Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing { .. }
        );
        if let RecordChanges::Typing { msg, desc } =
            ready_state.record_changes.as_ref().unwrap()
        {
            assert_eq!(msg.as_str(), record_msg);
            assert_eq!(&desc.text(), "");
        }
    }

    // Save the record
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::SaveRecord)),
    );
    let msg = task::await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
            invalidate_logs: true,
            ..
        }))
    );
    let _task = update(&mut state, msg);

    // Check the log
    {
        let ready_state = get_ready_state(&state);
        assert_eq!(ready_state.repo.short_log.len(), INITIAL_LOG_LEN + 1);
    }

    // _________________________________________________________________________
    // Case: start -> edit -> abandon

    // Add an untracked file
    let file_to_record = "soul.rs";
    fs::write(repo_path.join(file_to_record), "music")
        .await
        .unwrap();
    // Wait for the FS watch to pick it up
    let msg = await_next_msg(&mut fs_watch_task).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
            path
        )) if path == &repo_path.join(file_to_record)
    );
    // Update the state with it to send it to repo
    let _task = update(&mut state, msg);
    // Wait for response from repo
    let msg = await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(
            repo::MsgOut::Refreshed { .. }
        ))
    );
    let _task = update(&mut state, msg);

    // Select it
    update(
        &mut state,
        Msg::View(view::Msg::UnfilteredSelection(
            selection::UnfilteredMsg::Select(
                selection::Select::UntrackedFile {
                    ix: 0,
                    path: to_path(file_to_record),
                },
            ),
        )),
    );

    // Selection triggers `LoadedSrcFile`
    let msg = task::await_next_msg(&mut tasks).await;
    let id = file::Id {
        path: to_path(file_to_record),
        file_kind: file::Kind::Untracked,
    };
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
            if *loaded_id == id
    );

    // Add it to tracked files
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
            model::SubMenu::Add { recursive: false },
        ))),
    );
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
    );

    // Wait for it to be added
    let msg = task::await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile {result:_, path })) if path == file_to_record
    );

    // Start a record
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
    }

    // Edit the record msg
    let record_msg = "Added soul music";
    let _task = update(
        &mut state,
        Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing { .. }
        );
        if let RecordChanges::Typing { msg, desc } =
            ready_state.record_changes.as_ref().unwrap()
        {
            assert_eq!(msg.as_str(), record_msg);
            assert_eq!(&desc.text(), "");
        }
    }

    // Abandon it
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_none());
    }

    // _________________________________________________________________________
    // Case: start -> edit msg -> defer -> start -> abandon

    // Start a record - there's an an added file from previous case
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
    }

    // Edit the record msg
    let record_msg = "Added soul music";
    let _task = update(
        &mut state,
        Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing { .. }
        );
        if let RecordChanges::Typing { msg, desc } =
            ready_state.record_changes.as_ref().unwrap()
        {
            assert_eq!(msg.as_str(), record_msg);
            assert_eq!(&desc.text(), "");
        }
    }

    // Defer the record
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::PostponeRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Canceled { old_msg, old_desc: _ } if old_msg == record_msg
        );
    }

    // Start again
    // Start a record - there's an an added file from previous case
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing{ msg, desc: _ } if msg.as_str() == record_msg
        );
    }

    // Abandon it
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_none());
    }

    // _________________________________________________________________________
    // Case: start -> edit desc -> defer -> start

    // Start a record - there's an an added file from previous case
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
    }

    // Edit the record desc
    let record_desc = "Added honky tonk music";
    let _task = update(
        &mut state,
        Msg::View(view::Msg::EditRecordDesc(text_editor::Action::Edit(
            text_editor::Edit::Paste(Arc::new(record_desc.to_string())),
        ))),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing { .. }
        );
        if let RecordChanges::Typing { msg, desc } =
            ready_state.record_changes.as_ref().unwrap()
        {
            assert_eq!(msg.as_str(), "");
            assert_eq!(&desc.text(), record_desc);
        }
    }

    // Defer the record
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::PostponeRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Canceled { old_msg: _, old_desc } if old_desc == record_desc
        );
    }

    // Start again
    // Start a record - there's an an added file from previous case
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_some());
        assert!(ready_state.record_changes.is_some());
        assert_matches!(
            ready_state.record_changes.as_ref().unwrap(),
            RecordChanges::Typing{ msg: _, desc } if desc.text() == record_desc
        );
    }

    // Abandon it
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
    );
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_none());
    }
}

/// When there's nothing to record, trying to start recording does nothing
#[test(tokio::test)]
async fn test_start_record_when_nothing_to_record() {
    let TestState {
        mut state,
        tasks: _,
        fs_watch_task: _,
        id: _,
        repo: _repo,
    } = setup_state().await;

    let task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
    );
    assert!(task.is_none());
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.record_changes.is_none());
    }
}

/// Test add and rm untracked file
#[test(tokio::test)]
async fn test_add_and_rm_untracked_file() {
    let TestState {
        mut state,
        mut tasks,
        mut fs_watch_task,
        id: _,
        repo,
    } = setup_state().await;
    let repo_path = repo_path(&repo);

    {
        let ready_state = get_ready_state(&state);
        let repo_state = &ready_state.repo;
        assert!(repo_state.untracked_files.is_empty());
        assert!(repo_state.changed_files.is_empty());
    }

    let to_path = |raw: &str| file::Path {
        raw: raw.to_string(),
        is_dir: false,
    };

    // Add an untracked file
    let file_to_record = "new_file.rs";
    fs::write(repo_path.join(file_to_record), "some contents")
        .await
        .unwrap();
    // Wait for the FS watch to pick it up
    let msg = await_next_msg(&mut fs_watch_task).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
            path
        )) if path == &repo_path.join(file_to_record)
    );
    // Update the state with it to send it to repo
    let _task = update(&mut state, msg);
    // Wait for response from repo
    let msg = await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
            invalidate_logs: true,
            ..
        }))
    );
    let _task = update(&mut state, msg);

    {
        let ready_state = get_ready_state(&state);
        let repo_state = &ready_state.repo;
        assert_eq!(repo_state.untracked_files.len(), 1);
        assert!(repo_state
            .untracked_files
            .contains(&to_path(file_to_record)));
        assert!(repo_state.changed_files.is_empty());
    }

    // Select it
    update(
        &mut state,
        Msg::View(view::Msg::UnfilteredSelection(
            selection::UnfilteredMsg::Select(
                selection::Select::UntrackedFile {
                    ix: 0,
                    path: to_path(file_to_record),
                },
            ),
        )),
    );

    // Selection triggers `LoadedSrcFile`
    let msg = task::await_next_msg(&mut tasks).await;
    let id = file::Id {
        path: to_path(file_to_record),
        file_kind: file::Kind::Untracked,
    };
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
            if *loaded_id == id
    );

    // Add it to tracked files
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
            model::SubMenu::Add { recursive: false },
        ))),
    );
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
    );

    // Wait for it to be added
    let msg = task::await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile { result:_, path }))
            if path == file_to_record
    );

    {
        let ready_state = get_ready_state(&state);
        let repo_state = &ready_state.repo;
        assert!(repo_state.untracked_files.is_empty());
        assert_eq!(repo_state.changed_files.len(), 1);
        assert!(repo_state
            .changed_files
            .contains_key(&to_path(file_to_record)));
        assert_eq!(
            repo_state
                .changed_files
                .get(&to_path(file_to_record))
                .unwrap(),
            &BTreeSet::from_iter([repo::ChangedFileDiff::Add {
                contents: None
            }])
        );
    }

    // Select the added file
    update(
        &mut state,
        Msg::View(view::Msg::UnfilteredSelection(
            selection::UnfilteredMsg::Select(selection::Select::ChangedFile {
                ix: 0,
                path: to_path(file_to_record),
            }),
        )),
    );

    // Selection triggers `LoadedSrcFile`
    let msg = task::await_next_msg(&mut tasks).await;
    let id = file::Id {
        path: to_path(file_to_record),
        file_kind: file::Kind::Changed,
    };
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
            if *loaded_id == id
    );

    // Remove it to get it back into untracked files
    let _task = update(
        &mut state,
        Msg::View(view::Msg::Action(action::FilteredMsg::RmChange)),
    );

    // Wait for it to be rm'd
    let msg = task::await_next_msg(&mut tasks).await;
    assert_matches!(
        &msg,
        Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::RmedAddedFile { result:_, path }))
            if path == file_to_record
    );

    {
        let ready_state = get_ready_state(&state);
        let repo_state = &ready_state.repo;
        assert_eq!(repo_state.untracked_files.len(), 1);
        assert!(repo_state
            .untracked_files
            .contains(&to_path(file_to_record)));
        assert!(repo_state.changed_files.is_empty());
    }
}

/// When the repo is refreshed we try to re-index the selection if it's still
/// present.
///
/// If the selection is still present we must perform different actions
/// depending on the selection type:
///
/// - untracked file: We should start loading the file
/// - changed file: Load the file if the diff has any contents
/// - log change: Request the diffs
///
/// If the selection is not present any more, no action must be taken.
#[test(tokio::test)]
async fn test_reindex_selection() {
    let TestState {
        mut state,
        tasks: _tasks,
        fs_watch_task: _,
        id: _,
        repo: _,
    } = setup_state().await;

    let SubState::ManagingRepo(sub) = &mut state.model.sub else {
        panic!("Unexpected state: {:?}", state.model.sub)
    };
    let ManagingRepoSubState::Ready(ready_state) = &mut sub.sub else {
        panic!("Unexpected state: {:?}", sub.sub)
    };

    let to_path = |raw: &str| file::Path {
        raw: raw.to_string(),
        is_dir: false,
    };

    // _________________________________________________________________________
    // Case: no current selection

    assert!(ready_state.selection.status.is_none());

    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_none());
    assert!(state
        .managing_repo
        .as_ref()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .is_empty());

    assert!(task.is_none());

    // _________________________________________________________________________
    // Case: untracked file selection present after refresh, index is increased

    ready_state.repo.untracked_files = BTreeSet::from_iter([
        to_path("untracked_0.rs"),
        to_path("untracked_1.rs"),
        to_path("untracked_2.rs"),
    ]);

    ready_state.selection.status = Some(selection::Status::UntrackedFile {
        ix: 0,
        path: to_path("untracked_1.rs"),
        diff_selected: false,
    });
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_some());
    assert_matches!(
        ready_state.selection.status.as_ref().unwrap(),
        selection::Status::UntrackedFile { ix, path, diff_selected }
            if *ix == 1 && path.raw == "untracked_1.rs" && !diff_selected
    );

    assert!(task.is_none());
    assert_eq!(
        state
            .managing_repo
            .as_ref()
            .unwrap()
            .files
            .diffs_cache
            .inner
            .len(),
        1
    );
    assert_matches!(
        state
            .managing_repo
            .as_ref()
            .unwrap()
            .files
            .diffs_cache
            .inner
            .peek(&file::id_parts_hash(
                &to_path("untracked_1.rs"),
                file::Kind::Untracked
            )),
        Some(file::Diff::Loading)
    );
    // Clear the cache for next test case
    state
        .managing_repo
        .as_mut()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .clear();

    // _________________________________________________________________________
    // Case: untracked file selection not present after refresh

    ready_state.selection.status = Some(selection::Status::UntrackedFile {
        ix: 0,
        path: to_path("untracked_gone.rs"),
        diff_selected: false,
    });
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_none());

    assert!(task.is_none());
    assert!(state
        .managing_repo
        .as_ref()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .is_empty());

    // _________________________________________________________________________
    // Case: changed file selection present after refresh, index is decreased,
    // file has diffs with contents

    ready_state.repo.changed_files = repo::ChangedFiles::from_iter([
        (
            to_path("changed_0.rs"),
            BTreeSet::from_iter([repo::ChangedFileDiff::Add {
                contents: None,
            }]),
        ),
        (to_path("changed_1.rs"), BTreeSet::new()),
        (to_path("changed_2.rs"), BTreeSet::new()),
    ]);

    ready_state.selection.status = Some(selection::Status::ChangedFile {
        ix: 1,
        path: to_path("changed_0.rs"),
        diff_selected: false,
    });
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_some());
    assert_matches!(
        ready_state.selection.status.as_ref().unwrap(),
        selection::Status::ChangedFile { ix, path, diff_selected }
            if *ix == 0 && path.raw == "changed_0.rs" && !diff_selected
    );

    assert!(task.is_none());
    assert_eq!(
        state
            .managing_repo
            .as_ref()
            .unwrap()
            .files
            .diffs_cache
            .inner
            .len(),
        1
    );
    assert_matches!(
        state
            .managing_repo
            .as_ref()
            .unwrap()
            .files
            .diffs_cache
            .inner
            .peek(&file::id_parts_hash(
                &to_path("changed_0.rs"),
                file::Kind::Changed
            )),
        Some(file::Diff::Loading)
    );
    // Clear the cache for next test case
    state
        .managing_repo
        .as_mut()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .clear();

    // _________________________________________________________________________
    // Case: changed file selection present after refresh, index is the same,
    // but file has no diffs with contents

    ready_state.selection.status = Some(selection::Status::ChangedFile {
        ix: 1,
        path: to_path("changed_1.rs"),
        diff_selected: false,
    });
    // // Add a diff that would make `reindex_selection` expect that the file
    // doesn't exist
    {
        let changed_file = BTreeSet::from_iter([repo::ChangedFileDiff::Del {
            contents: None,
        }]);
        assert!(!diff::should_file_exist(&changed_file));
        ready_state
            .repo
            .changed_files
            .insert(to_path("changed_1.rs"), changed_file);
    }
    let mut task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_some());
    assert_matches!(
        ready_state.selection.status.as_ref().unwrap(),
        selection::Status::ChangedFile { ix, path, diff_selected }
            if *ix == 1 && path.raw == "changed_1.rs" && !diff_selected
    );

    assert!(task.is_some());
    let msg = await_next_msg(&mut task).await;
    assert_matches!(
        &msg,
        crate::ManagingRepoMsg::File(file::Msg::SrcFileDoesntExist { .. })
    );
    // The file will be assembled from the diff
    assert_matches!(
        state
            .managing_repo
            .as_ref()
            .unwrap()
            .files
            .diffs_cache
            .inner
            .peek(&file::id_parts_hash(
                &to_path("changed_1.rs"),
                file::Kind::Changed
            )),
        Some(file::Diff::Loaded(_))
    );
    // Clear the cache for next test case
    state
        .managing_repo
        .as_mut()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .clear();

    // _________________________________________________________________________
    // Case: changed file selection not present after refresh

    ready_state.selection.status = Some(selection::Status::ChangedFile {
        ix: 0,
        path: to_path("changed_gone.rs"),
        diff_selected: false,
    });
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_none());

    assert!(task.is_none());
    assert!(state
        .managing_repo
        .as_ref()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .is_empty());

    // _________________________________________________________________________
    // Case: log selection present after refresh

    let change_hash_0 = repo::hash_bytes(&[0]);
    let change_hash_1 = repo::hash_bytes(&[1]);
    let change_hash_2 = repo::hash_bytes(&[2]);
    ready_state.repo.short_log = vec![
        repo::LogEntry {
            hash: change_hash_0,
            message: "".to_string(),
            description: None,
            timestamp: Timestamp::now(),
            file_paths: vec![],
        },
        repo::LogEntry {
            hash: change_hash_1,
            message: "".to_string(),
            description: None,
            timestamp: Timestamp::now(),
            file_paths: vec![],
        },
        repo::LogEntry {
            hash: change_hash_2,
            message: "".to_string(),
            description: None,
            timestamp: Timestamp::now(),
            file_paths: vec![],
        },
    ];

    ready_state.selection.status =
        Some(selection::Status::LogChange(selection::LogChange {
            ix: 0,
            hash: change_hash_2,
            message: "".to_string(),
            file: None,
        }));
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_some());
    assert_matches!(
        ready_state.selection.status.as_ref().unwrap(),
        selection::Status::LogChange(selection::LogChange { ix, hash, .. })
            if *ix == 2 && *hash == change_hash_2
    );

    assert!(task.is_some());
    assert!(state
        .managing_repo
        .as_ref()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .is_empty());

    // _________________________________________________________________________
    // Case: log selection not present after refresh

    ready_state.selection.status = Some(selection::Status::UntrackedFile {
        ix: 0,
        path: to_path("log_gone.rs"),
        diff_selected: false,
    });
    let task = reindex_selection(
        &ready_state.repo,
        &mut ready_state.selection,
        &mut state.managing_repo.as_mut().unwrap().files,
        &ready_state.logs,
    );

    assert!(ready_state.selection.status.is_none());

    assert!(task.is_none());
    assert!(state
        .managing_repo
        .as_ref()
        .unwrap()
        .files
        .diffs_cache
        .inner
        .is_empty());
}

/// When a repo loads a change diff, it should only be used if the same change
/// is still selected
#[test(tokio::test)]
async fn test_repo_got_change_diffs() {
    let TestState {
        mut state,
        tasks: _tasks,
        fs_watch_task: _,
        id: _,
        repo: _,
    } = setup_state().await;

    let change_hash_0 = repo::hash_bytes(&[0]);
    let change_hash_1 = repo::hash_bytes(&[1]);

    // _________________________________________________________________________
    // Case: nothing is selected

    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.selection.status.is_none());
    }
    let diffs = BTreeMap::from_iter([]);
    let task = {
        let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
            panic!("Unexpected state {:?}", state.model.sub)
        };
        repo_got_change_diffs(model, change_hash_0, diffs)
    };
    assert!(task.is_none());
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.selection.status.is_none());
    }

    // _________________________________________________________________________
    // Case: selection is changed and doesn't match the loaded diff

    {
        let ready_state = get_ready_state_mut(&mut state);
        ready_state.selection.status =
            Some(selection::Status::LogChange(selection::LogChange {
                ix: 0,
                hash: change_hash_0,
                message: "".to_string(),
                file: None,
            }));
    }
    let diffs = BTreeMap::from_iter([]);
    let task = {
        let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
            panic!("Unexpected state {:?}", state.model.sub)
        };
        repo_got_change_diffs(model, change_hash_1, diffs)
    };
    assert!(task.is_none());
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.selection.status.is_some());
    }

    // _________________________________________________________________________
    // Case: selection is still the same and matches the loaded diff

    {
        let ready_state = get_ready_state_mut(&mut state);
        ready_state.selection.status =
            Some(selection::Status::LogChange(selection::LogChange {
                ix: 0,
                hash: change_hash_1,
                message: "".to_string(),
                file: None,
            }));
    }
    let diffs = BTreeMap::from_iter([]);
    let task = {
        let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
            panic!("Unexpected state {:?}", state.model.sub)
        };
        repo_got_change_diffs(model, change_hash_1, diffs)
    };
    assert!(task.is_none());
    {
        let ready_state = get_ready_state(&state);
        assert!(ready_state.selection.status.is_some());
    }
}

async fn setup_state() -> TestState {
    let id = setup_test_user_id();
    let repo = setup_test_repo();
    let repo_path = repo_path(&repo);

    let (mut state, mut tasks) = init(Some(repo_path.to_path_buf()));
    // Wait for the first 3 msgs:
    // - Msg::WindowOpened
    // - Msg::LoadedId
    // - Msg::FromRepo(repo::MsgOut::Init)
    let mut fs_watch_task: Task<Msg> = Task::none();
    for _ in 0..3 {
        let msg = task::await_next_msg(&mut tasks).await;
        let is_init = matches!(
            &msg,
            Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Init(_)))
        ); // Upate state with the msg received from task to init the repo

        let task = update(&mut state, msg);

        if is_init {
            fs_watch_task = task;
        }
    }
    let ready_state = get_ready_state(&state);

    // Expect 2 changes in the log:
    // - Add root
    // - Add ".ignore" file
    let log = &ready_state.repo.short_log;
    assert_eq!(log.len(), 2);
    assert_eq!(log[0].file_paths, [DEFAULT_IGNORE_FILE]);
    assert_eq!(log[1].file_paths, ["/"]);

    TestState {
        state,
        tasks,
        fs_watch_task,
        id,
        repo,
    }
}

#[derive(Derivative)]
#[derivative(Debug)]
struct TestState {
    pub state: State,
    /// Tasks to open window, set icon, tasks replies from repo, load user id
    /// and files
    #[derivative(Debug = "ignore")]
    pub tasks: Task<Msg>,
    #[derivative(Debug = "ignore")]
    pub fs_watch_task: Task<Msg>,
    pub id: TestUserId,
    pub repo: TestRepo,
}

#[track_caller]
fn get_ready_state(state: &State) -> &ReadyState {
    let SubState::ManagingRepo(state) = &state.model.sub else {
        panic!("Unexpected state: {:?}", state.model.sub)
    };
    let ManagingRepoSubState::Ready(ready_state) = &state.sub else {
        panic!("Unexpected state: {:?}", state.sub)
    };
    ready_state
}

#[track_caller]
fn get_ready_state_mut(state: &mut State) -> &mut ReadyState {
    let state_dbg = format!("{:?}", state.model.sub);
    let SubState::ManagingRepo(state) = &mut state.model.sub else {
        panic!("Unexpected state: {state_dbg}")
    };
    let state_dbg = format!("{:?}", state.sub);
    let ManagingRepoSubState::Ready(ready_state) = &mut state.sub else {
        panic!("Unexpected state: {state_dbg}")
    };
    ready_state
}