//! L9 tests for [`ConnectRemote`] with a mock [`RemoteHost`]. The same use case
//! must behave identically whatever the host kind (Liskov), so we drive it with a
//! fake host parameterised by kind + root reachability.

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

use async_trait::async_trait;
use domain::events::DomainEvent;
use domain::ports::{
    DirEntry, EventBus, EventStream, FileSystem, FsError, ProcessSpawner, PtyPort, RemoteError,
    RemoteHost, RemotePath,
};
use domain::{ProjectId, RemoteKind};
use uuid::Uuid;

use application::{ConnectRemote, ConnectRemoteInput};

// --- Fake filesystem (only `exists` matters here) -------------------------

struct FakeFs {
    existing_root: Option<String>,
}
#[async_trait]
impl FileSystem for FakeFs {
    async fn read(&self, p: &RemotePath) -> Result<Vec<u8>, FsError> {
        Err(FsError::NotFound(p.as_str().to_owned()))
    }
    async fn write(&self, _p: &RemotePath, _d: &[u8]) -> Result<(), FsError> {
        Ok(())
    }
    async fn exists(&self, p: &RemotePath) -> Result<bool, FsError> {
        Ok(self.existing_root.as_deref() == Some(p.as_str()))
    }
    async fn create_dir_all(&self, _p: &RemotePath) -> Result<(), FsError> {
        Ok(())
    }
    async fn list(&self, _p: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
        Ok(Vec::new())
    }
    async fn symlink(&self, _s: &RemotePath, _d: &RemotePath) -> Result<(), FsError> {
        Ok(())
    }
}

// --- Fake remote host -----------------------------------------------------

struct FakeHost {
    kind: RemoteKind,
    connect_ok: bool,
    fs: Arc<dyn FileSystem>,
}
impl FakeHost {
    fn make(
        kind: RemoteKind,
        connect_ok: bool,
        existing_root: Option<&str>,
    ) -> Arc<dyn RemoteHost> {
        Arc::new(Self {
            kind,
            connect_ok,
            fs: Arc::new(FakeFs {
                existing_root: existing_root.map(ToOwned::to_owned),
            }),
        })
    }
}
#[async_trait]
impl RemoteHost for FakeHost {
    fn kind(&self) -> RemoteKind {
        self.kind
    }
    async fn connect(&self) -> Result<(), RemoteError> {
        if self.connect_ok {
            Ok(())
        } else {
            Err(RemoteError::Connection("refused".to_owned()))
        }
    }
    fn file_system(&self) -> Arc<dyn FileSystem> {
        Arc::clone(&self.fs)
    }
    fn process_spawner(&self) -> Arc<dyn ProcessSpawner> {
        unreachable!("ConnectRemote does not use the spawner")
    }
    fn pty(&self) -> Arc<dyn PtyPort> {
        unreachable!("ConnectRemote does not use the pty")
    }
}

#[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, e: DomainEvent) {
        self.0.lock().unwrap().push(e);
    }
    fn subscribe(&self) -> EventStream {
        Box::new(std::iter::empty())
    }
}

fn pid() -> ProjectId {
    ProjectId::from_uuid(Uuid::from_u128(1))
}

#[tokio::test]
async fn connect_succeeds_and_emits_event_for_any_host_kind() {
    // Liskov: identical behaviour for Local, Ssh and Wsl hosts.
    for kind in [RemoteKind::Local, RemoteKind::Ssh, RemoteKind::Wsl] {
        let host = FakeHost::make(kind, true, Some("/srv/app"));
        let bus = SpyBus::default();
        let out = ConnectRemote::new(Arc::new(bus.clone()))
            .execute(ConnectRemoteInput {
                host,
                project_id: pid(),
                root: "/srv/app".to_owned(),
            })
            .await
            .unwrap();
        assert_eq!(out.kind, kind);
        assert_eq!(
            bus.events(),
            vec![DomainEvent::RemoteConnected { project_id: pid() }]
        );
    }
}

#[tokio::test]
async fn connect_propagates_connection_failure() {
    let host = FakeHost::make(RemoteKind::Ssh, false, Some("/srv/app"));
    let bus = SpyBus::default();
    let err = ConnectRemote::new(Arc::new(bus.clone()))
        .execute(ConnectRemoteInput {
            host,
            project_id: pid(),
            root: "/srv/app".to_owned(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "REMOTE", "got {err:?}");
    assert!(bus.events().is_empty());
}

#[tokio::test]
async fn connect_fails_when_root_unreachable() {
    let host = FakeHost::make(RemoteKind::Local, true, Some("/other"));
    let bus = SpyBus::default();
    let err = ConnectRemote::new(Arc::new(bus.clone()))
        .execute(ConnectRemoteInput {
            host,
            project_id: pid(),
            root: "/srv/app".to_owned(),
        })
        .await
        .unwrap_err();
    assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
    assert!(bus.events().is_empty(), "no event when root is missing");
}
