quinjet 0.0.71

A fast, live, keyboard-first Git source-control interface for the terminal
use std::cell::Cell;

use super::*;
use crate::git::diff::detect_protocol_from_vars;

#[test]
fn native_inference_and_unknown_terminals_do_not_invoke_queries() {
    for (key, value, expected) in [
        ("TERM", "xterm-kitty", ImageProtocol::Kitty),
        ("TERM_PROGRAM", "ghostty", ImageProtocol::Kitty),
        ("TERM_PROGRAM", "iTerm.app", ImageProtocol::Iterm2),
        ("TERM_PROGRAM", "WezTerm", ImageProtocol::Iterm2),
        ("LC_TERMINAL", "iTerm2", ImageProtocol::Iterm2),
        ("TERM_PROGRAM", "rio", ImageProtocol::Iterm2),
        ("TERM_PROGRAM", "Bobcat", ImageProtocol::Iterm2),
        ("TERM", "xterm-sixel", ImageProtocol::Sixel),
        ("TERM", "xterm-256color", ImageProtocol::Halfblocks),
    ] {
        let inferred = detect_protocol_from_vars([(key, value)]);
        let called = Cell::new(false);
        let (_picker, protocol) = choose_picker(inferred, "", None, || {
            called.set(true);
            Ok(None)
        })
        .unwrap();
        assert!(!called.get(), "{key}={value} must not negotiate");
        assert_eq!(protocol, expected);
        assert_eq!(protocol, inferred);
    }
}

#[test]
fn explicit_overrides_bypass_queries() {
    for (override_value, expected) in [
        ("kitty", ImageProtocol::Kitty),
        ("ITERM2", ImageProtocol::Iterm2),
        ("sixel", ImageProtocol::Sixel),
        ("halfblocks", ImageProtocol::Halfblocks),
        ("unknown", ImageProtocol::Kitty),
    ] {
        let inferred = detect_protocol_from_vars([
            ("TERM", "xterm-kitty"),
            ("QUINJET_IMAGE_PROTOCOL", override_value),
        ]);
        let called = Cell::new(false);
        let (_picker, protocol) = choose_picker(inferred, override_value, None, || {
            called.set(true);
            Ok(None)
        })
        .unwrap();
        assert!(!called.get(), "{override_value} must not negotiate");
        assert_eq!(protocol, expected);
    }
}

#[test]
fn unknown_environment_auto_accepts_kitty_and_sixel_records() {
    let inferred = detect_protocol_from_vars([("TERM", "xterm-256color")]);
    for (bytes, expected) in [
        (b"kitty 9 18\n".as_slice(), ImageProtocol::Kitty),
        (b"sixel 12 24\n".as_slice(), ImageProtocol::Sixel),
    ] {
        for override_value in ["auto", "AUTO"] {
            let called = Cell::new(false);
            let (picker, protocol) = choose_picker(inferred, override_value, None, || {
                called.set(true);
                Ok(PickerRecord::parse(bytes))
            })
            .unwrap();
            assert!(called.get(), "explicit auto must negotiate");
            assert_eq!(protocol, expected);
            assert!(protocol.is_native());
            assert_eq!(
                picker.font_size,
                if expected == ImageProtocol::Kitty {
                    (9, 18)
                } else {
                    (12, 24)
                }
            );
        }
    }
}

#[test]
fn auto_keeps_native_inference_and_uses_queried_font_size() {
    for inferred in [
        ImageProtocol::Kitty,
        ImageProtocol::Iterm2,
        ImageProtocol::Sixel,
    ] {
        let (picker, protocol) = choose_picker(inferred, "auto", None, || {
            Ok(PickerRecord::parse(b"halfblocks 9 18\n"))
        })
        .unwrap();
        assert_eq!(protocol, inferred);
        assert_eq!(picker.font_size, (9, 18));
    }
}

#[test]
fn invalid_and_partial_records_use_inference_and_ioctl_fallback() {
    let invalid: &[&[u8]] = &[
        b"",
        b"kitty 9 18",
        b"kitty 9\n",
        b"kitty 0 18\n",
        b"kitty 9 0\n",
        b"kitty -1 18\n",
        b"kitty 65536 18\n",
        b"kitty 9 invalid\n",
        b"unknown 9 18\n",
        b"kitty 9 18 error\n",
        b"kitty 9 18\nsixel 9 18\n",
        b"kitty 9\n18\n",
        b"kitty 9 18\n\n",
        b"kitty 9 18\r\n",
        b" kitty 9 18\n",
        b"\xff 9 18\n",
    ];
    let size = window();
    for bytes in invalid {
        assert!(PickerRecord::parse(bytes).is_none(), "{bytes:?}");
        for inferred in [ImageProtocol::Halfblocks, ImageProtocol::Kitty] {
            let (picker, protocol) = choose_picker(inferred, "auto", Some(&size), || {
                Ok(PickerRecord::parse(bytes))
            })
            .unwrap();
            assert_eq!(protocol, inferred, "{bytes:?}");
            assert_eq!(picker.font_size, (9, 18), "{bytes:?}");
        }
    }
    let oversized = vec![b' '; MAX_RECORD_BYTES + 1];
    assert!(PickerRecord::parse(&oversized).is_none());
}

#[test]
fn records_round_trip_all_protocols() {
    for protocol in [
        ImageProtocol::Kitty,
        ImageProtocol::Iterm2,
        ImageProtocol::Sixel,
        ImageProtocol::Halfblocks,
    ] {
        let record = PickerRecord {
            protocol,
            font_size: (9, 18),
        };
        let mut bytes = Vec::new();
        record.write(&mut bytes).unwrap();
        assert!(bytes.len() <= MAX_RECORD_BYTES);
        assert_eq!(PickerRecord::parse(&bytes), Some(record));
    }
}

#[test]
fn picker_uses_window_pixel_geometry() {
    let size = window();
    assert_eq!(picker_for_window(Some(&size)).font_size, (9, 18));
}

#[test]
fn picker_falls_back_for_missing_or_invalid_window_geometry() {
    assert_eq!(picker_for_window(None).font_size, (10, 20));
    for (columns, rows, width, height) in [
        (80, 24, 0, 0),
        (0, 24, 720, 432),
        (80, 0, 720, 432),
        (80, 24, 0, 432),
        (80, 24, 720, 0),
        (80, 24, 79, 432),
        (80, 24, 720, 23),
    ] {
        let size = WindowSize {
            columns,
            rows,
            width,
            height,
        };
        assert_eq!(picker_for_window(Some(&size)).font_size, (10, 20));
    }
}

#[test]
fn helper_mode_requires_its_single_internal_argument() {
    for arguments in [
        Vec::new(),
        vec!["--help"],
        vec![HELPER_ARGUMENT, "--help"],
        vec!["tui", HELPER_ARGUMENT],
    ] {
        assert!(!helper_requested(arguments.into_iter().map(OsString::from)));
    }
    let argument = OsString::from(HELPER_ARGUMENT);
    assert!(helper_requested(std::iter::once(argument)));
}

#[test]
fn query_parser_discovers_native_protocols_and_requires_terminating_status() {
    let picker = ImagePicker {
        font_size: (10, 20),
        is_tmux: false,
    };
    for (reply, expected) in [
        (
            b"\x1b[?64;4;28c\x1b[6;18;9t\x1b[0n".as_slice(),
            ImageProtocol::Sixel,
        ),
        (
            b"\x1b[?64;4;28c\x1b_Gi=31;OK\x1b\\\x1b[6;18;9t\x1b[0n".as_slice(),
            ImageProtocol::Kitty,
        ),
    ] {
        let mut output = Vec::new();
        let mut reader = reply;
        let record =
            query_record(&mut reader, &mut output, ImageProtocol::Halfblocks, picker).unwrap();
        assert_eq!(record.protocol, expected);
        assert_eq!(record.font_size, (9, 18));
        assert!(output.ends_with(b"\x1b[5n"));
        let mut incomplete = reply.strip_suffix(b"\x1b[0n").unwrap();
        let error = query_record(
            &mut incomplete,
            &mut Vec::new(),
            ImageProtocol::Halfblocks,
            picker,
        )
        .unwrap_err();
        assert!(error.to_string().contains("status reply"));
    }
}

#[test]
fn query_parser_preserves_native_inference_and_tmux_wrapping() {
    let picker = ImagePicker {
        font_size: (10, 20),
        is_tmux: true,
    };
    let mut reply = b"\x1b[?64;4c\x1b[0n".as_slice();
    let mut output = Vec::new();
    let record = query_record(&mut reply, &mut output, ImageProtocol::Iterm2, picker).unwrap();
    assert_eq!(record.protocol, ImageProtocol::Iterm2);
    assert_eq!(record.font_size, picker.font_size);
    assert!(output.starts_with(b"\x1bPtmux;"));
    assert!(output.ends_with(b"\x1b\x1b[5n\x1b\\"));
}

const fn window() -> WindowSize {
    WindowSize {
        columns: 80,
        rows: 24,
        width: 720,
        height: 432,
    }
}