//! L8 integration tests for [`Git2Repository`] against a real temporary repo,
//! exercising the local flow end to end: init → status → stage → commit →
//! branch/current_branch → log, plus the not-a-repo error path.

use std::path::PathBuf;

use domain::ports::GitError;
use domain::ports::GitPort;
use domain::project::ProjectPath;
use infrastructure::Git2Repository;
use uuid::Uuid;

struct TempDir(PathBuf);
impl TempDir {
    fn new() -> Self {
        let p = std::env::temp_dir().join(format!("idea-l8-git-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&p).unwrap();
        Self(p)
    }
    fn root(&self) -> ProjectPath {
        ProjectPath::new(self.0.to_string_lossy().into_owned()).unwrap()
    }
    fn write(&self, name: &str, content: &str) {
        std::fs::write(self.0.join(name), content).unwrap();
    }
}
impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

#[tokio::test]
async fn init_status_stage_commit_branch_log_flow() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let git = Git2Repository::new();

    git.init(&root).await.expect("init");

    // A new untracked file shows up as not-staged.
    tmp.write("a.txt", "hello");
    let status = git.status(&root).await.unwrap();
    let a = status
        .iter()
        .find(|s| s.path == "a.txt")
        .expect("a.txt appears in status");
    assert!(!a.staged, "untracked file is not staged");

    // Staging flips the staged flag.
    git.stage(&root, "a.txt").await.unwrap();
    let staged = git.status(&root).await.unwrap();
    assert!(
        staged.iter().find(|s| s.path == "a.txt").unwrap().staged,
        "file is staged after stage()"
    );

    // Commit the index.
    let commit = git.commit(&root, "first commit").await.unwrap();
    assert!(!commit.hash.is_empty());
    assert_eq!(commit.summary, "first commit");

    // After committing, the tree is clean.
    assert!(
        git.status(&root).await.unwrap().is_empty(),
        "no changes after commit"
    );

    // A current branch exists and is listed among local branches.
    let current = git.current_branch(&root).await.unwrap();
    let current = current.expect("a current branch after the first commit");
    let branches = git.branches(&root).await.unwrap();
    assert!(branches.contains(&current), "current branch is listed");

    // The log has exactly our commit.
    let log = git.log(&root, 10).await.unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].summary, "first commit");
    assert_eq!(log[0].hash, commit.hash);
}

#[tokio::test]
async fn status_on_non_repo_is_not_found() {
    let tmp = TempDir::new();
    let err = Git2Repository::new().status(&tmp.root()).await.unwrap_err();
    assert!(matches!(err, GitError::NotFound), "got {err:?}");
}

#[tokio::test]
async fn unstage_after_first_commit_resets_index() {
    let tmp = TempDir::new();
    let root = tmp.root();
    let git = Git2Repository::new();
    git.init(&root).await.unwrap();
    tmp.write("a.txt", "v1");
    git.stage(&root, "a.txt").await.unwrap();
    git.commit(&root, "c1").await.unwrap();

    // Modify + stage, then unstage → the change is no longer staged.
    tmp.write("a.txt", "v2");
    git.stage(&root, "a.txt").await.unwrap();
    assert!(git
        .status(&root)
        .await
        .unwrap()
        .iter()
        .any(|s| s.path == "a.txt" && s.staged));

    git.unstage(&root, "a.txt").await.unwrap();
    let st = git.status(&root).await.unwrap();
    let a = st.iter().find(|s| s.path == "a.txt").unwrap();
    assert!(!a.staged, "unstaged change is no longer in the index");
}
