mxrun 0.3.0

Run one build entry across local and remote machines in parallel
use ratatui::{
    Terminal,
    backend::TestBackend,
    layout::Rect,
    style::{Color, Modifier},
};

use crate::{
    app::JobStage,
    model::{MxrunConfig, ResultMirrorPlan},
    runner::BuildPlan,
    ui::{BuildScreen, GridShape, TileLayout, TileStatus, TileViewport},
};

#[test]
fn grid_shape_tiles_one_target() {
    let shape = GridShape::from_count(1);

    assert_eq!(shape.rows(Rect::new(0, 0, 80, 24)).len(), 1);
    assert_eq!(shape.cols(Rect::new(0, 0, 80, 24)).len(), 1);
}

#[test]
fn grid_shape_tiles_four_targets_into_two_by_two() {
    let shape = GridShape::from_count(4);

    assert_eq!(shape.rows(Rect::new(0, 0, 120, 40)).len(), 2);
    assert_eq!(shape.cols(Rect::new(0, 0, 120, 40)).len(), 2);
}

#[test]
fn tile_layout_reserves_status_bar_below_viewport() {
    let layout = TileLayout::new(Rect::new(0, 0, 80, 20)).split();

    assert_eq!(layout.status().height, 1);
    assert_eq!(layout.viewport().height, 19);
    assert_eq!(layout.viewport().y, 0);
    assert_eq!(layout.status().y, 19);
}

#[test]
fn build_screen_creates_one_tile_per_job() {
    let fixture = Fixture::new();
    let plan = fixture.plan();
    let screen = BuildScreen::from_plan(&plan);

    assert_eq!(screen.tiles().len(), 2);
    assert!(screen.tiles()[0].status().title().contains("local"));
    assert!(
        screen.tiles()[1]
            .status()
            .title()
            .contains("192.168.122.122")
    );
}

#[test]
fn build_screen_renders_tiled_terminal_viewports() {
    let fixture = Fixture::new();
    let plan = fixture.plan();
    let screen = BuildScreen::from_plan(&plan);
    let backend = TestBackend::new(120, 30);
    let mut terminal = Terminal::new(backend).expect("test terminal should be created");

    terminal
        .draw(|frame| screen.render(frame))
        .expect("screen should render");
}

#[test]
fn tile_status_renders_purple_bar_with_white_text_when_building() {
    let backend = TestBackend::new(80, 3);
    let mut terminal = Terminal::new(backend).expect("test terminal should be created");

    terminal
        .draw(|frame| {
            TileStatus::from_job(&Fixture::new().plan().jobs()[1]).render(
                frame,
                Rect::new(0, 0, 80, 1),
                false,
            );
        })
        .expect("status should render");

    let run_blue = Color::Rgb(175, 0, 175);

    assert_eq!(
        terminal
            .backend()
            .buffer()
            .cell((55, 0))
            .expect("cell should exist")
            .fg,
        Color::White
    );
    assert_eq!(
        terminal
            .backend()
            .buffer()
            .cell((55, 0))
            .expect("cell should exist")
            .bg,
        run_blue
    );
    assert_eq!(
        terminal
            .backend()
            .buffer()
            .cell((0, 0))
            .expect("cell should exist")
            .fg,
        run_blue
    );
}

#[test]
fn tile_status_colors_match_stage() {
    let pending = TileStatus::from_fixture("Linux", "x86_64", "host", JobStage::Pending);
    assert_eq!(pending.title(), "Linux x86_64 host");

    let success = TileStatus::from_fixture("Linux", "x86_64", "host", JobStage::Success);
    assert_eq!(success.title(), "Linux x86_64 host");
}

#[test]
fn tile_viewport_renders_ansi_colors() {
    let backend = TestBackend::new(40, 6);
    let mut terminal = Terminal::new(backend).expect("test terminal should be created");

    terminal
        .draw(|frame| {
            TileViewport::from_ansi("\u{1b}[1;92mOK\u{1b}[0m", 0).render(
                frame,
                Rect::new(0, 0, 20, 4),
                false,
                JobStage::Building,
            );
        })
        .expect("viewport should render");

    assert!(
        terminal
            .backend()
            .buffer()
            .content()
            .iter()
            .any(|cell| cell.symbol() == "O"
                && cell.fg == Color::LightGreen
                && cell.modifier.contains(Modifier::BOLD))
    );
}

#[test]
fn tile_viewport_scrolls_to_bottom_like_terminal_tail() {
    let viewport = TileViewport::from_ansi("1\n2\n3\n4\n5\n6", 0);

    assert_eq!(viewport.scroll_y(6, Rect::new(0, 0, 20, 3)), 3);
}

#[test]
fn build_screen_renders_finish_popup_when_requested() {
    let fixture = Fixture::new();
    let plan = fixture.plan();
    let states = plan
        .jobs()
        .iter()
        .map(crate::app::JobState::from_job)
        .collect::<Vec<_>>();
    let screen = BuildScreen::from_states(
        &states,
        0,
        &[0, 0],
        Some(crate::app::PopupState::Finished),
        false,
        None,
        None,
    );
    let backend = TestBackend::new(120, 30);
    let mut terminal = Terminal::new(backend).expect("test terminal should be created");

    terminal
        .draw(|frame| screen.render(frame))
        .expect("screen should render popup");

    assert!(
        terminal
            .backend()
            .buffer()
            .content()
            .iter()
            .any(|cell| cell.symbol() == "q")
    );
}

#[test]
fn tile_viewport_marks_active_running_frame_purple() {
    let backend = TestBackend::new(20, 4);
    let mut terminal = Terminal::new(backend).expect("test terminal should be created");

    terminal
        .draw(|frame| {
            TileViewport::from_ansi("line", 0).render(
                frame,
                Rect::new(0, 0, 20, 3),
                true,
                JobStage::Building,
            );
        })
        .expect("viewport should render");

    assert_eq!(
        terminal
            .backend()
            .buffer()
            .cell((0, 0))
            .expect("cell should exist")
            .fg,
        Color::Rgb(175, 0, 175)
    );
}

#[test]
fn tile_viewport_page_up_moves_above_tail() {
    let viewport = TileViewport::from_ansi("1\n2\n3\n4\n5\n6", 10);

    assert_eq!(viewport.scroll_y(6, Rect::new(0, 0, 20, 3)), 0);
}

struct Fixture {
    config: MxrunConfig,
}

impl Fixture {
    fn new() -> Self {
        Self {
            config: MxrunConfig::parse(
                "local\nFreeBSD amd64 192.168.122.122:work/sysinspect-mxrun\n",
            )
            .expect("config should parse"),
        }
    }

    fn plan(&self) -> BuildPlan {
        BuildPlan::new(
            &self.config,
            "dev",
            std::path::Path::new("/tmp/sysinspect"),
            std::path::Path::new("/tmp/logs"),
            "make",
            ResultMirrorPlan::disabled(std::path::PathBuf::from("/tmp/mxrun"), "dev"),
        )
    }
}