use async_trait::async_trait;
use endbasic_core::ast::{ArgSep, Expr, Value, VarType};
use endbasic_core::eval::{CallableMetadata, CallableMetadataBuilder};
use endbasic_core::exec::{self, Command, Machine};
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
#[derive(Clone, Debug)]
pub enum Key {
    
    ArrowDown,
    
    ArrowLeft,
    
    ArrowRight,
    
    ArrowUp,
    
    Backspace,
    
    CarriageReturn,
    
    Char(char),
    
    Eof,
    
    Escape,
    
    
    
    
    Interrupt,
    
    NewLine,
    
    Unknown(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClearType {
    
    All,
    
    CurrentLine,
    
    UntilNewLine,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Position {
    
    pub row: usize,
    
    pub column: usize,
}
impl std::ops::Sub for Position {
    type Output = Self;
    fn sub(self, other: Self) -> Self::Output {
        Position { row: self.row - other.row, column: self.column - other.column }
    }
}
#[async_trait(?Send)]
pub trait Console {
    
    fn clear(&mut self, how: ClearType) -> io::Result<()>;
    
    
    
    fn color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()>;
    
    
    fn enter_alt(&mut self) -> io::Result<()>;
    
    
    fn hide_cursor(&mut self) -> io::Result<()>;
    
    
    fn is_interactive(&self) -> bool;
    
    fn leave_alt(&mut self) -> io::Result<()>;
    
    fn locate(&mut self, pos: Position) -> io::Result<()>;
    
    fn move_within_line(&mut self, off: i16) -> io::Result<()>;
    
    
    
    fn print(&mut self, text: &str) -> io::Result<()>;
    
    async fn read_key(&mut self) -> io::Result<Key>;
    
    fn show_cursor(&mut self) -> io::Result<()>;
    
    
    
    fn size(&self) -> io::Result<Position>;
    
    fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
}
async fn read_line_interactive(
    console: &mut dyn Console,
    prompt: &str,
    previous: &str,
) -> io::Result<String> {
    let mut line = String::from(previous);
    console.clear(ClearType::UntilNewLine)?;
    if !prompt.is_empty() || !line.is_empty() {
        console.write(format!("{}{}", prompt, line).as_bytes())?;
    }
    let width = {
        
        
        let console_size = console.size()?;
        console_size.column - prompt.len()
    };
    
    let mut pos = line.len();
    loop {
        match console.read_key().await? {
            Key::ArrowUp | Key::ArrowDown => {
                
            }
            Key::ArrowLeft => {
                if pos > 0 {
                    console.move_within_line(-1)?;
                    pos -= 1;
                }
            }
            Key::ArrowRight => {
                if pos < line.len() {
                    console.move_within_line(1)?;
                    pos += 1;
                }
            }
            Key::Backspace => {
                if pos > 0 {
                    console.hide_cursor()?;
                    console.move_within_line(-1)?;
                    console.write(line[pos..].as_bytes())?;
                    console.write(&[b' '])?;
                    console.move_within_line(-((line.len() - pos) as i16 + 1))?;
                    console.show_cursor()?;
                    line.remove(pos - 1);
                    pos -= 1;
                }
            }
            Key::CarriageReturn => {
                
                
                
                
                if cfg!(not(target_os = "windows")) {
                    console.write(&[b'\r', b'\n'])?;
                    break;
                }
            }
            Key::Char(ch) => {
                debug_assert!(line.len() < width);
                if line.len() == width - 1 {
                    
                    
                    continue;
                }
                if pos < line.len() {
                    console.hide_cursor()?;
                    console.write(&[ch as u8])?;
                    console.write(line[pos..].as_bytes())?;
                    console.move_within_line(-((line.len() - pos) as i16))?;
                    console.show_cursor()?;
                } else {
                    console.write(&[ch as u8])?;
                }
                line.insert(pos, ch);
                pos += 1;
            }
            Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
            Key::Escape => {
                
            }
            Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
            Key::NewLine => {
                console.write(&[b'\r', b'\n'])?;
                break;
            }
            
            Key::Unknown(_) => (),
        }
    }
    Ok(line)
}
async fn read_line_raw(console: &mut dyn Console) -> io::Result<String> {
    let mut line = String::new();
    loop {
        match console.read_key().await? {
            Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight => (),
            Key::Backspace => {
                if !line.is_empty() {
                    line.pop();
                }
            }
            Key::CarriageReturn => {
                
                
                
                
                if cfg!(not(target_os = "windows")) {
                    break;
                }
            }
            Key::Char(ch) => line.push(ch),
            Key::Escape => (),
            Key::Eof => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")),
            Key::Interrupt => return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C")),
            Key::NewLine => break,
            Key::Unknown(bad_input) => line += &bad_input,
        }
    }
    Ok(line)
}
pub async fn read_line(
    console: &mut dyn Console,
    prompt: &str,
    previous: &str,
) -> io::Result<String> {
    if console.is_interactive() {
        read_line_interactive(console, prompt, previous).await
    } else {
        read_line_raw(console).await
    }
}
pub struct ClsCommand {
    metadata: CallableMetadata,
    console: Rc<RefCell<dyn Console>>,
}
impl ClsCommand {
    
    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("CLS", VarType::Void)
                .with_syntax("")
                .with_category("Console manipulation")
                .with_description("Clears the screen.")
                .build(),
            console,
        })
    }
}
#[async_trait(?Send)]
impl Command for ClsCommand {
    fn metadata(&self) -> &CallableMetadata {
        &self.metadata
    }
    async fn exec(
        &self,
        args: &[(Option<Expr>, ArgSep)],
        _machine: &mut Machine,
    ) -> exec::Result<()> {
        if !args.is_empty() {
            return exec::new_usage_error("CLS takes no arguments");
        }
        self.console.borrow_mut().clear(ClearType::All)?;
        Ok(())
    }
}
pub struct ColorCommand {
    metadata: CallableMetadata,
    console: Rc<RefCell<dyn Console>>,
}
impl ColorCommand {
    
    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("COLOR", VarType::Void)
                .with_syntax("[fg%][, [bg%]]")
                .with_category("Console manipulation")
                .with_description(
                    "Sets the foreground and background colors.
Color numbers are given as ANSI numbers and can be between 0 and 255.  If a color number is not \
specified, then the color is reset to the console's default.  The console default does not \
necessarily match any other color specifiable in the 0 to 255 range, as it might be transparent.",
                )
                .build(),
            console,
        })
    }
}
#[async_trait(?Send)]
impl Command for ColorCommand {
    fn metadata(&self) -> &CallableMetadata {
        &self.metadata
    }
    async fn exec(
        &self,
        args: &[(Option<Expr>, ArgSep)],
        machine: &mut Machine,
    ) -> exec::Result<()> {
        let (fg_expr, bg_expr): (&Option<Expr>, &Option<Expr>) = match args {
            [] => (&None, &None),
            [(fg, ArgSep::End)] => (fg, &None),
            [(fg, ArgSep::Long), (bg, ArgSep::End)] => (fg, bg),
            _ => {
                return exec::new_usage_error(
                    "COLOR takes at most two arguments separated by a comma",
                )
            }
        };
        fn get_color(e: &Option<Expr>, machine: &Machine) -> exec::Result<Option<u8>> {
            match e {
                Some(e) => match e.eval(machine.get_vars(), machine.get_functions())? {
                    Value::Integer(i) if i >= 0 && i <= std::u8::MAX as i32 => Ok(Some(i as u8)),
                    Value::Integer(_) => exec::new_usage_error("Color out of range"),
                    _ => exec::new_usage_error("Color must be an integer"),
                },
                None => Ok(None),
            }
        }
        let fg = get_color(fg_expr, machine)?;
        let bg = get_color(bg_expr, machine)?;
        self.console.borrow_mut().color(fg, bg)?;
        Ok(())
    }
}
pub struct InputCommand {
    metadata: CallableMetadata,
    console: Rc<RefCell<dyn Console>>,
}
impl InputCommand {
    
    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("INPUT", VarType::Void)
                .with_syntax("[\"prompt\"] <;|,> variableref")
                .with_category("Console manipulation")
                .with_description(
                    "Obtains user input from the console.
The first expression to this function must be empty or evaluate to a string, and specifies \
the prompt to print.  If this first argument is followed by the short `;` separator, the \
prompt is extended with a question mark.
The second expression to this function must be a bare variable reference and indicates the \
variable to update with the obtained input.",
                )
                .build(),
            console,
        })
    }
}
#[async_trait(?Send)]
impl Command for InputCommand {
    fn metadata(&self) -> &CallableMetadata {
        &self.metadata
    }
    async fn exec(
        &self,
        args: &[(Option<Expr>, ArgSep)],
        machine: &mut Machine,
    ) -> exec::Result<()> {
        if args.len() != 2 {
            return exec::new_usage_error("INPUT requires two arguments");
        }
        let mut prompt = match &args[0].0 {
            Some(e) => match e.eval(machine.get_vars(), machine.get_functions())? {
                Value::Text(t) => t,
                _ => return exec::new_usage_error("INPUT prompt must be a string"),
            },
            None => "".to_owned(),
        };
        if let ArgSep::Short = args[0].1 {
            prompt += "? ";
        }
        let vref = match &args[1].0 {
            Some(Expr::Symbol(vref)) => vref,
            _ => return exec::new_usage_error("INPUT requires a variable reference"),
        };
        let mut console = self.console.borrow_mut();
        let mut previous_answer = String::new();
        loop {
            match read_line(&mut *console, &prompt, &previous_answer).await {
                Ok(answer) => match Value::parse_as(vref.ref_type(), answer.trim_end()) {
                    Ok(value) => {
                        machine.get_mut_vars().set(vref, value)?;
                        return Ok(());
                    }
                    Err(e) => {
                        console.print(&format!("Retry input: {}", e))?;
                        previous_answer = answer;
                    }
                },
                Err(e) if e.kind() == io::ErrorKind::InvalidData => {
                    console.print(&format!("Retry input: {}", e))?
                }
                Err(e) => return Err(e.into()),
            }
        }
    }
}
pub struct LocateCommand {
    metadata: CallableMetadata,
    console: Rc<RefCell<dyn Console>>,
}
impl LocateCommand {
    
    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("LOCATE", VarType::Void)
                .with_syntax("row%, column%")
                .with_category("Console manipulation")
                .with_description("Moves the cursor to the given position.")
                .build(),
            console,
        })
    }
}
#[async_trait(?Send)]
impl Command for LocateCommand {
    fn metadata(&self) -> &CallableMetadata {
        &self.metadata
    }
    async fn exec(
        &self,
        args: &[(Option<Expr>, ArgSep)],
        machine: &mut Machine,
    ) -> exec::Result<()> {
        if args.len() != 2 {
            return exec::new_usage_error("LOCATE takes two arguments");
        }
        let (row_arg, column_arg) = (&args[0], &args[1]);
        if row_arg.1 != ArgSep::Long {
            return exec::new_usage_error("LOCATE expects arguments separated by a comma");
        }
        debug_assert!(column_arg.1 == ArgSep::End);
        let row = match &row_arg.0 {
            Some(arg) => match arg.eval(machine.get_vars(), machine.get_functions())? {
                Value::Integer(i) => {
                    if i < 0 {
                        return exec::new_usage_error("Row cannot be negative");
                    }
                    i as usize
                }
                _ => return exec::new_usage_error("Row must be an integer"),
            },
            None => return exec::new_usage_error("Row cannot be empty"),
        };
        let column = match &column_arg.0 {
            Some(arg) => match arg.eval(machine.get_vars(), machine.get_functions())? {
                Value::Integer(i) => {
                    if i < 0 {
                        return exec::new_usage_error("Column cannot be negative");
                    }
                    i as usize
                }
                _ => return exec::new_usage_error("Column must be an integer"),
            },
            None => return exec::new_usage_error("Column cannot be empty"),
        };
        self.console.borrow_mut().locate(Position { row, column })?;
        Ok(())
    }
}
pub struct PrintCommand {
    metadata: CallableMetadata,
    console: Rc<RefCell<dyn Console>>,
}
impl PrintCommand {
    
    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("PRINT", VarType::Void)
                .with_syntax("[expr1 [<;|,> .. exprN]]")
                .with_category("Console manipulation")
                .with_description(
                    "Prints a message to the console.
The expressions given as arguments are all evaluated and converted to strings.  Arguments \
separated by the short `;` separator are concatenated with a single space, while arguments \
separated by the long `,` separator are concatenated with a tab character.",
                )
                .build(),
            console,
        })
    }
}
#[async_trait(?Send)]
impl Command for PrintCommand {
    fn metadata(&self) -> &CallableMetadata {
        &self.metadata
    }
    async fn exec(
        &self,
        args: &[(Option<Expr>, ArgSep)],
        machine: &mut Machine,
    ) -> exec::Result<()> {
        let mut text = String::new();
        for arg in args.iter() {
            if let Some(expr) = arg.0.as_ref() {
                text += &expr.eval(machine.get_vars(), machine.get_functions())?.to_string();
            }
            match arg.1 {
                ArgSep::End => break,
                ArgSep::Short => text += " ",
                ArgSep::Long => text += "\t",
            }
        }
        self.console.borrow_mut().print(&text)?;
        Ok(())
    }
}
pub fn add_all(machine: &mut Machine, console: Rc<RefCell<dyn Console>>) {
    machine.add_command(ClsCommand::new(console.clone()));
    machine.add_command(ColorCommand::new(console.clone()));
    machine.add_command(InputCommand::new(console.clone()));
    machine.add_command(LocateCommand::new(console.clone()));
    machine.add_command(PrintCommand::new(console));
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutils::*;
    use futures_lite::future::block_on;
    
    #[must_use]
    struct ReadLineInteractiveTest {
        keys: Vec<Key>,
        prompt: &'static str,
        previous: &'static str,
        exp_line: &'static str,
        exp_output: Vec<CapturedOut>,
    }
    impl Default for ReadLineInteractiveTest {
        
        
        fn default() -> Self {
            Self {
                keys: vec![],
                prompt: "",
                previous: "",
                exp_line: "",
                exp_output: vec![CapturedOut::Clear(ClearType::UntilNewLine)],
            }
        }
    }
    impl ReadLineInteractiveTest {
        
        fn add_key(mut self, key: Key) -> Self {
            self.keys.push(key);
            self
        }
        
        fn add_key_chars(mut self, chars: &'static str) -> Self {
            for ch in chars.chars() {
                self.keys.push(Key::Char(ch));
            }
            self
        }
        
        fn add_output(mut self, output: CapturedOut) -> Self {
            self.exp_output.push(output);
            self
        }
        
        fn add_output_bytes(mut self, bytes: &'static [u8]) -> Self {
            if bytes.is_empty() {
                self.exp_output.push(CapturedOut::Write(vec![]))
            } else {
                for b in bytes.iter() {
                    self.exp_output.push(CapturedOut::Write(vec![*b]))
                }
            }
            self
        }
        
        fn set_line(mut self, line: &'static str) -> Self {
            self.exp_line = line;
            self
        }
        
        fn set_prompt(mut self, prompt: &'static str) -> Self {
            self.prompt = prompt;
            self
        }
        
        fn set_previous(mut self, previous: &'static str) -> Self {
            self.previous = previous;
            self
        }
        
        
        fn accept(mut self) {
            self.keys.push(Key::NewLine);
            self.exp_output.push(CapturedOut::Write(vec![b'\r', b'\n']));
            let mut console = MockConsole::default();
            console.add_input_keys(&self.keys);
            console.set_size(Position { row: 5, column: 15 });
            let line =
                block_on(read_line_interactive(&mut console, self.prompt, self.previous)).unwrap();
            assert_eq!(self.exp_line, &line);
            assert_eq!(self.exp_output.as_slice(), console.captured_out());
        }
    }
    #[test]
    fn test_read_line_interactive_empty() {
        ReadLineInteractiveTest::default().accept();
        ReadLineInteractiveTest::default().add_key(Key::Backspace).accept();
        ReadLineInteractiveTest::default().add_key(Key::ArrowLeft).accept();
        ReadLineInteractiveTest::default().add_key(Key::ArrowRight).accept();
    }
    #[test]
    fn test_read_line_with_prompt() {
        ReadLineInteractiveTest::default()
            .set_prompt("Ready> ")
            .add_output(CapturedOut::Write(b"Ready> ".to_vec()))
            
            .add_key_chars("hello")
            .add_output_bytes(b"hello")
            
            .set_line("hello")
            .accept();
        ReadLineInteractiveTest::default()
            .set_prompt("Cannot delete")
            .add_output(CapturedOut::Write(b"Cannot delete".to_vec()))
            
            .add_key(Key::Backspace)
            .accept();
    }
    #[test]
    fn test_read_line_interactive_trailing_input() {
        ReadLineInteractiveTest::default()
            .add_key_chars("hello")
            .add_output_bytes(b"hello")
            
            .set_line("hello")
            .accept();
        ReadLineInteractiveTest::default()
            .set_previous("123")
            .add_output(CapturedOut::Write(b"123".to_vec()))
            
            .add_key_chars("hello")
            .add_output_bytes(b"hello")
            
            .set_line("123hello")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_middle_input() {
        ReadLineInteractiveTest::default()
            .add_key_chars("some text")
            .add_output_bytes(b"some text")
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowRight)
            .add_output(CapturedOut::MoveWithinLine(1))
            
            .add_key_chars(" ")
            .add_output(CapturedOut::HideCursor)
            .add_output_bytes(b" ")
            .add_output(CapturedOut::Write(b"xt".to_vec()))
            .add_output(CapturedOut::MoveWithinLine(-2))
            .add_output(CapturedOut::ShowCursor)
            
            .add_key_chars(".")
            .add_output(CapturedOut::HideCursor)
            .add_output_bytes(b".")
            .add_output(CapturedOut::Write(b"xt".to_vec()))
            .add_output(CapturedOut::MoveWithinLine(-2))
            .add_output(CapturedOut::ShowCursor)
            
            .set_line("some te .xt")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_trailing_backspace() {
        ReadLineInteractiveTest::default()
            .add_key_chars("bar")
            .add_output_bytes(b"bar")
            
            .add_key(Key::Backspace)
            .add_output(CapturedOut::HideCursor)
            .add_output(CapturedOut::MoveWithinLine(-1))
            .add_output_bytes(b"")
            .add_output_bytes(b" ")
            .add_output(CapturedOut::MoveWithinLine(-1))
            .add_output(CapturedOut::ShowCursor)
            
            .add_key_chars("zar")
            .add_output_bytes(b"zar")
            
            .set_line("bazar")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_middle_backspace() {
        ReadLineInteractiveTest::default()
            .add_key_chars("has a tYpo")
            .add_output_bytes(b"has a tYpo")
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::Backspace)
            .add_output(CapturedOut::HideCursor)
            .add_output(CapturedOut::MoveWithinLine(-1))
            .add_output(CapturedOut::Write(b"po".to_vec()))
            .add_output_bytes(b" ")
            .add_output(CapturedOut::MoveWithinLine(-3))
            .add_output(CapturedOut::ShowCursor)
            
            .add_key_chars("y")
            .add_output(CapturedOut::HideCursor)
            .add_output_bytes(b"y")
            .add_output(CapturedOut::Write(b"po".to_vec()))
            .add_output(CapturedOut::MoveWithinLine(-2))
            .add_output(CapturedOut::ShowCursor)
            
            .set_line("has a typo")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_test_move_bounds() {
        ReadLineInteractiveTest::default()
            .set_previous("12")
            .add_output(CapturedOut::Write(b"12".to_vec()))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_key(Key::ArrowLeft)
            .add_key(Key::ArrowLeft)
            .add_key(Key::ArrowLeft)
            
            .add_key(Key::ArrowRight)
            .add_output(CapturedOut::MoveWithinLine(1))
            
            .add_key(Key::ArrowRight)
            .add_output(CapturedOut::MoveWithinLine(1))
            
            .add_key(Key::ArrowRight)
            .add_key(Key::ArrowRight)
            
            .add_key_chars("3")
            .add_output_bytes(b"3")
            
            .set_line("123")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_horizontal_scrolling_not_implemented() {
        ReadLineInteractiveTest::default()
            .add_key_chars("1234567890123456789")
            .add_output_bytes(b"12345678901234")
            
            .set_line("12345678901234")
            .accept();
        ReadLineInteractiveTest::default()
            .add_key_chars("1234567890123456789")
            .add_output_bytes(b"12345678901234")
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key(Key::ArrowLeft)
            .add_output(CapturedOut::MoveWithinLine(-1))
            
            .add_key_chars("these will all be ignored")
            
            .set_line("12345678901234")
            .accept();
        ReadLineInteractiveTest::default()
            .set_prompt("12345")
            .set_previous("67890")
            .add_output(CapturedOut::Write(b"1234567890".to_vec()))
            
            .add_key_chars("1234567890")
            .add_output_bytes(b"1234")
            
            .set_line("678901234")
            .accept();
    }
    #[test]
    fn test_read_line_interactive_history_not_implemented() {
        ReadLineInteractiveTest::default().add_key(Key::ArrowUp).accept();
        ReadLineInteractiveTest::default().add_key(Key::ArrowDown).accept();
    }
    #[test]
    fn test_read_line_ignored_keys() {
        ReadLineInteractiveTest::default()
            .add_key_chars("not ")
            .add_output_bytes(b"not ")
            
            .add_key(Key::Escape)
            
            .add_key_chars("affected")
            .add_output_bytes(b"affected")
            
            .set_line("not affected")
            .accept();
    }
    #[test]
    fn test_cls_ok() {
        Tester::default().run("CLS").expect_output([CapturedOut::Clear(ClearType::All)]).check();
    }
    #[test]
    fn test_cls_errors() {
        check_stmt_err("CLS takes no arguments", "CLS 1");
    }
    #[test]
    fn test_color_ok() {
        fn t() -> Tester {
            Tester::default()
        }
        t().run("COLOR").expect_output([CapturedOut::Color(None, None)]).check();
        t().run("COLOR ,").expect_output([CapturedOut::Color(None, None)]).check();
        t().run("COLOR 1").expect_output([CapturedOut::Color(Some(1), None)]).check();
        t().run("COLOR 1,").expect_output([CapturedOut::Color(Some(1), None)]).check();
        t().run("COLOR , 1").expect_output([CapturedOut::Color(None, Some(1))]).check();
        t().run("COLOR 10, 5").expect_output([CapturedOut::Color(Some(10), Some(5))]).check();
        t().run("COLOR 0, 0").expect_output([CapturedOut::Color(Some(0), Some(0))]).check();
        t().run("COLOR 255, 255").expect_output([CapturedOut::Color(Some(255), Some(255))]).check();
    }
    #[test]
    fn test_color_errors() {
        check_stmt_err("COLOR takes at most two arguments separated by a comma", "COLOR 1, 2, 3");
        check_stmt_err("Color out of range", "COLOR 1000, 0");
        check_stmt_err("Color out of range", "COLOR 0, 1000");
        check_stmt_err("Color must be an integer", "COLOR TRUE, 0");
        check_stmt_err("Color must be an integer", "COLOR 0, TRUE");
    }
    #[test]
    fn test_input_ok() {
        fn t<V: Into<Value>>(stmt: &str, input: &str, output: &str, var: &str, value: V) {
            Tester::default()
                .add_input_chars(input)
                .run(stmt)
                .expect_prints([output])
                .expect_var(var, value)
                .check();
        }
        t("INPUT ; foo\nPRINT foo", "9\n", "9", "foo", 9);
        t("INPUT ; foo\nPRINT foo", "-9\n", "-9", "foo", -9);
        t("INPUT , bar?\nPRINT bar", "true\n", "TRUE", "bar", true);
        t("INPUT ; foo$\nPRINT foo", "\n", "", "foo", "");
        t(
            "INPUT \"With question mark\"; a$\nPRINT a$",
            "some long text\n",
            "some long text",
            "a",
            "some long text",
        );
        Tester::default()
            .add_input_chars("42\n")
            .run("prompt$ = \"Indirectly without question mark\"\nINPUT prompt$, b\nPRINT b * 2")
            .expect_prints(["84"])
            .expect_var("prompt", "Indirectly without question mark")
            .expect_var("b", 42)
            .check();
    }
    #[test]
    fn test_input_retry() {
        Tester::default()
            .add_input_chars("\ntrue\n")
            .run("INPUT ; b?")
            .expect_prints(["Retry input: Invalid boolean literal "])
            .expect_var("b", true)
            .check();
        Tester::default()
            .add_input_chars("0\ntrue\n")
            .run("INPUT ; b?")
            .expect_prints(["Retry input: Invalid boolean literal 0"])
            .expect_var("b", true)
            .check();
        Tester::default()
            .add_input_chars("\n7\n")
            .run("a = 3\nINPUT ; a")
            .expect_prints(["Retry input: Invalid integer literal "])
            .expect_var("a", 7)
            .check();
        Tester::default()
            .add_input_chars("x\n7\n")
            .run("a = 3\nINPUT ; a")
            .expect_prints(["Retry input: Invalid integer literal x"])
            .expect_var("a", 7)
            .check();
    }
    #[test]
    fn test_input_errors() {
        check_stmt_err("INPUT requires two arguments", "INPUT");
        check_stmt_err("INPUT requires two arguments", "INPUT ; ,");
        check_stmt_err("INPUT requires a variable reference", "INPUT ;");
        check_stmt_err("INPUT prompt must be a string", "INPUT 3 ; a");
        check_stmt_err("INPUT requires a variable reference", "INPUT ; a + 1");
        check_stmt_err("Cannot add Text(\"a\") and Boolean(true)", "INPUT \"a\" + TRUE; b?");
    }
    #[test]
    fn test_locate_ok() {
        Tester::default()
            .run("LOCATE 0, 0")
            .expect_output([CapturedOut::Locate(Position { row: 0, column: 0 })])
            .check();
        Tester::default()
            .run("LOCATE 1000, 2000")
            .expect_output([CapturedOut::Locate(Position { row: 1000, column: 2000 })])
            .check();
    }
    #[test]
    fn test_locate_errors() {
        check_stmt_err("LOCATE takes two arguments", "LOCATE");
        check_stmt_err("LOCATE takes two arguments", "LOCATE 1");
        check_stmt_err("LOCATE takes two arguments", "LOCATE 1, 2, 3");
        check_stmt_err("LOCATE expects arguments separated by a comma", "LOCATE 1; 2");
        check_stmt_err("Row cannot be negative", "LOCATE -1, 2");
        check_stmt_err("Row must be an integer", "LOCATE TRUE, 2");
        check_stmt_err("Row cannot be empty", "LOCATE , 2");
        check_stmt_err("Column cannot be negative", "LOCATE 1, -2");
        check_stmt_err("Column must be an integer", "LOCATE 1, TRUE");
        check_stmt_err("Column cannot be empty", "LOCATE 1,");
    }
    #[test]
    fn test_print_ok() {
        Tester::default().run("PRINT").expect_prints([""]).check();
        Tester::default().run("PRINT ;").expect_prints([" "]).check();
        Tester::default().run("PRINT ,").expect_prints(["\t"]).check();
        Tester::default().run("PRINT ;,;,").expect_prints([" \t \t"]).check();
        Tester::default().run("PRINT 3").expect_prints(["3"]).check();
        Tester::default().run("PRINT 3 = 5").expect_prints(["FALSE"]).check();
        Tester::default()
            .run("PRINT true;123;\"foo bar\"")
            .expect_prints(["TRUE 123 foo bar"])
            .check();
        Tester::default().run("PRINT 6,1;3,5").expect_prints(["6\t1 3\t5"]).check();
        Tester::default()
            .run(r#"word = "foo": PRINT word, word: PRINT word + "s""#)
            .expect_prints(["foo\tfoo", "foos"])
            .expect_var("word", "foo")
            .check();
    }
    #[test]
    fn test_print_errors() {
        
        check_stmt_err("Unexpected value in expression", "PRINT a b");
        check_stmt_err("Cannot add Integer(3) and Boolean(true)", "PRINT 3 + TRUE");
    }
}