Skip to content
//! Widget-level tests: real egui layout, real clicks, no window.
//!
//! These drive the same functions the running client draws, through the
//! AccessKit tree, so a broken button wiring fails here rather than in a
//! screenshot review.

use std::cell::RefCell;

use egui_kittest::Harness;
use egui_kittest::kittest::Queryable as _;
use twice_core::{AppSummary, ContainerName, ContainerState, Host, ImageRef, JobOutcome};

use super::*;
use crate::app::{AutoUpdateChoice, InfrastructureEditor, JobPane, UpdateDialog};
use crate::backend::UiCommand;

fn demo_app() -> AppSummary {
    AppSummary {
        host: Host::parse("writebook.example.com").unwrap(),
        container: ContainerName("writebook".to_owned()),
        image: ImageRef::parse("ghcr.io/basecamp/writebook:1.3.0").unwrap(),
        state: ContainerState::Running,
        status: "Up 6 hours".to_owned(),
        auto_update: Some(true),
        auto_backup: Some(false),
    }
}

/// Renders the table, clicks `label`, and returns the action it produced.
fn click_in_table(label: &str) -> Option<RowAction> {
    let captured = RefCell::new(None);
    let apps = vec![demo_app()];
    let mut harness = Harness::new_ui(|ui| {
        if let Some(action) = apps_table(&apps, ui) {
            *captured.borrow_mut() = Some(action);
        }
    });
    harness.run();
    harness.get_by_label(label).click();
    harness.run();
    drop(harness);
    captured.into_inner()
}

#[test]
fn the_table_renders_every_column_of_an_application() {
    let apps = vec![demo_app()];
    let mut harness = Harness::new_ui(|ui| {
        let _ignored = apps_table(&apps, ui);
    });
    harness.run();

    for expected in [
        "writebook.example.com",
        "running",
        "ghcr.io/basecamp/writebook:1.3.0",
        "Up 6 hours",
        "on",
    ] {
        assert!(
            harness.query_by_label(expected).is_some(),
            "the table did not render {expected:?}"
        );
    }
}

#[test]
fn an_empty_inventory_explains_itself_instead_of_rendering_a_blank_grid() {
    let mut harness = Harness::new_ui(|ui| {
        let _ignored = apps_table(&[], ui);
    });
    harness.run();
    assert!(
        harness
            .query_by_label_contains("No once-managed applications")
            .is_some()
    );
}

#[test]
fn clicking_start_asks_to_start_that_host() {
    match click_in_table("Start") {
        Some(RowAction::Start(host)) => assert_eq!(host.as_str(), "writebook.example.com"),
        other => panic!("expected a start action, got {other:?}"),
    }
}

#[test]
fn clicking_stop_asks_to_stop_that_host() {
    match click_in_table("Stop") {
        Some(RowAction::Stop(host)) => assert_eq!(host.as_str(), "writebook.example.com"),
        other => panic!("expected a stop action, got {other:?}"),
    }
}

#[test]
fn clicking_update_opens_the_dialog_prefilled_with_the_running_image() {
    match click_in_table("Update image…") {
        Some(RowAction::OpenUpdate(host, image)) => {
            assert_eq!(host.as_str(), "writebook.example.com");
            assert_eq!(image, "ghcr.io/basecamp/writebook:1.3.0");
        }
        other => panic!("expected an update action, got {other:?}"),
    }
}

#[test]
fn no_button_press_produces_no_action() {
    let apps = vec![demo_app()];
    let captured = RefCell::new(None);
    let mut harness = Harness::new_ui(|ui| {
        if let Some(action) = apps_table(&apps, ui) {
            *captured.borrow_mut() = Some(action);
        }
    });
    harness.run();
    drop(harness);
    assert!(captured.into_inner().is_none());
}

fn dialog(image: &str) -> UpdateDialog {
    UpdateDialog {
        host: Host::parse("writebook.example.com").unwrap(),
        image: image.to_owned(),
        auto_update: AutoUpdateChoice::Disable,
        error: None,
        closed: false,
    }
}

/// Renders the dialog, presses the confirm button, and reports what came out.
fn confirm_dialog(image: &str) -> (Option<UiCommand>, Option<String>) {
    let state = RefCell::new((dialog(image), None));
    let mut harness = Harness::new_ui(|ui| {
        let ctx = ui.ctx().clone();
        let mut borrowed = state.borrow_mut();
        let (dialog, command) = &mut *borrowed;
        if let Some(produced) = update_dialog(dialog, &ctx) {
            *command = Some(produced);
        }
    });
    harness.run();
    harness.get_by_label("Roll onto this image").click();
    harness.run();
    drop(harness);
    let (dialog, command) = state.into_inner();
    (command, dialog.error)
}

#[test]
fn confirming_a_valid_image_emits_an_update_command_with_the_chosen_flag() {
    let (command, error) = confirm_dialog("ghcr.io/basecamp/writebook:1.4.0");
    assert!(error.is_none(), "unexpected validation error: {error:?}");
    match command {
        Some(UiCommand::UpdateImage { host, request }) => {
            assert_eq!(host.as_str(), "writebook.example.com");
            assert_eq!(request.image.as_str(), "ghcr.io/basecamp/writebook:1.4.0");
            assert_eq!(request.auto_update, Some(false));
        }
        other => panic!("expected an update command, got {other:?}"),
    }
}

#[test]
fn confirming_a_flag_shaped_image_shows_an_error_and_sends_nothing() {
    let (command, error) = confirm_dialog("--env=EVIL");
    assert!(command.is_none(), "a hostile image reached the server");
    assert!(
        error.is_some_and(|message| message.contains("invalid image reference")),
        "the operator was not told why the image was rejected"
    );
}

#[test]
fn applying_infrastructure_emits_the_typed_document() {
    let editor = InfrastructureEditor {
        document: concat!(
            "host = \"writebook.example.com\"\n",
            "source = \"ghcr.io/basecamp/writebook\"\n",
            "version = \"1.4.0\"\n",
        )
        .to_owned(),
        error: None,
    };
    let state = RefCell::new((editor, None));
    let mut harness = Harness::new_ui(|ui| {
        let mut borrowed = state.borrow_mut();
        let (editor, command) = &mut *borrowed;
        if let Some(produced) = infrastructure_editor(editor, ui) {
            *command = Some(produced);
        }
    });
    harness.run();
    harness.get_by_label("Apply infrastructure").click();
    harness.run();
    drop(harness);

    let (editor, command) = state.into_inner();
    assert!(editor.error.is_none());
    match command {
        Some(UiCommand::ApplyInfrastructure(spec)) => {
            assert_eq!(spec.host.as_str(), "writebook.example.com");
            assert_eq!(spec.source.as_str(), "ghcr.io/basecamp/writebook");
            assert_eq!(spec.version.as_str(), "1.4.0");
        }
        other => panic!("expected infrastructure command, got {other:?}"),
    }
}

#[test]
fn invalid_infrastructure_stays_in_the_editor() {
    let editor = InfrastructureEditor {
        document: "host = \"bad host\"\nsource = \"repo/app\"\nversion = \"1\"\n".to_owned(),
        error: None,
    };
    let state = RefCell::new(editor);
    let mut harness = Harness::new_ui(|ui| {
        let _ignored = infrastructure_editor(&mut state.borrow_mut(), ui);
    });
    harness.run();
    harness.get_by_label("Apply infrastructure").click();
    harness.run();
    drop(harness);
    assert!(state.into_inner().error.is_some());
}

#[test]
fn a_failed_job_shows_its_exit_code_and_output() {
    let job = JobPane {
        title: "update image · writebook.example.com".to_owned(),
        lines: vec!["Error: no application found".to_owned()],
        outcome: Some(JobOutcome::Failed {
            exit_code: Some(1),
            message: "once exited with exit status: 1".to_owned(),
        }),
    };
    let mut harness = Harness::new_ui(|ui| job_pane(&job, ui));
    harness.run();

    assert!(harness.query_by_label_contains("failed (1)").is_some());
    assert!(
        harness
            .query_by_label("Error: no application found")
            .is_some()
    );
}

#[test]
fn a_succeeded_job_says_so() {
    let job = JobPane {
        title: "stop · writebook.example.com".to_owned(),
        lines: Vec::new(),
        outcome: Some(JobOutcome::Succeeded),
    };
    let mut harness = Harness::new_ui(|ui| job_pane(&job, ui));
    harness.run();
    assert!(harness.query_by_label("succeeded").is_some());
}