//! L8 tests for the Git use cases with a faked [`GitPort`] (no real repo):
//! pass-through to the port, event emission on state changes, and input
//! validation (empty message, non-absolute root).

use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ports::{
    EventBus, EventStream, GitCommitInfo, GitError, GitFileStatus, GitPort, GraphCommit,
};
use domain::{ProjectId, ProjectPath};
use uuid::Uuid;

use application::{
    GitBranches, GitBranchesInput, GitCheckout, GitCheckoutInput, GitCommit, GitCommitInput,
    GitStage, GitStagePathInput, GitStatus, GitStatusInput,
};

/// A recording [`GitPort`] with canned return values.
#[derive(Default)]
struct FakeGitInner {
    calls: Vec<String>,
    status: Vec<GitFileStatus>,
    branches: Vec<String>,
    current: Option<String>,
}

#[derive(Default, Clone)]
struct FakeGit(Arc<Mutex<FakeGitInner>>);
impl FakeGit {
    fn calls(&self) -> Vec<String> {
        self.0.lock().unwrap().calls.clone()
    }
    fn set_status(&self, s: Vec<GitFileStatus>) {
        self.0.lock().unwrap().status = s;
    }
    fn set_branches(&self, b: Vec<String>, current: Option<String>) {
        let mut i = self.0.lock().unwrap();
        i.branches = b;
        i.current = current;
    }
    fn record(&self, c: &str) {
        self.0.lock().unwrap().calls.push(c.to_owned());
    }
}

#[async_trait]
impl GitPort for FakeGit {
    async fn init(&self, _r: &ProjectPath) -> Result<(), GitError> {
        self.record("init");
        Ok(())
    }
    async fn status(&self, _r: &ProjectPath) -> Result<Vec<GitFileStatus>, GitError> {
        self.record("status");
        Ok(self.0.lock().unwrap().status.clone())
    }
    async fn stage(&self, _r: &ProjectPath, path: &str) -> Result<(), GitError> {
        self.record(&format!("stage:{path}"));
        Ok(())
    }
    async fn unstage(&self, _r: &ProjectPath, path: &str) -> Result<(), GitError> {
        self.record(&format!("unstage:{path}"));
        Ok(())
    }
    async fn commit(&self, _r: &ProjectPath, message: &str) -> Result<GitCommitInfo, GitError> {
        self.record(&format!("commit:{message}"));
        Ok(GitCommitInfo {
            hash: "abc123".to_owned(),
            summary: message.to_owned(),
        })
    }
    async fn branches(&self, _r: &ProjectPath) -> Result<Vec<String>, GitError> {
        self.record("branches");
        Ok(self.0.lock().unwrap().branches.clone())
    }
    async fn current_branch(&self, _r: &ProjectPath) -> Result<Option<String>, GitError> {
        self.record("current_branch");
        Ok(self.0.lock().unwrap().current.clone())
    }
    async fn checkout(&self, _r: &ProjectPath, branch: &str) -> Result<(), GitError> {
        self.record(&format!("checkout:{branch}"));
        Ok(())
    }
    async fn log(&self, _r: &ProjectPath, _limit: usize) -> Result<Vec<GitCommitInfo>, GitError> {
        self.record("log");
        Ok(Vec::new())
    }
    async fn log_graph(
        &self,
        _r: &ProjectPath,
        _limit: usize,
    ) -> Result<Vec<GraphCommit>, GitError> {
        self.record("log_graph");
        Ok(Vec::new())
    }
    async fn pull(&self, _r: &ProjectPath) -> Result<(), GitError> {
        Ok(())
    }
    async fn push(&self, _r: &ProjectPath) -> Result<(), GitError> {
        Ok(())
    }
}

#[derive(Default, Clone)]
struct SpyBus(Arc<Mutex<Vec<DomainEvent>>>);
impl SpyBus {
    fn events(&self) -> Vec<DomainEvent> {
        self.0.lock().unwrap().clone()
    }
}
impl EventBus for SpyBus {
    fn publish(&self, event: DomainEvent) {
        self.0.lock().unwrap().push(event);
    }
    fn subscribe(&self) -> EventStream {
        Box::new(std::iter::empty())
    }
}

fn pid() -> ProjectId {
    ProjectId::from_uuid(Uuid::from_u128(1))
}
const ROOT: &str = "/home/me/repo";

#[tokio::test]
async fn status_passes_through_to_port() {
    let git = FakeGit::default();
    git.set_status(vec![GitFileStatus {
        path: "a.txt".to_owned(),
        staged: true,
    }]);
    let out = GitStatus::new(Arc::new(git.clone()))
        .execute(GitStatusInput {
            root: ROOT.to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(out.entries.len(), 1);
    assert_eq!(out.entries[0].path, "a.txt");
    assert_eq!(git.calls(), vec!["status"]);
}

#[tokio::test]
async fn status_rejects_non_absolute_root() {
    let git = FakeGit::default();
    let err = GitStatus::new(Arc::new(git.clone()))
        .execute(GitStatusInput {
            root: "relative/path".to_owned(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "INVALID", "got {err:?}");
    assert!(git.calls().is_empty(), "no port call on invalid root");
}

#[tokio::test]
async fn stage_calls_port_with_path() {
    let git = FakeGit::default();
    GitStage::new(Arc::new(git.clone()))
        .execute(GitStagePathInput {
            root: ROOT.to_owned(),
            path: "src/x.rs".to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(git.calls(), vec!["stage:src/x.rs"]);
}

#[tokio::test]
async fn commit_returns_commit_and_publishes_event() {
    let git = FakeGit::default();
    let bus = SpyBus::default();
    let out = GitCommit::new(Arc::new(git.clone()), Arc::new(bus.clone()))
        .execute(GitCommitInput {
            project_id: pid(),
            root: ROOT.to_owned(),
            message: "feat: x".to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(out.commit.hash, "abc123");
    assert_eq!(out.commit.summary, "feat: x");
    assert_eq!(git.calls(), vec!["commit:feat: x"]);
    assert_eq!(
        bus.events(),
        vec![DomainEvent::GitStateChanged { project_id: pid() }]
    );
}

#[tokio::test]
async fn commit_rejects_empty_message_without_touching_port() {
    let git = FakeGit::default();
    let bus = SpyBus::default();
    let err = GitCommit::new(Arc::new(git.clone()), Arc::new(bus.clone()))
        .execute(GitCommitInput {
            project_id: pid(),
            root: ROOT.to_owned(),
            message: "   ".to_owned(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "INVALID", "got {err:?}");
    assert!(git.calls().is_empty(), "no commit attempted");
    assert!(bus.events().is_empty(), "no event on rejected commit");
}

#[tokio::test]
async fn checkout_publishes_event() {
    let git = FakeGit::default();
    let bus = SpyBus::default();
    GitCheckout::new(Arc::new(git.clone()), Arc::new(bus.clone()))
        .execute(GitCheckoutInput {
            project_id: pid(),
            root: ROOT.to_owned(),
            branch: "dev".to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(git.calls(), vec!["checkout:dev"]);
    assert_eq!(
        bus.events(),
        vec![DomainEvent::GitStateChanged { project_id: pid() }]
    );
}

#[tokio::test]
async fn branches_returns_list_and_current() {
    let git = FakeGit::default();
    git.set_branches(
        vec!["main".to_owned(), "dev".to_owned()],
        Some("main".to_owned()),
    );
    let out = GitBranches::new(Arc::new(git.clone()))
        .execute(GitBranchesInput {
            root: ROOT.to_owned(),
        })
        .await
        .unwrap();
    assert_eq!(out.branches, vec!["main", "dev"]);
    assert_eq!(out.current.as_deref(), Some("main"));
    assert_eq!(git.calls(), vec!["branches", "current_branch"]);
}
