use super::{BuildError, BuildResult};
use crate::{
    code::{
        execute::LanguageSnippetExecutor,
        snippet::{
            ExternalFile, Highlight, HighlightContext, HighlightGroup, HighlightMutator, HighlightedLine, PtyArgs,
            Snippet, SnippetExecArgs, SnippetExecution, SnippetExecutorSpec, SnippetLanguage, SnippetLine,
            SnippetParser, SnippetRepr, SnippetSplitter,
        },
    },
    markdown::elements::SourcePosition,
    presentation::builder::{PresentationBuilder, error::InvalidPresentation},
    render::{
        operation::{AsRenderOperations, RenderAsyncStartPolicy, RenderOperation},
        properties::WindowSize,
    },
    theme::{Alignment, CodeBlockStyle},
    third_party::ThirdPartyRenderRequest,
    ui::execution::{
        RunAcquireTerminalSnippet, RunImageSnippet, SnippetExecutionDisabledOperation, SnippetOutputOperation,
        disabled::ExecutionType,
        output::{ExecIndicator, ExecIndicatorStyle, RunSnippetTrigger, SnippetHandle, WrappedSnippetHandle},
        pty::{PtySnippetHandle, PtySnippetOutputOperation, RunPtySnippetTrigger},
        validator::ValidateSnippetOperation,
    },
};
use itertools::Itertools;
use std::{cell::RefCell, rc::Rc};

impl PresentationBuilder<'_, '_> {
    pub(crate) fn push_code(&mut self, info: String, code: String, source_position: SourcePosition) -> BuildResult {
        let mut snippet = SnippetParser::parse(info, code)
            .map_err(|e| self.invalid_presentation(source_position, InvalidPresentation::Snippet(e.to_string())))?;
        if matches!(snippet.language, SnippetLanguage::File) {
            snippet = self.load_external_snippet(snippet, source_position)?;
        }
        if self.theme.code.line_numbers {
            snippet.attributes.line_numbers = true;
        }
        if self.options.auto_render_languages.contains(&snippet.language) {
            snippet.attributes.execution = SnippetExecution::Render;
        }
        // Ids can only be used in `+exec` snippets.
        if snippet.attributes.id.is_some()
            && !matches!(
                snippet.attributes.execution,
                SnippetExecution::Exec(SnippetExecArgs { repr: SnippetRepr::SnippetOutput, .. })
            )
        {
            return Err(self.invalid_presentation(source_position, InvalidPresentation::SnippetIdNonExec));
        }

        self.push_differ(snippet.contents.clone());
        // Redraw slide if attributes change
        self.push_differ(format!("{:?}", snippet.attributes));

        if let Some(spec) = &snippet.attributes.validate {
            // Take the execution spec if we use the default one, otherwise use the one provided.
            // This allows using `rust +exec:foo +validate` and using `foo` as the executor.
            let spec = match (spec, &snippet.attributes.execution) {
                (SnippetExecutorSpec::Default, SnippetExecution::Exec(args)) => &args.spec,
                _ => spec,
            };
            let executor = self.snippet_executor.language_executor(&snippet.language, spec)?;
            self.push_validator(&snippet, &executor);
        }

        match &snippet.attributes.execution {
            SnippetExecution::None => {
                self.push_code_lines(&snippet);
            }
            SnippetExecution::Render => {
                self.push_rendered_code(snippet, source_position)?;
            }
            SnippetExecution::Exec(args) if !self.is_execution_allowed(args) => {
                self.push_code_lines(&snippet);
                let mut exec_type = match args.repr {
                    SnippetRepr::Image => ExecutionType::Image,
                    SnippetRepr::ExecReplace => ExecutionType::ExecReplace,
                    SnippetRepr::SnippetOutput | SnippetRepr::AcquireTerminal => ExecutionType::Execute,
                };
                if args.auto {
                    exec_type = ExecutionType::ExecReplace;
                }
                self.push_execution_disabled_operation(exec_type);
            }
            SnippetExecution::Exec(args) => {
                let executor = self.snippet_executor.language_executor(&snippet.language, &args.spec)?;
                match args.repr {
                    SnippetRepr::Image => {
                        self.push_code_as_image(snippet, executor)?;
                    }
                    SnippetRepr::ExecReplace => match &args.pty {
                        Some(args) => {
                            let args = args.clone();
                            self.push_replace_pty_code_execution(snippet, executor, args)?;
                        }
                        None => {
                            self.push_replace_code_execution(snippet, executor)?;
                        }
                    },
                    SnippetRepr::AcquireTerminal => {
                        let block_length = self.push_code_lines(&snippet);
                        self.push_acquire_terminal_execution(snippet, block_length, executor)?;
                    }
                    SnippetRepr::SnippetOutput => {
                        let block_length = self.push_code_lines(&snippet);
                        let policy = if args.auto {
                            RenderAsyncStartPolicy::Automatic
                        } else {
                            RenderAsyncStartPolicy::OnDemand
                        };
                        let handle: WrappedSnippetHandle = match &args.pty {
                            Some(args) => PtySnippetHandle::new(snippet.clone(), executor, policy, args.clone()).into(),
                            None => SnippetHandle::new(snippet.clone(), executor, policy).into(),
                        };
                        self.chunk_operations.push(RenderOperation::RenderAsync(handle.build_trigger().into()));

                        let alignment = self.code_style(&snippet).alignment;
                        self.push_indicator(handle.clone(), block_length, alignment);
                        match snippet.attributes.id.clone() {
                            Some(id) => {
                                if self.executable_snippets.insert(id.clone(), handle).is_some() {
                                    return Err(self.invalid_presentation(
                                        source_position,
                                        InvalidPresentation::SnippetAlreadyExists(id),
                                    ));
                                }
                            }
                            None => {
                                self.push_line_break();
                                match handle {
                                    WrappedSnippetHandle::Normal(handle) => {
                                        self.push_code_execution(block_length, handle, alignment)?;
                                    }
                                    WrappedSnippetHandle::Pty(handle) => self.push_pty_code_execution(handle)?,
                                }
                            }
                        }
                    }
                }
            }
        };
        Ok(())
    }

    pub(crate) fn push_detached_code_execution(&mut self, handle: WrappedSnippetHandle) -> BuildResult {
        match handle {
            WrappedSnippetHandle::Normal(handle) => {
                let alignment = self.code_style(&handle.snippet()).alignment;
                self.push_code_execution(0, handle, alignment)
            }
            WrappedSnippetHandle::Pty(handle) => self.push_pty_code_execution(handle),
        }
    }

    fn is_execution_allowed(&self, args: &SnippetExecArgs) -> bool {
        match args.repr {
            SnippetRepr::SnippetOutput | SnippetRepr::AcquireTerminal => self.options.enable_snippet_execution,
            SnippetRepr::Image | SnippetRepr::ExecReplace => self.options.enable_snippet_execution_replace,
        }
    }

    fn push_code_lines(&mut self, snippet: &Snippet) -> u16 {
        let lines = SnippetSplitter::new(&self.theme.code, self.snippet_executor.hidden_line_prefix(&snippet.language))
            .split(snippet);
        let block_length = lines.iter().map(|line| line.width()).max().unwrap_or(0) * self.slide_font_size() as usize;
        let block_length = block_length as u16;
        let (lines, context) = self.highlight_lines(snippet, lines, block_length);
        for line in lines {
            self.chunk_operations.push(RenderOperation::RenderDynamic(Rc::new(line)));
        }
        self.chunk_operations.push(RenderOperation::SetColors(self.theme.default_style.style.colors));
        if self.options.allow_mutations && context.borrow().groups.len() > 1 {
            self.chunk_mutators.push(Box::new(HighlightMutator::new(context)));
        }
        block_length
    }

    fn push_replace_code_execution(&mut self, snippet: Snippet, executor: LanguageSnippetExecutor) -> BuildResult {
        let alignment = match self.code_style(&snippet).alignment {
            // If we're replacing the snippet output, we have center alignment and no background, use
            // center alignment but without any margins and minimum sizes so we truly center the output.
            Alignment::Center { .. } if snippet.attributes.no_background => {
                Alignment::Center { minimum_margin: Default::default(), minimum_size: 0 }
            }
            other => other,
        };
        let handle = SnippetHandle::new(snippet, executor, RenderAsyncStartPolicy::Automatic);
        self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(RunSnippetTrigger::new(handle.clone()))));
        self.push_code_execution(0, handle, alignment)
    }

    fn push_replace_pty_code_execution(
        &mut self,
        snippet: Snippet,
        executor: LanguageSnippetExecutor,
        args: PtyArgs,
    ) -> BuildResult {
        let standby = args.standby;
        let policy = if standby { RenderAsyncStartPolicy::OnDemand } else { RenderAsyncStartPolicy::Automatic };
        let handle = PtySnippetHandle::new(snippet, executor, policy, args);
        // If we're using standby mode we still need a trigger
        if standby {
            self.chunk_operations
                .push(RenderOperation::RenderAsync(WrappedSnippetHandle::from(handle.clone()).build_trigger().into()));
        }
        self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(RunPtySnippetTrigger::new(handle.clone()))));
        self.push_pty_code_execution(handle)
    }

    fn load_external_snippet(
        &mut self,
        mut code: Snippet,
        source_position: SourcePosition,
    ) -> Result<Snippet, BuildError> {
        let file: ExternalFile = serde_yaml::from_str(&code.contents)
            .map_err(|e| self.invalid_presentation(source_position, InvalidPresentation::Snippet(e.to_string())))?;
        let path = file.path;
        let base_path = self.resource_base_path();
        let contents = self.resources.external_text_file(&path, &base_path).map_err(|e| {
            self.invalid_presentation(
                source_position,
                InvalidPresentation::Snippet(format!("failed to load snippet {path:?}: {e}")),
            )
        })?;
        code.language = file.language;
        code.contents = Self::filter_lines(contents, file.start_line, file.end_line);
        Ok(code)
    }

    fn filter_lines(code: String, start: Option<usize>, end: Option<usize>) -> String {
        let start = start.map(|s| s.saturating_sub(1));
        match (start, end) {
            (None, None) => code,
            (None, Some(end)) => code.lines().take(end).join("\n"),
            (Some(start), None) => code.lines().skip(start).join("\n"),
            (Some(start), Some(end)) => code.lines().skip(start).take(end.saturating_sub(start)).join("\n"),
        }
    }

    fn push_rendered_code(&mut self, code: Snippet, source_position: SourcePosition) -> BuildResult {
        let Snippet { contents, language, attributes } = code;
        let request = match language {
            SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
            SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
            SnippetLanguage::Mermaid => ThirdPartyRenderRequest::Mermaid(contents, self.theme.mermaid.clone()),
            SnippetLanguage::D2 => ThirdPartyRenderRequest::D2(contents, self.theme.d2.clone()),
            _ => {
                return Err(self.invalid_presentation(
                    source_position,
                    InvalidPresentation::Snippet(format!("language {language:?} doesn't support rendering")),
                ));
            }
        };
        let operation = self.third_party.render(request, &self.theme, attributes.width)?;
        self.chunk_operations.push(operation);
        Ok(())
    }

    fn highlight_lines(
        &self,
        code: &Snippet,
        lines: Vec<SnippetLine>,
        block_length: u16,
    ) -> (Vec<HighlightedLine>, Rc<RefCell<HighlightContext>>) {
        let mut code_highlighter = self.highlighter.language_highlighter(&code.language);
        let style = self.code_style(code);
        let block_length = self.theme.code.alignment.adjust_size(block_length);
        let font_size = self.slide_font_size();
        let dim_style = {
            let mut highlighter = self.highlighter.language_highlighter(&SnippetLanguage::Rust);
            highlighter.style_line("//", &style).0.first().expect("no styles").style.size(font_size)
        };
        let groups = match self.options.allow_mutations {
            true => code.attributes.highlight_groups.clone(),
            false => vec![HighlightGroup::new(vec![Highlight::All])],
        };
        let context =
            Rc::new(RefCell::new(HighlightContext { groups, current: 0, block_length, alignment: style.alignment }));

        let mut output = Vec::new();
        for line in lines.into_iter() {
            let prefix = line.dim_prefix(&dim_style);
            let highlighted = line.highlight(&mut code_highlighter, &style, font_size);
            let not_highlighted = line.dim(&dim_style);
            let line_number = line.line_number;
            let context = context.clone();
            output.push(HighlightedLine {
                prefix,
                right_padding_length: line.right_padding_length * font_size as u16,
                highlighted,
                not_highlighted,
                line_number,
                context,
                block_color: dim_style.colors.background,
            });
        }
        (output, context)
    }

    fn code_style(&self, snippet: &Snippet) -> CodeBlockStyle {
        let mut style = self.theme.code.clone();
        if snippet.attributes.no_background {
            style.background = false;
        }
        style
    }

    fn push_execution_disabled_operation(&mut self, exec_type: ExecutionType) {
        let policy = match exec_type {
            ExecutionType::ExecReplace | ExecutionType::Image => RenderAsyncStartPolicy::Automatic,
            ExecutionType::Execute => RenderAsyncStartPolicy::OnDemand,
        };
        let operation = SnippetExecutionDisabledOperation::new(
            self.theme.execution_output.status.failure_style,
            self.theme.code.alignment,
            policy,
            exec_type,
        );
        self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
    }

    fn push_code_as_image(&mut self, snippet: Snippet, executor: LanguageSnippetExecutor) -> BuildResult {
        let operation =
            RunImageSnippet::new(snippet, executor, self.image_registry.clone(), self.theme.execution_output.status);
        let operation = RenderOperation::RenderAsync(Rc::new(operation));
        self.chunk_operations.push(operation);
        Ok(())
    }

    fn push_acquire_terminal_execution(
        &mut self,
        snippet: Snippet,
        block_length: u16,
        executor: LanguageSnippetExecutor,
    ) -> BuildResult {
        let block_length = self.theme.code.alignment.adjust_size(block_length);
        let operation = RunAcquireTerminalSnippet::new(
            snippet,
            executor,
            self.theme.execution_output.status,
            block_length,
            self.slide_font_size(),
        );
        let operation = RenderOperation::RenderAsync(Rc::new(operation));
        self.chunk_operations.push(operation);
        Ok(())
    }

    fn push_indicator<T: Into<WrappedSnippetHandle>>(&mut self, handle: T, block_length: u16, alignment: Alignment) {
        let style = ExecIndicatorStyle {
            theme: self.theme.execution_output.status,
            block_length,
            font_size: self.slide_font_size(),
            alignment,
        };
        let indicator = Rc::new(ExecIndicator::new(handle, style));
        self.chunk_operations.push(RenderOperation::RenderDynamic(indicator));
    }

    fn push_code_execution(&mut self, block_length: u16, handle: SnippetHandle, alignment: Alignment) -> BuildResult {
        let snippet = handle.snippet();
        let default_colors = self.theme.default_style.style.colors;
        let mut execution_output_style = self.theme.execution_output.clone();
        if snippet.attributes.no_background {
            execution_output_style.style.colors.background = None;
            execution_output_style.padding = Default::default();
        }
        let operation = SnippetOutputOperation::new(
            handle,
            default_colors,
            execution_output_style,
            block_length,
            alignment,
            self.slide_font_size(),
        );
        let operation = RenderOperation::RenderDynamic(Rc::new(operation));
        self.chunk_operations.push(operation);
        Ok(())
    }

    fn push_pty_code_execution(&mut self, handle: PtySnippetHandle) -> BuildResult {
        let snippet = handle.snippet();
        let mut style = self.theme.pty_output.clone();
        if snippet.attributes.no_background {
            style.style.colors.background = None;
        }
        let operation = PtySnippetOutputOperation::new(handle, style, self.slide_font_size());
        let operation = RenderOperation::RenderDynamic(Rc::new(operation));
        self.chunk_operations.push(operation);
        Ok(())
    }

    fn push_differ(&mut self, text: String) {
        self.chunk_operations.push(RenderOperation::RenderDynamic(Rc::new(Differ(text))));
    }

    fn push_validator(&mut self, snippet: &Snippet, executor: &LanguageSnippetExecutor) {
        if !self.options.validate_snippets {
            return;
        }
        let operation = ValidateSnippetOperation::new(snippet.clone(), executor.clone());
        self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
    }
}

#[derive(Debug)]
struct Differ(String);

impl AsRenderOperations for Differ {
    fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
        Vec::new()
    }

    fn diffable_content(&self) -> Option<&str> {
        Some(&self.0)
    }
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
    use super::*;
    use crate::{
        markdown::text_style::Color,
        presentation::builder::utils::{RunAsyncRendersPolicy, Test},
        theme::raw,
    };
    use rstest::rstest;
    use std::fs;

    #[rstest]
    #[case::no_filters(None, None, &["a", "b", "c", "d", "e"])]
    #[case::start_from_first(Some(1), None, &["a", "b", "c", "d", "e"])]
    #[case::start_from_second(Some(2), None, &["b", "c", "d", "e"])]
    #[case::start_from_end(Some(5), None, &["e"])]
    #[case::start_from_past_end(Some(6), None, &[])]
    #[case::end_last(None, Some(5), &["a", "b", "c", "d", "e"])]
    #[case::end_one_before_last(None, Some(4), &["a", "b", "c", "d"])]
    #[case::end_at_first(None, Some(1), &["a"])]
    #[case::end_at_zero(None, Some(0), &[])]
    #[case::start_and_end(Some(2), Some(3), &["b", "c"])]
    #[case::crossed(Some(2), Some(1), &[])]
    fn filter_lines(#[case] start: Option<usize>, #[case] end: Option<usize>, #[case] expected: &[&str]) {
        let code = ["a", "b", "c", "d", "e"].join("\n");
        let output = PresentationBuilder::filter_lines(code, start, end);
        let expected = expected.join("\n");
        assert_eq!(output, expected);
    }

    #[test]
    fn plain() {
        let input = "
```bash
echo hi
```";
        let lines = Test::new(input).render().rows(3).columns(7).into_lines();
        let expected = &["       ", "echo hi", "       "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn external_snippet() {
        let temp = tempfile::NamedTempFile::new().expect("failed to create tempfile");
        let path = temp.path();
        fs::write(path, "echo hi").unwrap();

        let path = path.to_string_lossy();
        let input = format!(
            "
```file
path: {path}
language: bash
```
"
        );
        let lines = Test::new(input).render().rows(3).columns(7).into_lines();
        let expected = &["       ", "echo hi", "       "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn line_numbers() {
        let input = "
```bash +line_numbers
hi
bye
```";
        let lines = Test::new(input).render().rows(4).columns(5).into_lines();
        let expected = &["     ", "1 hi ", "2 bye", "     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn line_numbers_via_theme() {
        let input = "---
theme:
  override:
    code:
      line_numbers: true
---

```bash
hi
bye
```";
        let lines = Test::new(input).render().rows(4).columns(5).into_lines();
        let expected = &["     ", "1 hi ", "2 bye", "     "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn surroundings() {
        let input = "
---
```bash
echo hi
```
---";
        let lines = Test::new(input).render().rows(7).columns(7).into_lines();
        let expected = &[
            //
            "       ",
            "———————",
            "       ",
            "echo hi",
            "       ",
            "———————",
            "       ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn padding() {
        let input = "
```bash
echo hi
```";
        let theme = raw::PresentationTheme {
            code: raw::CodeBlockStyle {
                padding: raw::PaddingRect { horizontal: Some(2), vertical: Some(1) },
                ..Default::default()
            },
            ..Default::default()
        };
        let lines = Test::new(input).theme(theme).render().rows(5).columns(13).into_lines();
        let expected = &[
            //
            "             ",
            "             ",
            "  echo hi    ",
            "             ",
            "             ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_no_run() {
        let input = "
```bash +exec
echo hi
```";
        let lines =
            Test::new(input).render().rows(4).columns(19).run_async_renders(RunAsyncRendersPolicy::None).into_lines();
        let expected = &[
            //
            "                   ",
            "echo hi            ",
            "                   ",
            "—— [not started] ——",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_auto() {
        let input = "
```bash +auto_exec
echo hi
```";
        let lines = Test::new(input)
            .render()
            .rows(6)
            .columns(19)
            .run_async_renders(RunAsyncRendersPolicy::OnlyAutomatic)
            .into_lines();
        let expected = &[
            //
            "                   ",
            "echo hi            ",
            "                   ",
            "——— [finished] ————",
            "                   ",
            "hi                 ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn validate() {
        let input = "
```bash +validate
echo hi
```";
        let lines =
            Test::new(input).render().rows(4).columns(19).run_async_renders(RunAsyncRendersPolicy::None).into_lines();
        let expected = &["                   ", "echo hi            ", "                   ", "                   "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_disabled() {
        let input = "
```bash +exec
echo hi
```";
        let lines = Test::new(input).disable_exec().render().rows(6).columns(25).into_lines();
        let expected = &[
            "                         ",
            "echo hi                  ",
            "                         ",
            "snippet +exec is         ",
            "disabled, run with -x to ",
            "enable                   ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_replace_disabled() {
        let input = "
```bash +exec_replace
echo hi
```";
        let lines = Test::new(input).disable_exec_replace().render().rows(6).columns(25).into_lines();
        let expected = &[
            "                         ",
            "echo hi                  ",
            "                         ",
            "snippet +exec_replace is ",
            "disabled, run with -X to ",
            "enable                   ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec() {
        let input = "
```bash +exec
echo hi
```";
        let theme = raw::PresentationTheme {
            execution_output: raw::ExecutionOutputBlockStyle {
                colors: raw::RawColors {
                    background: Some(raw::RawColor::Color(Color::new(45, 45, 45))),
                    foreground: None,
                },
                padding: raw::PaddingRect { horizontal: Some(1), vertical: Some(1) },
                ..Default::default()
            },
            ..Default::default()
        };
        let (lines, styles) = Test::new(input)
            .theme(theme)
            .render()
            .map_background(Color::new(45, 45, 45), 'x')
            .rows(8)
            .columns(16)
            .into_parts();
        let expected_lines = &[
            "                ",
            "echo hi         ",
            "                ",
            "—— [finished] ——",
            "                ",
            "                ",
            " hi             ",
            "                ",
        ];
        let expected_styles = &[
            "                ",
            "xxxxxxxxxxxxxxxx",
            "                ",
            "                ",
            "                ",
            "xxxxxxxxxxxxxxxx",
            "xxxxxxxxxxxxxxxx",
            "xxxxxxxxxxxxxxxx",
        ];
        assert_eq!(lines, expected_lines);
        assert_eq!(styles, expected_styles);
    }

    #[test]
    fn exec_font_size() {
        let input = "
<!-- font_size: 2 -->
```bash +exec
echo hi
```";
        let lines = Test::new(input).render().rows(8).columns(32).into_lines();
        let expected = &[
            "                                ",
            "e c h o   h i                   ",
            "                                ",
            "                                ",
            "— —   [ f i n i s h e d ]   — — ",
            "                                ",
            "                                ",
            "h i                             ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_font_size_centered() {
        let input = "
<!-- font_size: 2 -->
```bash +exec
echo hi
```";
        let theme = raw::PresentationTheme {
            code: raw::CodeBlockStyle {
                alignment: Some(raw::Alignment::Center { minimum_margin: raw::Margin::Fixed(0), minimum_size: 40 }),
                ..Default::default()
            },
            execution_output: raw::ExecutionOutputBlockStyle {
                colors: raw::RawColors {
                    background: Some(raw::RawColor::Color(Color::new(45, 45, 45))),
                    foreground: None,
                },
                padding: raw::PaddingRect { horizontal: Some(1), vertical: Some(1) },
                ..Default::default()
            },
            ..Default::default()
        };
        let (lines, styles) = Test::new(input)
            .theme(theme)
            .render()
            .map_background(Color::new(45, 45, 45), 'x')
            .rows(10)
            .columns(40)
            .into_parts();
        let expected_lines = &[
            "                                        ",
            "e c h o   h i                           ",
            "                                        ",
            "                                        ",
            "— — — —   [ f i n i s h e d ]   — — — — ",
            "                                        ",
            "                                        ",
            "                                        ",
            "                                        ",
            "  h i                                   ",
        ];
        let expected_styles = &[
            "                                        ",
            "x x x x x x x x x x x x x x x x x x x x ",
            "                                        ",
            "                                        ",
            "                                        ",
            "                                        ",
            "                                        ",
            "x x x x x x x x x x x x x x x x x x x x ",
            "                                        ",
            "x x x x x x x x x x x x x x x x x x x x ",
        ];
        assert_eq!(lines, expected_lines);
        assert_eq!(styles, expected_styles);
    }

    #[test]
    fn exec_adjacent_detached_output() {
        let input = "
```bash +exec +id:foo
echo hi
```
<!-- snippet_output: foo -->";
        let lines =
            Test::new(input).render().rows(4).columns(19).run_async_renders(RunAsyncRendersPolicy::None).into_lines();
        // this should look exactly the same as if we hadn't detached the output
        let expected = &[
            //
            "                   ",
            "echo hi            ",
            "                   ",
            "—— [not started] ——",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_detached_output() {
        let input = "
```bash +exec +id:foo
echo hi
```

bar

<!-- snippet_output: foo -->";
        let lines = Test::new(input).render().rows(8).columns(16).into_lines();
        let expected = &[
            "                ",
            "echo hi         ",
            "                ",
            "—— [finished] ——",
            "                ",
            "bar             ",
            "                ",
            "hi              ",
        ];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_replace() {
        let input = "
```bash +exec_replace
echo hi
```";
        let lines = Test::new(input).render().rows(3).columns(7).into_lines();
        let expected = &["       ", "hi     ", "       "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn snippet_exec_replace_centered() {
        let input = "
```bash +exec_replace
echo hi
```";
        let theme = raw::PresentationTheme {
            code: raw::CodeBlockStyle {
                alignment: Some(raw::Alignment::Center { minimum_margin: raw::Margin::Fixed(1), minimum_size: 1 }),
                ..Default::default()
            },
            ..Default::default()
        };
        let lines = Test::new(input).theme(theme).render().rows(3).columns(6).into_lines();
        let expected = &["      ", "  hi  ", "      "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_replace_font_size() {
        let input = "
<!-- font_size: 2 -->
```bash +exec_replace
echo hi
```";
        let lines = Test::new(input).render().rows(3).columns(7).into_lines();
        let expected = &["       ", "h i    ", "       "];
        assert_eq!(lines, expected);
    }

    #[test]
    fn exec_replace_long() {
        let qr = [
            "█▀▀▀▀▀█ ▄▀ ▄▀ █▀▀▀▀▀█",
            "█ ███ █ ▄▀ ▄  █ ███ █",
            "█ ▀▀▀ █ ▄▄█▀█ █ ▀▀▀ █",
            "▀▀▀▀▀▀▀ ▀ █▄█ ▀▀▀▀▀▀▀",
            "█▀▀██ ▀▀█▀  █▀ █ ▀ ▀▄",
            "▄▄██▀▄▀▀▄ █▀ ▀ ▄█▀█▀ ",
            "▀  ▀▀ ▀▀▄█▄█▄█▄▄▀ ▄ █",
            "█▀▀▀▀▀█ ▀▀ ▄█▄█▀ ▄█▀▄",
            "█ ███ █ ██▀ █  ▄█▄ ▀ ",
            "█ ▀▀▀ █ █ ▄▀ ▀  ▄██  ",
            "▀▀▀▀▀▀▀ ▀▀ ▀ ▀  ▀  ▀ ",
        ]
        .join("\n");

        let input = format!(
            r#"
```bash +exec_replace
echo "{qr}"
```
"#
        );
        let rows = 13;
        let columns = 21;
        let lines = Test::new(input).render().rows(rows).columns(columns).into_lines();
        let empty = " ".repeat(columns as usize);
        let expected: Vec<_> = [empty.as_str()].into_iter().chain(qr.lines()).chain([empty.as_str()]).collect();
        assert_eq!(lines, expected);
    }
}
