//! Terminal lifecycle wrapper that restores state on success, error, and panic.

use std::{
    io,
    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
};

use crossterm::event::{
    DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange,
};
use ratatui::DefaultTerminal;

pub fn initialize() -> io::Result<DefaultTerminal> {
    let terminal = ratatui::try_init()?;
    if let Err(error) = crossterm::execute!(io::stdout(), EnableBracketedPaste, EnableFocusChange) {
        let _ignored = ratatui::try_restore();
        return Err(error);
    }
    Ok(terminal)
}

pub fn restore() -> io::Result<()> {
    let event_modes = crossterm::execute!(io::stdout(), DisableFocusChange, DisableBracketedPaste);
    let terminal = ratatui::try_restore();
    event_modes.and(terminal)
}

pub fn run<T, R, I, C, F>(initialize: I, mut restore: C, operation: F) -> io::Result<R>
where
    I: FnOnce() -> io::Result<T>,
    C: FnMut() -> io::Result<()>,
    F: FnOnce(&mut T) -> io::Result<R>,
{
    let mut terminal = initialize()?;
    let outcome = catch_unwind(AssertUnwindSafe(|| operation(&mut terminal)));
    let restore_result = restore();

    match outcome {
        Ok(Ok(value)) => restore_result.map(|()| value),
        Ok(Err(error)) => Err(error),
        Err(payload) => resume_unwind(payload),
    }
}

#[cfg(test)]
mod tests {
    use std::{
        cell::Cell,
        io::{self, ErrorKind},
        panic,
        rc::Rc,
    };

    use super::run;

    #[test]
    fn restores_after_normal_exit() {
        let restored = Rc::new(Cell::new(false));
        let observed = Rc::clone(&restored);
        let result = run(
            || Ok(()),
            move || {
                observed.set(true);
                Ok(())
            },
            |_| Ok(42),
        );
        assert_eq!(result.expect("normal run"), 42);
        assert!(restored.get());
    }

    #[test]
    fn restores_after_application_error() {
        let restored = Rc::new(Cell::new(false));
        let observed = Rc::clone(&restored);
        let result = run(
            || Ok(()),
            move || {
                observed.set(true);
                Ok(())
            },
            |_| Err::<(), _>(io::Error::other("application failed")),
        );
        assert_eq!(
            result.expect_err("error must be preserved").kind(),
            ErrorKind::Other
        );
        assert!(restored.get());
    }

    #[test]
    fn restores_before_resuming_a_panic() {
        let restored = Rc::new(Cell::new(false));
        let observed = Rc::clone(&restored);
        let outcome = panic::catch_unwind(panic::AssertUnwindSafe(|| {
            let _ = run(
                || Ok(()),
                move || {
                    observed.set(true);
                    Ok(())
                },
                |_| -> io::Result<()> { panic!("application panicked") },
            );
        }));
        assert!(outcome.is_err());
        assert!(restored.get());
    }

    #[test]
    fn initialization_failure_does_not_claim_an_active_terminal() {
        let restored = Rc::new(Cell::new(false));
        let observed = Rc::clone(&restored);
        let result = run(
            || Err::<(), _>(io::Error::new(ErrorKind::NotFound, "no terminal")),
            move || {
                observed.set(true);
                Ok(())
            },
            |_| Ok(()),
        );
        assert_eq!(
            result.expect_err("initialization must fail").kind(),
            ErrorKind::NotFound
        );
        assert!(!restored.get());
    }

    #[test]
    fn restoration_failure_is_reported_after_success() {
        let result = run(
            || Ok(()),
            || Err(io::Error::new(ErrorKind::BrokenPipe, "restore failed")),
            |_| Ok(()),
        );
        assert_eq!(
            result.expect_err("restore must fail").kind(),
            ErrorKind::BrokenPipe
        );
    }
}
