use super::*;

struct TestCase {
    input: &'static str,
    output: &'static str,
}

#[expect(clippy::needless_pass_by_value)]
fn run_test(name: &str, case: TestCase) {
    let formatter = Formatter::new();
    let actual = formatter.format_terminal(case.input).unwrap();
    assert_eq!(actual, case.output, "failed case: {name}");
}

#[test]
fn test_terminal_strong() {
    let cases = vec![
        ("simple", TestCase {
            input: "Hello **World**!",
            output: "Hello \u{1b}[1m**World**\u{1b}[22m!\n",
        }),
        ("multiple", TestCase {
            input: "Hi **One** and Hi **Two**!",
            output: "Hi \u{1b}[1m**One**\u{1b}[22m and Hi \u{1b}[1m**Two**\u{1b}[22m!\n",
        }),
        ("nested", TestCase {
            input: "***Hello***!",
            output: "\u{1b}[3m*\u{1b}[1m**Hello**\u{1b}[22m*\u{1b}[23m!\n",
        }),
    ];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_emphasized() {
    let cases = vec![
        ("simple", TestCase {
            input: "Hello _World_!",
            output: "Hello \u{1b}[3m*World*\u{1b}[23m!\n",
        }),
        ("multiple", TestCase {
            input: "Hi _One_ and Hi *Two*!",
            output: "Hi \u{1b}[3m*One*\u{1b}[23m and Hi \u{1b}[3m*Two*\u{1b}[23m!\n",
        }),
        ("nested", TestCase {
            input: "*Hello*!",
            output: "\u{1b}[3m*Hello*\u{1b}[23m!\n",
        }),
    ];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_underlined() {
    let cases = vec![("simple", TestCase {
        input: "Hello __World__!",
        output: "Hello \u{1b}[4m__World__\u{1b}[24m!\n",
    })];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_strikethrough() {
    let cases = vec![("simple", TestCase {
        input: "Hello ~~World~~!",
        output: "Hello \u{1b}[9m~~World~~\u{1b}[29m!\n",
    })];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_code() {
    let cases = vec![("simple", TestCase {
        input: "Hello `World`!",
        output: "Hello \x1b[48;2;34;34;34m`World`\x1b[49m!\n",
    })];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_blockquote() {
    let cases = vec![("simple", TestCase {
        input: "> Hello World!",
        output: "\u{1b}[38;2;131;148;150m> Hello World!\u{1b}[39m\n",
    })];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_terminal_blockquote_continuation_lines_colored() {
    // Bug: in a multi-line blockquote, the first `>` is rendered in the
    // blockquote gray, but subsequent `>` markers (from the prefix) are
    // rendered in the default color because ANSI attributes were restored
    // AFTER writing the prefix.
    let input = "> `exclusive` is a property of a specific question, not of a prompt kind. A\n> \
                 `PromptKind::Tool` question may or may not be exclusive. Permission and \
                 Result\n> prompts are implicitly exclusive in a different sense\n";

    let formatter = Formatter::with_width(0);
    let output = formatter.format_terminal(input).unwrap();

    // The blockquote foreground color (from theme gutter_foreground).
    let bq_fg = "\x1b[38;2;131;148;150m";

    // Every `> ` in the output should be preceded by the blockquote color.
    // Split on "> " and check that each occurrence (except possibly the
    // very first line) has the color escape right before it.
    for line in output.lines() {
        if !line.contains("> ") {
            continue;
        }
        // The `> ` should appear after the blockquote foreground escape,
        // not after a reset/default color.
        let gt_pos = line.find("> ").unwrap();
        let before = &line[..gt_pos];
        assert!(
            before.contains(bq_fg) || before.is_empty(),
            "Blockquote '> ' should be preceded by foreground color escape.\nLine: {line:?}\nFull \
             output: {output:?}"
        );
    }
}

#[test]
fn test_no_escaped_special_characters() {
    let cases = vec![
        ("exclamation_mark", TestCase {
            input: "Hello World!!",
            output: "Hello World!!\n",
        }),
        ("question_mark", TestCase {
            input: "Hello World??",
            output: "Hello World??\n",
        }),
        ("parentheses", TestCase {
            input: "Hello World()?",
            output: "Hello World()?\n",
        }),
        ("square_brackets", TestCase {
            input: "Hello World[]?",
            output: "Hello World[]?\n",
        }),
        ("curly_braces", TestCase {
            input: "Hello World{}?",
            output: "Hello World{}?\n",
        }),
        ("angle_brackets", TestCase {
            input: "Hello World<>?",
            output: "Hello World<>?\n",
        }),
    ];

    for (name, case) in cases {
        run_test(name, case);
    }
}

#[test]
fn test_no_trailing_background_after_wrapped_inline_code() {
    // Bug: when comrak soft-wraps a paragraph at `width: 80` and an
    // inline code span falls near the wrap boundary, the ANSI escape
    // codes injected as Raw nodes are counted as visible characters by
    // comrak's line-width calculation. This causes:
    //
    // 1. Premature line wrapping (comrak thinks the line is wider than
    //    it visually is).
    // 2. Trailing spaces on the wrapped line that inherit the active
    //    background color from the inline code escape sequence.
    //
    // The user sees colored whitespace extending past the text content
    // on lines that follow a wrapped inline code span.
    let formatter = Formatter::new();

    // Use the user's exact example: a paragraph with multiple inline
    // code spans where wrapping causes the background to bleed.
    // First example from the user's bug report:
    let input = "**`use` statement split into two blocks** — A standard `use` block appears at \
                 the top, then `signal_stream` is defined, then a second `use super::{ ... }` \
                 block appears. All `use` statements should be at the top per Rust convention.";

    assert_no_bg_bleed(&formatter, input, "use statement example");

    // Second example from the user's bug report:
    let input = "**Inline `ToolConfig` construction is verbose** — Each test constructs \
                 `ToolConfig { source: ..., command: ..., run: ..., enable: None, description: \
                 None, parameters: IndexMap::new(), result: None, style: None, questions: \
                 IndexMap::new() }`. A builder or `ToolConfig::test(source, run_mode)` helper \
                 would eliminate the 6 `None`/default fields.";

    assert_no_bg_bleed(&formatter, input, "ToolConfig example");

    // Third example from the user's bug report:
    let input = "Looking at the user's example more carefully, this IS a blockquote with \
                 continuation lines. When comrak renders those lines, it adds the `>` prefix. But \
                 that's only 2 characters, nowhere near 91.";

    assert_no_bg_bleed(&formatter, input, "blockquote example");
}

/// Assert that no line in the formatted terminal output has an unclosed
/// background color escape (which would cause the terminal to render the
/// background color for the remainder of the line).
#[track_caller]
fn assert_no_bg_bleed(formatter: &Formatter, input: &str, case: &str) {
    let output = formatter.format_terminal(input).unwrap();
    let bg_end = "\x1b[49m";
    let full_reset = "\x1b[0m";
    for (i, line) in output.lines().enumerate() {
        // Count any background-set escape (\x1b[48;...).
        let starts = line.matches("\x1b[48;").count();
        // Both \x1b[49m and \x1b[0m close the background.
        let ends = line.matches(bg_end).count() + line.matches(full_reset).count();
        assert!(
            starts <= ends,
            "[{case}] Line {i} has {starts} bg start(s) but {ends} bg end(s) — background color \
             bleeds to end of terminal line.\nLine: {line:?}\nFull output: {output:?}"
        );
    }
}

#[test]
fn test_table_wrapping_end_to_end() {
    let formatter = Formatter::new(); // default max_column_width = 40
    let input = "| Name | Description |\n| --- | --- |\n| short | brief |\n| long | This is an \
                 extremely long description that definitely exceeds the forty character column \
                 width limit |\n";
    let output = formatter.format_terminal(input).unwrap();

    // No truncation markers — content is wrapped, not truncated.
    assert!(
        !output.contains('…'),
        "should not truncate, should wrap:\n{output}"
    );

    // Full text must be preserved.
    let plain: String = output
        .lines()
        .flat_map(|l| l.chars())
        .filter(|c| !c.is_control() && *c != '|')
        .collect();
    let normalized: String = plain.split_whitespace().collect::<Vec<_>>().join(" ");
    assert!(
        normalized.contains("column width limit"),
        "full cell text should be preserved:\n{output}"
    );

    // No line should be excessively wide.
    for line in output.lines() {
        let vw: usize = {
            let mut len = 0;
            let mut in_esc = false;
            for c in line.chars() {
                if in_esc {
                    if c.is_ascii_alphabetic() || c == '~' {
                        in_esc = false;
                    }
                } else if c == '\x1b' {
                    in_esc = true;
                } else if !c.is_control() {
                    len += 1;
                }
            }
            len
        };
        assert!(vw <= 100, "line too wide ({vw} chars): {line:?}");
    }
}

#[test]
fn test_terminal_blockquote_nested() {
    // This test captures a bug where nested blockquotes cause the outer
    // blockquote's styling to be prematurely terminated.
    //
    // The issue: when exiting the inner blockquote, the formatter emits
    // FG_END (\x1b[39m) which resets the foreground color, even though
    // we're still inside the outer blockquote.
    let input = "> This is a blockquote.\n>\n> > It can be nested.\n>\n> Still in outer.\n";

    let formatter = Formatter::new();
    let actual = formatter.format_terminal(input).unwrap();

    // The text "Still in outer." should still have the blockquote styling
    // applied (gray foreground), but due to the bug, the FG_END from the
    // nested blockquote resets it prematurely.
    //
    // We check that the gray color code appears AFTER the nested blockquote
    // content, indicating the outer blockquote styling is restored.
    let nested_end_pos = actual.find("It can be nested").unwrap();
    let still_in_outer_pos = actual.find("Still in outer").unwrap();

    // Find color codes between the nested blockquote and "Still in outer"
    let between = &actual[nested_end_pos..still_in_outer_pos];

    // After the nested blockquote ends, we should re-apply the outer
    // blockquote's foreground color. Currently this fails because the
    // formatter doesn't track nesting depth.
    assert!(
        between.contains("\x1b[38;"),
        "Expected outer blockquote color to be restored after nested blockquote.\nActual \
         output:\n{actual:?}"
    );
}

#[test]
fn test_default_background_column_fill_with_indent() {
    // When the renderer is seeded with a visual indent (from a Buffer
    // event emitted inside a nested list item, say) and the active
    // background uses `BackgroundFill::Column(target)`, the line fill
    // should pad to `target` columns counting the seeded prefix.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(20),
        }),
        indent: 4,
        ..Default::default()
    };

    let rendered = Formatter::with_width(0)
        .format_terminal_with("Hi\n", &opts)
        .unwrap();

    // Strip ANSI for column counting.
    let plain = strip_ansi_for_test(&rendered);
    let first_line = plain.lines().next().expect("at least one line");

    // "    Hi" + padding to column 20 = 4 prefix + 2 content + 14 pad.
    assert_eq!(
        first_line.chars().count(),
        20,
        "line should be padded to column 20.\nLine: {first_line:?}\nFull: {rendered:?}"
    );
    assert!(
        first_line.starts_with("    Hi"),
        "line should start with seeded indent then content.\nLine: {first_line:?}"
    );
}

#[test]
fn test_default_background_terminal_fill_with_indent() {
    // Same as above for `BackgroundFill::Terminal` (erase-to-EOL).
    // The erase escape should appear after content (so the bg fills to
    // the terminal edge from there). The seeded prefix doesn't affect
    // the erase position; it's just rendered with the bg.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Terminal,
        }),
        indent: 4,
        ..Default::default()
    };

    let rendered = Formatter::with_width(0)
        .format_terminal_with("Hi\n", &opts)
        .unwrap();

    assert!(
        rendered.contains("\x1b[K"),
        "output should contain erase-to-EOL.\n{rendered:?}"
    );
    // Strip ANSI; verify the prefix is preserved before the content.
    let plain = strip_ansi_for_test(&rendered);
    assert!(
        plain.lines().next().unwrap().starts_with("    Hi"),
        "output should begin with seeded prefix.\n{plain:?}"
    );
}

#[test]
fn test_default_background_column_fill_wraps_three_times() {
    // Content long enough to wrap onto three lines should pad every
    // line to the target column.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(25),
        }),
        ..Default::default()
    };

    // Each ~10-char chunk forces a wrap at width=15.
    let input = "alpha bravo charlie delta echo foxtrot golf hotel.\n";
    let rendered = Formatter::with_width(15)
        .format_terminal_with(input, &opts)
        .unwrap();

    let plain = strip_ansi_for_test(&rendered);
    let lines: Vec<&str> = plain.lines().collect();
    assert!(
        lines.len() >= 3,
        "expected 3+ wrapped lines. Got: {plain:?}"
    );
    for (i, line) in lines.iter().enumerate() {
        assert_eq!(
            line.chars().count(),
            25,
            "line {i} should be padded to column 25.\nLine: {line:?}"
        );
    }
}

#[test]
fn test_default_background_column_fill_with_inline_code_at_wrap() {
    // When an inline code span sits at the wrap boundary, the bg-fill
    // pad should still use the wrapped-line width, not the unwrapped
    // buffer width, and the inline code's own bg shouldn't bleed into
    // the padding spaces.
    let bg_param = "48;5;236";
    let bg_escape = format!("\x1b[{bg_param}m");
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(30),
        }),
        ..Default::default()
    };

    let input = "prefix `inline code spans here` suffix\n";
    let rendered = Formatter::with_width(20)
        .format_terminal_with(input, &opts)
        .unwrap();

    // Every line's plaintext width should equal target.
    let plain = strip_ansi_for_test(&rendered);
    for (i, line) in plain.lines().enumerate() {
        assert_eq!(
            line.chars().count(),
            30,
            "line {i} should be padded to column 30.\nLine: {line:?}\nFull plain: {plain:?}"
        );
    }

    // The trailing pad spaces on each line should be under the default
    // (reasoning) bg, not the inline-code bg. Approximate check: each
    // line should end with the default bg escape somewhere before the
    // pad spaces start.
    for line in rendered.lines() {
        if let Some(idx) = line.rfind("\x1b[48;") {
            assert!(
                line[idx..].starts_with(&bg_escape),
                "last bg escape on line should be the default bg.\nLine: {line:?}"
            );
        }
    }
}

#[test]
fn test_default_background_column_fill_exact_width() {
    // Content that fits exactly at target column - 0 pad spaces.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(5),
        }),
        ..Default::default()
    };

    // "Hello" is exactly 5 chars.
    let rendered = Formatter::with_width(0)
        .format_terminal_with("Hello\n", &opts)
        .unwrap();
    let plain = strip_ansi_for_test(&rendered);
    assert_eq!(plain.lines().next().unwrap().chars().count(), 5);
}

#[test]
fn test_default_background_column_fill_content_exceeds_target() {
    // Content wider than the target column should not be truncated;
    // the pad calculation saturates at 0.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(5),
        }),
        ..Default::default()
    };

    let rendered = Formatter::with_width(0)
        .format_terminal_with("Hello world\n", &opts)
        .unwrap();
    let plain = strip_ansi_for_test(&rendered);
    let line = plain.lines().next().unwrap();
    assert!(
        line.starts_with("Hello world"),
        "content should be preserved.\nLine: {line:?}"
    );
    // Content is 11 chars; pad to 5 yields 0 pad. Line is 11 chars.
    assert_eq!(line.chars().count(), 11);
}

#[test]
fn test_default_background_column_fill_no_indent_wrapped() {
    // Pre-existing-baseline: does `BackgroundFill::Column` already work
    // correctly with wrapped lines when there is no seeded indent?
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(30),
        }),
        ..Default::default()
    };

    let input = "This is a fairly long paragraph that will wrap.\n";
    let rendered = Formatter::with_width(20)
        .format_terminal_with(input, &opts)
        .unwrap();

    let plain = strip_ansi_for_test(&rendered);
    let lines: Vec<&str> = plain.lines().collect();
    assert!(lines.len() >= 2, "expected wrapping. Got: {plain:?}");

    for (i, line) in lines.iter().enumerate() {
        assert_eq!(
            line.chars().count(),
            30,
            "line {i} should be padded to column 30.\nLine: {line:?}\nFull: {plain:?}"
        );
    }
}

#[test]
fn test_default_background_column_fill_with_indent_wrapped() {
    // When width-wrapping is active and content wraps, the continuation
    // line should *also* start with the seeded prefix, and the column
    // fill should still pad to `target`.
    let bg_param = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Column(30),
        }),
        indent: 4,
        ..Default::default()
    };

    // Long enough to wrap at width=20.
    let input = "This is a fairly long paragraph that will wrap.\n";
    let rendered = Formatter::with_width(20)
        .format_terminal_with(input, &opts)
        .unwrap();

    let plain = strip_ansi_for_test(&rendered);
    let lines: Vec<&str> = plain.lines().collect();
    assert!(lines.len() >= 2, "expected wrapping. Got: {plain:?}");

    for (i, line) in lines.iter().enumerate() {
        assert!(
            line.starts_with("    "),
            "line {i} should start with seeded prefix.\nLine: {line:?}\nFull: {plain:?}"
        );
        assert_eq!(
            line.chars().count(),
            30,
            "line {i} should be padded to column 30.\nLine: {line:?}\nFull: {plain:?}"
        );
    }
}

#[test]
fn test_default_background_terminal_fill() {
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: "48;5;236".into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    // With wrapping enabled (width > 0)
    let actual = Formatter::new()
        .format_terminal_with("Deep thought", &opts)
        .unwrap();
    assert!(
        actual.contains("\x1b[48;5;236m"),
        "Should set background. Got: {actual:?}"
    );
    assert!(
        actual.contains("\x1b[K"),
        "Should contain erase-to-EOL. Got: {actual:?}"
    );
    assert!(
        actual.contains("\x1b[0m"),
        "Should contain RESET. Got: {actual:?}"
    );

    // With wrapping disabled (width == 0)
    let actual = Formatter::with_width(0)
        .format_terminal_with("Deep thought", &opts)
        .unwrap();
    assert!(
        actual.contains("\x1b[48;5;236m"),
        "width=0: Should set background. Got: {actual:?}"
    );
    assert!(
        actual.contains("\x1b[K"),
        "width=0: Should contain erase-to-EOL. Got: {actual:?}"
    );
    assert!(
        actual.contains("\x1b[0m"),
        "width=0: Should contain RESET. Got: {actual:?}"
    );
}

#[test]
fn test_inline_code_wrap_uses_default_bg_for_fill() {
    // Bug: when an inline code span wraps across lines with a reasoning
    // background active, the line-fill (erase to EOL) uses the inline
    // code background instead of the reasoning background, causing the
    // code bg to extend to the terminal edge.
    let reasoning_bg = "48;5;236";
    let reasoning_esc = format!("\x1b[{reasoning_bg}m");
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: reasoning_bg.into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    // A paragraph with inline code near the wrap boundary.
    let input = "An empty slice starts with `low = 0` and `high = 0`, so the while condition \
                 fails immediately.";
    let actual = Formatter::with_width(40)
        .format_terminal_with(input, &opts)
        .unwrap();

    // Every \x1b[K (erase to EOL) should be preceded by the reasoning
    // background, not the inline code background.
    for (i, line) in actual.lines().enumerate() {
        // Find each \x1b[K and check that the bg set just before it
        // is the reasoning bg, not a code bg.
        for (pos, _) in line.match_indices("\x1b[K") {
            let before = &line[..pos];
            // The last bg escape before \x1b[K should be the reasoning bg.
            let last_bg_pos = before.rfind("\x1b[48;");
            if let Some(bg_pos) = last_bg_pos {
                let bg_escape = &before[bg_pos..];
                assert!(
                    bg_escape.starts_with(&reasoning_esc),
                    "Line {i}: erase-to-EOL should use reasoning bg, not code bg.\nBefore \
                     \\x1b[K: {bg_escape:?}\nFull line: {line:?}\nFull output: {actual:?}"
                );
            }
        }
    }
}

#[test]
fn test_inline_code_wrap_prefix_uses_reasoning_bg() {
    // Bug: when inline code wraps across lines with reasoning background,
    // the indentation prefix on the continuation line gets the inline code
    // background instead of the reasoning background.
    let reasoning_bg = "48;5;236";
    let reasoning_esc = format!("\x1b[{reasoning_bg}m");
    let code_bg = "48;5;248"; // theme code bg
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: reasoning_bg.into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    // List item where the inline code span crosses the wrap boundary.
    // At width=35, "1. Begin " (9) + "`some long code " (16) = 25,
    // then "that " wraps. The wrap happens INSIDE the code span.
    let input = "1. Begin `some long code that crosses the wrap boundary` end.\n";
    let actual = Formatter::with_width(35)
        .format_terminal_with(input, &opts)
        .unwrap();

    let lines: Vec<&str> = actual.lines().collect();
    assert!(lines.len() >= 2, "Expected wrapping. Got: {actual:?}");

    let cont = lines[1];
    assert!(
        !cont.starts_with(&format!("\x1b[{code_bg}m")),
        "Continuation line prefix should NOT have inline code bg.\nLine: {cont:?}\nFull output: \
         {actual:?}"
    );
    assert!(
        cont.starts_with(&reasoning_esc),
        "Continuation line prefix should start with reasoning bg.\nLine: {cont:?}\nFull output: \
         {actual:?}"
    );
}

#[test]
fn test_inline_code_wrap_preserves_code_bg_on_content() {
    // When inline code wraps across lines, the code CONTENT on the
    // continuation line must keep the inline code background. Only the
    // prefix and line-fill should use the reasoning (default) bg.
    let reasoning_bg = "48;5;236";
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: reasoning_bg.into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    // Same input as prefix test — wrap happens inside the code span.
    let input = "1. Begin `some long code that crosses the wrap boundary` end.\n";
    let actual = Formatter::with_width(35)
        .format_terminal_with(input, &opts)
        .unwrap();

    let lines: Vec<&str> = actual.lines().collect();
    assert!(lines.len() >= 2, "Expected wrapping. Got: {actual:?}");

    // The continuation line should have at least two bg escapes:
    // one for reasoning (prefix) and one for inline code (content).
    // The continuation line should have at least two DIFFERENT bg escapes:
    // reasoning bg for the prefix and code bg for the content.
    let cont = lines[1];
    let bg_escapes: Vec<_> = cont
        .match_indices("\x1b[48;")
        .map(|(pos, _)| {
            let rest = &cont[pos..];
            let end = rest.find('m').unwrap_or(rest.len());
            &cont[pos..=pos + end]
        })
        .collect();
    assert!(
        bg_escapes.len() >= 2,
        "Continuation should have at least 2 bg escapes.\nFound: {bg_escapes:?}\nLine: \
         {cont:?}\nFull output: {actual:?}"
    );
    // The first bg should be reasoning, a later one should differ (code bg).
    let has_different = bg_escapes.windows(2).any(|w| w[0] != w[1]);
    assert!(
        has_different,
        "Continuation should have different bg for prefix vs content.\nAll bg escapes are \
         identical: {bg_escapes:?}\nLine: {cont:?}\nFull output: {actual:?}"
    );
}

/// Regression: when a wrap-break landed between the *last breakable space* and
/// the start of an inline span (e.g.
/// `**bold**` opened mid-line), the escapes recorded past the break point were
/// dropped.
/// The continuation line was then re-stylized using `attrs.restore_sequence()`,
/// which promoted the style (here: bold) onto text that should have been plain.
#[test]
fn test_strong_does_not_bleed_across_wrap() {
    // Width chosen so:
    //   - "abcdef" fits before the breakable space,
    //   - by the time we open `**bold**`, BOLD_START is recorded past the
    //     last breakable space,
    //   - emitting the first `*` overflows width=10 and triggers a wrap.
    let actual = Formatter::with_width(10)
        .format_terminal("abcdef ghi**bold**\n")
        .unwrap();

    // Sanity: it actually wrapped.
    assert!(
        actual.lines().count() >= 2,
        "Expected wrapping. Got: {actual:?}"
    );

    // Walk the output and assert that bold is *not* active at "ghi".
    let pos = actual.find("ghi").expect("`ghi` should be in the output");
    let prefix = &actual[..pos];
    let mut bold = false;
    for sgr in sgr_params_in_order(prefix) {
        for tok in sgr.split(';') {
            match tok {
                "" | "0" | "22" => bold = false,
                "1" => bold = true,
                _ => {}
            }
        }
    }
    assert!(
        !bold,
        "Bold should NOT be active over `ghi` (it sits before `**`).\nFull output: {actual:?}"
    );
}

/// Yield the SGR parameter strings (between `\x1b[` and `m`) of `s`, in order.
fn sgr_params_in_order(s: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut iter = s.chars();
    while let Some(c) = iter.next() {
        if c != '\x1b' || iter.next() != Some('[') {
            continue;
        }
        let mut params = String::new();
        for nc in iter.by_ref() {
            if nc.is_ascii_alphabetic() {
                if nc == 'm' {
                    out.push(params.clone());
                }
                break;
            }
            params.push(nc);
        }
    }
    out
}

#[test]
fn test_code_block_inherits_default_background() {
    // Bug: when rendering with a default background (reasoning mode),
    // syntax-highlighted code blocks didn't get the background because
    // write_raw bypassed the background management.
    let bg_param = "48;5;236";
    let bg_esc = format!("\x1b[{bg_param}m");
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    let input = "```rust\nfn main() {}\n```\n";
    let actual = Formatter::with_width(0)
        .format_terminal_with(input, &opts)
        .unwrap();

    // Every line should have the background escape.
    // (syntect inserts color escapes between tokens, so search for "fn".)
    for line in actual.lines() {
        assert!(
            line.contains(&bg_esc),
            "Every line should have default background.\nLine: {line:?}\nFull output: {actual:?}"
        );
    }
}

#[test]
fn test_list_continuation_has_background_on_prefix() {
    // Bug: when rendering a list with a default background (e.g. reasoning),
    // wrapped continuation lines had the background on the text but NOT on
    // the indentation prefix spaces. The prefix should also be background-
    // colored so there's no gap.
    let bg_param = "48;5;236";
    let bg_escape = format!("\x1b[{bg_param}m");
    let opts = TerminalOptions {
        default_background: Some(DefaultBackground {
            param: bg_param.into(),
            fill: BackgroundFill::Terminal,
        }),
        ..Default::default()
    };

    // A list item long enough to wrap at width=40.
    let input = "1. This is a list item that is long enough to wrap onto a second line\n";
    let actual = Formatter::with_width(40)
        .format_terminal_with(input, &opts)
        .unwrap();

    // Each continuation line should have the background escape BEFORE
    // the prefix spaces, not after.
    let lines: Vec<&str> = actual.lines().collect();
    assert!(lines.len() >= 2, "Expected wrapped output, got {lines:?}");

    // The continuation line (second line) should start with the bg escape
    // followed by prefix spaces.
    let cont = lines[1];
    assert!(
        cont.starts_with(&bg_escape),
        "Continuation line should start with background escape.\nLine: {cont:?}\nFull output: \
         {actual:?}"
    );
}

#[test]
fn test_blockquote_with_code_block_has_prefix_on_fences() {
    // Inside a blockquote, the code fence lines and blank separator lines
    // should have the "> " prefix. The code content lines should NOT.
    let input = "> Some text\n>\n> ```rust\n> fn main() {}\n> ```\n>\n> More text\n";

    let formatter = Formatter::with_width(0);
    let actual = formatter.format_terminal(input).unwrap();

    // Strip ANSI for easier inspection.
    let plain = strip_ansi_for_test(&actual);
    let lines: Vec<&str> = plain.lines().collect();

    // Fences and blank lines should have "> " prefix.
    assert!(lines[0].starts_with("> "), "line 0: {lines:?}");
    assert_eq!(lines[1], "> ", "blank line before fence: {lines:?}");
    assert!(lines[2].starts_with("> ```"), "opening fence: {lines:?}");
    // Code content should NOT have "> " prefix.
    assert!(
        !lines[3].starts_with("> "),
        "code content should not have prefix: {lines:?}"
    );
    assert!(lines[3].contains("fn main"), "code content: {lines:?}");
    // Closing fence and after.
    assert!(lines[4].starts_with("> ```"), "closing fence: {lines:?}");
    assert_eq!(lines[5], "> ", "blank line after fence: {lines:?}");
    assert!(lines[6].starts_with("> "), "text after code: {lines:?}");
}

#[test]
fn test_terminal_list_last_item_gets_separator() {
    // Bug: the last list item (ending with \n\n from the buffer) is
    // parsed by comrak as a tight single-item list. ends_with_tight_list
    // returns true and suppresses the separator, so the paragraph after
    // the list has no blank line in front of it.
    let fmt = Formatter::with_width(0);

    // Simulate what the buffer produces: terminal item ends with \n\n.
    let last_item = "5. Fifth item\n\n";
    let output = fmt
        .format_terminal_with(last_item, &TerminalOptions::default())
        .unwrap();
    let plain = strip_ansi_for_test(&output);

    // The formatted output should end with a blank line (separator)
    // so the next block has spacing.
    assert!(
        plain.ends_with("\n\n"),
        "Terminal list item should have trailing separator.\nPlain: {plain:?}\nOutput: {output:?}"
    );
}

#[test]
fn test_terminal_list_mid_item_no_separator() {
    // Mid-list items (ending with \n, no blank line) should NOT get
    // a separator, so consecutive items render tight.
    let fmt = Formatter::with_width(0);

    let mid_item = "1. First item\n";
    let output = fmt
        .format_terminal_with(mid_item, &TerminalOptions::default())
        .unwrap();
    let plain = strip_ansi_for_test(&output);

    // Should end with exactly one newline, not two.
    assert!(
        plain.ends_with('\n') && !plain.ends_with("\n\n"),
        "Mid-list item should NOT have trailing separator.\nPlain: {plain:?}\nOutput: {output:?}"
    );
}

/// Strip ANSI escapes for readable test assertions.
fn strip_ansi_for_test(s: &str) -> String {
    let mut out = String::new();
    let mut in_escape = false;
    for c in s.chars() {
        if in_escape {
            if c.is_ascii_alphabetic() || c == '~' {
                in_escape = false;
            }
        } else if c == '\x1b' {
            in_escape = true;
        } else {
            out.push(c);
        }
    }
    out
}

#[test]
fn test_thematic_break_default_line_style() {
    // Default hr_style is Line — renders a unicode horizontal line.
    let formatter = Formatter::new();
    let actual = formatter.format_terminal("above\n\n---\n\nbelow").unwrap();
    let line: String = "─".repeat(80);
    assert!(
        actual.contains(&line),
        "Expected 80-char unicode line (default width).\nActual: {actual:?}"
    );
}

#[test]
fn test_thematic_break_markdown_style() {
    // HrStyle::Markdown renders the standard `-----`.
    let mut formatter = Formatter::new();
    formatter.hr_style = HrStyle::Markdown;
    let actual = formatter.format_terminal("above\n\n---\n\nbelow").unwrap();
    assert!(
        actual.contains("-----"),
        "Expected markdown-style thematic break.\nActual: {actual:?}"
    );
}

#[test]
fn test_thematic_break_line_style_uses_configured_width() {
    // HrStyle::Line should produce a line of `─` characters using
    // the configured wrap width when no terminal_width is set.
    let mut formatter = Formatter::with_width(40);
    formatter.hr_style = HrStyle::Line;

    let actual = formatter.format_terminal("above\n\n---\n\nbelow").unwrap();
    let line: String = "─".repeat(40);
    assert!(
        actual.contains(&line),
        "Expected 40-char unicode line.\nActual: {actual:?}"
    );
}

#[test]
fn test_thematic_break_line_style_uses_terminal_width() {
    // When terminal_width is set, it takes precedence over the
    // configured wrap width.
    let mut formatter = Formatter::with_width(40).terminal_width(120);
    formatter.hr_style = HrStyle::Line;

    let actual = formatter.format_terminal("above\n\n---\n\nbelow").unwrap();
    let line: String = "─".repeat(120);
    assert!(
        actual.contains(&line),
        "Expected 120-char unicode line.\nActual: {actual:?}"
    );
    // Should NOT contain a 40-char line (unless it's a substring, but
    // we check exact length by verifying no extra `─` beyond 120).
    assert!(
        !actual.contains(&"─".repeat(121)),
        "Line should not exceed terminal width.\nActual: {actual:?}"
    );
}

/// Regression: the terminal renderer used to emit `<!-- end list -->` between
/// adjacent lists (and before indented code blocks) — a CommonMark round-trip
/// hint borrowed from comrak's serializer that has no business appearing in
/// terminal output.
/// See also: `format_list` in `render.rs`.
#[test]
fn test_terminal_no_end_list_marker_between_adjacent_lists() {
    let formatter = Formatter::new();

    // Two adjacent bullet lists.
    let out = formatter.format_terminal("- a\n\n- b\n").unwrap();
    assert!(
        !out.contains("end list"),
        "Unexpected `<!-- end list -->` marker in terminal output: {out:?}"
    );

    // Bullet list followed by an ordered list.
    let out = formatter.format_terminal("- a\n\n1. b\n").unwrap();
    assert!(
        !out.contains("end list"),
        "Unexpected `<!-- end list -->` marker in terminal output: {out:?}"
    );

    // List followed by a fenced code block.
    let out = formatter
        .format_terminal("- a\n\n```\ncode\n```\n")
        .unwrap();
    assert!(
        !out.contains("end list"),
        "Unexpected `<!-- end list -->` marker in terminal output: {out:?}"
    );
}
