systemd-manager-tui 1.3.0

TUI for managing systemd services
use super::*;
use crate::domain::connection_type::ConnectionType;
use crate::test_support::{FakeRepository, service};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use std::sync::mpsc;

fn request_context(name: &str) -> ServiceRequestContext {
    ServiceRequestContext {
        connection: ConnectionType::System,
        service_name: name.to_string(),
    }
}

fn log_with(repository: FakeRepository) -> (ServiceLog, mpsc::Receiver<AppEvent>) {
    let (sender, receiver) = mpsc::channel();
    let manager = ServicesManager::new(Box::new(repository));
    (
        ServiceLog::new(sender, Rc::new(RefCell::new(manager))),
        receiver,
    )
}

fn rendered_text(log: &mut ServiceLog, width: u16, height: u16) -> String {
    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|frame| log.render(frame, frame.area()))
        .unwrap();
    terminal
        .backend()
        .buffer()
        .content()
        .iter()
        .map(|cell| cell.symbol())
        .collect()
}

#[test]
fn empty_log_renders_loading_state() {
    let (mut log, _receiver) = log_with(FakeRepository::default());

    assert!(rendered_text(&mut log, 40, 8).contains("Loading..."));
}

#[test]
fn renders_service_name_and_latest_log_lines() {
    let (mut log, _receiver) = log_with(FakeRepository::default());
    log.update("demo.service".into(), "first\nsecond\nthird".into());

    let screen = rendered_text(&mut log, 40, 5);

    assert!(screen.contains("demo.service log"));
    assert!(screen.contains("first"));
    assert!(screen.contains("third"));
}

#[test]
fn fetch_dispatches_log_update_event() {
    let fake = FakeRepository::with_content("journal output", "", 0);
    let observer = fake.clone();
    let (mut log, receiver) = log_with(fake);
    let unit = service("demo.service", "active", "enabled");

    log.fetch_log_and_dispatch(request_context(unit.name()));

    assert!(matches!(
        receiver.recv().unwrap(),
        AppEvent::Action(Actions::UpdateLog(context, content))
            if context == request_context("demo.service") && content == "journal output"
    ));
    assert_eq!(observer.calls(), ["log:demo.service"]);
}

#[test]
fn log_failure_is_reported_without_panicking() {
    let fake = FakeRepository::default();
    fake.fail("log");
    let (mut log, receiver) = log_with(fake);

    log.fetch_log_and_dispatch(request_context("broken.service"));

    assert!(matches!(
        receiver.recv().unwrap(),
        AppEvent::Error(message)
            if message.contains("broken.service") && message.contains("log failed")
    ));
}

#[test]
fn auto_refresh_changes_border_and_shortcut_label() {
    let (mut log, _receiver) = log_with(FakeRepository::default());

    log.set_auto_refresh(true);

    assert!(matches!(log.border_color, BorderColor::Orange));
    let shortcuts = log.shortcuts();
    assert!(
        shortcuts
            .iter()
            .any(|line| line.to_string().contains("Disable auto-refresh"))
    );
    log.set_auto_refresh(false);
}

#[test]
fn auto_refresh_state_is_visible_in_rendered_border_color() {
    let (mut log, _receiver) = log_with(FakeRepository::default());
    log.update("demo.service".into(), "journal output".into());
    log.set_auto_refresh(true);
    let backend = TestBackend::new(40, 5);
    let mut terminal = Terminal::new(backend).unwrap();

    terminal
        .draw(|frame| log.render(frame, frame.area()))
        .unwrap();

    assert_eq!(
        terminal.backend().buffer().content()[0].fg,
        Color::Rgb(255, 165, 0)
    );
    log.set_auto_refresh(false);
}

#[test]
fn auto_refresh_owns_exactly_one_stoppable_worker() {
    let (mut log, _receiver) = log_with(FakeRepository::default());
    let toggle = KeyEvent::new(KeyCode::Char('a'), crossterm::event::KeyModifiers::NONE);

    log.on_key_event(toggle);
    assert!(log.auto_refresh_worker.is_some());
    assert!(*log.auto_refresh.lock().unwrap());

    log.on_key_event(toggle);
    assert!(log.auto_refresh_worker.is_none());
    assert!(!*log.auto_refresh.lock().unwrap());
}

#[test]
fn scrolling_changes_the_visible_log_window() {
    let (mut log, _receiver) = log_with(FakeRepository::default());
    log.update(
        "demo.service".into(),
        "line1\nline2\nline3\nline4\nline5\nline6".into(),
    );

    let latest = rendered_text(&mut log, 40, 4);
    assert!(latest.contains("line5"));
    assert!(latest.contains("line6"));
    assert!(!latest.contains("line4"));

    log.on_key_event(KeyEvent::new(
        KeyCode::Up,
        crossterm::event::KeyModifiers::NONE,
    ));
    let older = rendered_text(&mut log, 40, 4);
    assert!(older.contains("line4"));
    assert!(older.contains("line5"));
    assert!(!older.contains("line6"));
}

#[test]
fn leaving_log_resets_state_and_returns_to_list() {
    let (mut log, receiver) = log_with(FakeRepository::default());
    log.update("demo.service".into(), "journal output".into());
    log.set_auto_refresh(true);
    log.scroll = 10;

    log.on_key_event(KeyEvent::new(
        KeyCode::Esc,
        crossterm::event::KeyModifiers::NONE,
    ));

    assert!(log.log.is_empty());
    assert_eq!(log.scroll, 0);
    assert!(!*log.auto_refresh.lock().unwrap());
    assert!(matches!(
        receiver.recv().unwrap(),
        AppEvent::Action(Actions::GoList)
    ));
}

#[test]
fn switching_from_log_resets_content_and_opens_details() {
    let (mut log, receiver) = log_with(FakeRepository::default());
    log.update("demo.service".into(), "journal output".into());
    log.scroll = 3;

    log.on_key_event(KeyEvent::new(
        KeyCode::Right,
        crossterm::event::KeyModifiers::NONE,
    ));

    assert!(log.log.is_empty());
    assert_eq!(log.scroll, 0);
    assert!(matches!(
        receiver.recv().unwrap(),
        AppEvent::Action(Actions::GoDetails)
    ));
}

#[test]
fn downward_scrolling_saturates_at_zero() {
    let (mut log, _receiver) = log_with(FakeRepository::default());

    log.on_key_event(KeyEvent::new(
        KeyCode::Down,
        crossterm::event::KeyModifiers::NONE,
    ));
    log.on_key_event(KeyEvent::new(
        KeyCode::PageDown,
        crossterm::event::KeyModifiers::NONE,
    ));

    assert_eq!(log.scroll, 0);
}

#[test]
fn long_log_lines_wrap_to_available_width() {
    let (mut log, _receiver) = log_with(FakeRepository::default());
    log.update("demo.service".into(), "abcdefghijklmnopqrst".into());

    let screen = rendered_text(&mut log, 12, 4);

    assert!(screen.contains("abcdefghij"));
    assert!(screen.contains("klmnopqrst"));
}