import assert from "node:assert";
import { describe, it } from "node:test";
import type { Terminal as XtermTerminalType } from "@xterm/headless";
import { Chalk } from "chalk";
import { Markdown } from "../src/components/markdown.js";
import { type Component, TUI } from "../src/tui.js";
import { defaultMarkdownTheme } from "./test-themes.js";
import { VirtualTerminal } from "./virtual-terminal.js";

// Force full color in CI so ANSI assertions are deterministic
const chalk = new Chalk({ level: 3 });

function getCellItalic(
  terminal: VirtualTerminal,
  row: number,
  col: number,
): number {
  const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
  const buffer = xterm.buffer.active;
  const line = buffer.getLine(buffer.viewportY + row);
  assert.ok(line, `Missing buffer line at row ${row}`);
  const cell = line.getCell(col);
  assert.ok(cell, `Missing cell at row ${row} col ${col}`);
  return cell.isItalic();
}

describe("Markdown component", () => {
  describe("Nested lists", () => {
    it("should render simple nested list", () => {
      const markdown = new Markdown(
        `- Item 1
  - Nested 1.1
  - Nested 1.2
- Item 2`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);

      // Check that we have content
      assert.ok(lines.length > 0);

      // Strip ANSI codes for checking
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Check structure
      assert.ok(plainLines.some((line) => line.includes("- Item 1")));
      assert.ok(plainLines.some((line) => line.includes("  - Nested 1.1")));
      assert.ok(plainLines.some((line) => line.includes("  - Nested 1.2")));
      assert.ok(plainLines.some((line) => line.includes("- Item 2")));
    });

    it("should render deeply nested list", () => {
      const markdown = new Markdown(
        `- Level 1
  - Level 2
    - Level 3
      - Level 4`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Check proper indentation
      assert.ok(plainLines.some((line) => line.includes("- Level 1")));
      assert.ok(plainLines.some((line) => line.includes("  - Level 2")));
      assert.ok(plainLines.some((line) => line.includes("    - Level 3")));
      assert.ok(plainLines.some((line) => line.includes("      - Level 4")));
    });

    it("should render ordered nested list", () => {
      const markdown = new Markdown(
        `1. First
   1. Nested first
   2. Nested second
2. Second`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      assert.ok(plainLines.some((line) => line.includes("1. First")));
      assert.ok(plainLines.some((line) => line.includes("  1. Nested first")));
      assert.ok(plainLines.some((line) => line.includes("  2. Nested second")));
      assert.ok(plainLines.some((line) => line.includes("2. Second")));
    });

    it("should render mixed ordered and unordered nested lists", () => {
      const markdown = new Markdown(
        `1. Ordered item
   - Unordered nested
   - Another nested
2. Second ordered
   - More nested`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      assert.ok(plainLines.some((line) => line.includes("1. Ordered item")));
      assert.ok(
        plainLines.some((line) => line.includes("  - Unordered nested")),
      );
      assert.ok(plainLines.some((line) => line.includes("2. Second ordered")));
    });

    it("should maintain numbering when code blocks are not indented (LLM output)", () => {
      // When code blocks aren't indented, marked parses each item as a separate list.
      // We use token.start to preserve the original numbering.
      const markdown = new Markdown(
        `1. First item

\`\`\`typescript
// code block
\`\`\`

2. Second item

\`\`\`typescript
// another code block
\`\`\`

3. Third item`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trim(),
      );

      // Find all lines that start with a number and period
      const numberedLines = plainLines.filter((line) => /^\d+\./.test(line));

      // Should have 3 numbered items
      assert.strictEqual(
        numberedLines.length,
        3,
        `Expected 3 numbered items, got: ${numberedLines.join(", ")}`,
      );

      // Check the actual numbers
      assert.ok(
        numberedLines[0].startsWith("1."),
        `First item should be "1.", got: ${numberedLines[0]}`,
      );
      assert.ok(
        numberedLines[1].startsWith("2."),
        `Second item should be "2.", got: ${numberedLines[1]}`,
      );
      assert.ok(
        numberedLines[2].startsWith("3."),
        `Third item should be "3.", got: ${numberedLines[2]}`,
      );
    });
  });

  describe("Tables", () => {
    it("should render simple table", () => {
      const markdown = new Markdown(
        `| Name | Age |
| --- | --- |
| Alice | 30 |
| Bob | 25 |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Check table structure
      assert.ok(plainLines.some((line) => line.includes("Name")));
      assert.ok(plainLines.some((line) => line.includes("Age")));
      assert.ok(plainLines.some((line) => line.includes("Alice")));
      assert.ok(plainLines.some((line) => line.includes("Bob")));
      // Check for table borders
      assert.ok(plainLines.some((line) => line.includes("│")));
      assert.ok(plainLines.some((line) => line.includes("─")));
    });

    it("should render row dividers between data rows", () => {
      const markdown = new Markdown(
        `| Name | Age |
| --- | --- |
| Alice | 30 |
| Bob | 25 |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const dividerLines = plainLines.filter((line) => line.includes("┼"));

      assert.strictEqual(
        dividerLines.length,
        2,
        "Expected header + row divider",
      );
    });

    it("should keep column width at least the longest word", () => {
      const longestWord = "superlongword";
      const markdown = new Markdown(
        `| Column One | Column Two |
| --- | --- |
| ${longestWord} short | otherword |
| small | tiny |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(32);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const dataLine = plainLines.find((line) => line.includes(longestWord));
      assert.ok(dataLine, "Expected data row containing longest word");

      const segments = dataLine.split("│").slice(1, -1);
      const [firstSegment] = segments;
      assert.ok(firstSegment, "Expected first column segment");
      const firstColumnWidth = firstSegment.length - 2;

      assert.ok(
        firstColumnWidth >= longestWord.length,
        `Expected first column width >= ${longestWord.length}, got ${firstColumnWidth}`,
      );
    });

    it("should render table with alignment", () => {
      const markdown = new Markdown(
        `| Left | Center | Right |
| :--- | :---: | ---: |
| A | B | C |
| Long text | Middle | End |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Check headers
      assert.ok(plainLines.some((line) => line.includes("Left")));
      assert.ok(plainLines.some((line) => line.includes("Center")));
      assert.ok(plainLines.some((line) => line.includes("Right")));
      // Check content
      assert.ok(plainLines.some((line) => line.includes("Long text")));
    });

    it("should handle tables with varying column widths", () => {
      const markdown = new Markdown(
        `| Short | Very long column header |
| --- | --- |
| A | This is a much longer cell content |
| B | Short |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);

      // Should render without errors
      assert.ok(lines.length > 0);

      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      assert.ok(
        plainLines.some((line) => line.includes("Very long column header")),
      );
      assert.ok(
        plainLines.some((line) =>
          line.includes("This is a much longer cell content"),
        ),
      );
    });

    it("should wrap table cells when table exceeds available width", () => {
      const markdown = new Markdown(
        `| Command | Description | Example |
| --- | --- | --- |
| npm install | Install all dependencies | npm install |
| npm run build | Build the project | npm run build |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      // Render at narrow width that forces wrapping
      const lines = markdown.render(50);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // All lines should fit within width
      for (const line of plainLines) {
        assert.ok(
          line.length <= 50,
          `Line exceeds width 50: "${line}" (length: ${line.length})`,
        );
      }

      // Content should still be present (possibly wrapped across lines)
      const allText = plainLines.join(" ");
      assert.ok(allText.includes("Command"), "Should contain 'Command'");
      assert.ok(
        allText.includes("Description"),
        "Should contain 'Description'",
      );
      assert.ok(
        allText.includes("npm install"),
        "Should contain 'npm install'",
      );
      assert.ok(allText.includes("Install"), "Should contain 'Install'");
    });

    it("should wrap long cell content to multiple lines", () => {
      const markdown = new Markdown(
        `| Header |
| --- |
| This is a very long cell content that should wrap |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      // Render at width that forces the cell to wrap
      const lines = markdown.render(25);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // Should have multiple data rows due to wrapping
      const dataRows = plainLines.filter(
        (line) => line.startsWith("│") && !line.includes("─"),
      );
      assert.ok(
        dataRows.length > 2,
        `Expected wrapped rows, got ${dataRows.length} rows`,
      );

      // All content should be preserved (may be split across lines)
      const allText = plainLines.join(" ");
      assert.ok(allText.includes("very long"), "Should preserve 'very long'");
      assert.ok(
        allText.includes("cell content"),
        "Should preserve 'cell content'",
      );
      assert.ok(
        allText.includes("should wrap"),
        "Should preserve 'should wrap'",
      );
    });

    it("should wrap long unbroken tokens inside table cells (not only at line start)", () => {
      const url =
        "https://example.com/this/is/a/very/long/url/that/should/wrap";
      const markdown = new Markdown(
        `| Value |
| --- |
| prefix ${url} |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const width = 30;
      const lines = markdown.render(width);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      for (const line of plainLines) {
        assert.ok(
          line.length <= width,
          `Line exceeds width ${width}: "${line}" (length: ${line.length})`,
        );
      }

      // Borders should stay intact (exactly 2 vertical borders for a 1-col table)
      const tableLines = plainLines.filter((line) => line.startsWith("│"));
      assert.ok(tableLines.length > 0, "Expected table rows to render");
      for (const line of tableLines) {
        const borderCount = line.split("│").length - 1;
        assert.strictEqual(
          borderCount,
          2,
          `Expected 2 borders, got ${borderCount}: "${line}"`,
        );
      }

      // Strip box drawing characters + whitespace so we can assert the URL is preserved
      // even if it was split across multiple wrapped lines.
      const extracted = plainLines.join("").replace(/[│├┤─\s]/g, "");
      assert.ok(extracted.includes("prefix"), "Should preserve 'prefix'");
      assert.ok(extracted.includes(url), "Should preserve URL");
    });

    it("should wrap styled inline code inside table cells without breaking borders", () => {
      const markdown = new Markdown(
        `| Code |
| --- |
| \`averyveryveryverylongidentifier\` |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const width = 20;
      const lines = markdown.render(width);
      const joinedOutput = lines.join("\n");
      assert.ok(
        joinedOutput.includes("\x1b[33m"),
        "Inline code should be styled (yellow)",
      );

      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );
      for (const line of plainLines) {
        assert.ok(
          line.length <= width,
          `Line exceeds width ${width}: "${line}" (length: ${line.length})`,
        );
      }

      const tableLines = plainLines.filter((line) => line.startsWith("│"));
      for (const line of tableLines) {
        const borderCount = line.split("│").length - 1;
        assert.strictEqual(
          borderCount,
          2,
          `Expected 2 borders, got ${borderCount}: "${line}"`,
        );
      }
    });

    it("should handle extremely narrow width gracefully", () => {
      const markdown = new Markdown(
        `| A | B | C |
| --- | --- | --- |
| 1 | 2 | 3 |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      // Very narrow width
      const lines = markdown.render(15);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // Should not crash and should produce output
      assert.ok(lines.length > 0, "Should produce output");

      // Lines should not exceed width
      for (const line of plainLines) {
        assert.ok(
          line.length <= 15,
          `Line exceeds width 15: "${line}" (length: ${line.length})`,
        );
      }
    });

    it("should render table correctly when it fits naturally", () => {
      const markdown = new Markdown(
        `| A | B |
| --- | --- |
| 1 | 2 |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      // Wide width where table fits naturally
      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // Should have proper table structure
      const headerLine = plainLines.find(
        (line) => line.includes("A") && line.includes("B"),
      );
      assert.ok(headerLine, "Should have header row");
      assert.ok(headerLine?.includes("│"), "Header should have borders");

      const separatorLine = plainLines.find(
        (line) => line.includes("├") && line.includes("┼"),
      );
      assert.ok(separatorLine, "Should have separator row");

      const dataLine = plainLines.find(
        (line) => line.includes("1") && line.includes("2"),
      );
      assert.ok(dataLine, "Should have data row");
    });

    it("should respect paddingX when calculating table width", () => {
      const markdown = new Markdown(
        `| Column One | Column Two |
| --- | --- |
| Data 1 | Data 2 |`,
        2, // paddingX = 2
        0,
        defaultMarkdownTheme,
      );

      // Width 40 with paddingX=2 means contentWidth=36
      const lines = markdown.render(40);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // All lines should respect width
      for (const line of plainLines) {
        assert.ok(
          line.length <= 40,
          `Line exceeds width 40: "${line}" (length: ${line.length})`,
        );
      }

      // Table rows should have left padding
      const tableRow = plainLines.find((line) => line.includes("│"));
      assert.ok(tableRow?.startsWith("  "), "Table should have left padding");
    });
  });

  describe("Combined features", () => {
    it("should render lists and tables together", () => {
      const markdown = new Markdown(
        `# Test Document

- Item 1
  - Nested item
- Item 2

| Col1 | Col2 |
| --- | --- |
| A | B |`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Check heading
      assert.ok(plainLines.some((line) => line.includes("Test Document")));
      // Check list
      assert.ok(plainLines.some((line) => line.includes("- Item 1")));
      assert.ok(plainLines.some((line) => line.includes("  - Nested item")));
      // Check table
      assert.ok(plainLines.some((line) => line.includes("Col1")));
      assert.ok(plainLines.some((line) => line.includes("│")));
    });
  });

  describe("Pre-styled text (thinking traces)", () => {
    it("should preserve gray italic styling after inline code", () => {
      // This replicates how thinking content is rendered in assistant-message.ts
      const markdown = new Markdown(
        "This is thinking with `inline code` and more text after",
        1,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.gray(text),
          italic: true,
        },
      );

      const lines = markdown.render(80);
      const joinedOutput = lines.join("\n");

      // Should contain the inline code block
      assert.ok(joinedOutput.includes("inline code"));

      // The output should have ANSI codes for gray (90) and italic (3)
      assert.ok(
        joinedOutput.includes("\x1b[90m"),
        "Should have gray color code",
      );
      assert.ok(joinedOutput.includes("\x1b[3m"), "Should have italic code");

      // Verify that inline code is styled (theme uses yellow)
      const hasCodeColor = joinedOutput.includes("\x1b[33m");
      assert.ok(hasCodeColor, "Should style inline code");
    });

    it("should preserve gray italic styling after bold text", () => {
      const markdown = new Markdown(
        "This is thinking with **bold text** and more after",
        1,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.gray(text),
          italic: true,
        },
      );

      const lines = markdown.render(80);
      const joinedOutput = lines.join("\n");

      // Should contain bold text
      assert.ok(joinedOutput.includes("bold text"));

      // The output should have ANSI codes for gray (90) and italic (3)
      assert.ok(
        joinedOutput.includes("\x1b[90m"),
        "Should have gray color code",
      );
      assert.ok(joinedOutput.includes("\x1b[3m"), "Should have italic code");

      // Should have bold codes (1 or 22 for bold on/off)
      assert.ok(joinedOutput.includes("\x1b[1m"), "Should have bold code");
    });

    it("should not leak styles into following lines when rendered in TUI", async () => {
      class MarkdownWithInput implements Component {
        public markdownLineCount = 0;

        constructor(private readonly markdown: Markdown) {}

        render(width: number): string[] {
          const lines = this.markdown.render(width);
          this.markdownLineCount = lines.length;
          return [...lines, "INPUT"];
        }

        invalidate(): void {
          this.markdown.invalidate();
        }
      }

      const markdown = new Markdown(
        "This is thinking with `inline code`",
        1,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.gray(text),
          italic: true,
        },
      );

      const terminal = new VirtualTerminal(80, 6);
      const tui = new TUI(terminal);
      const component = new MarkdownWithInput(markdown);
      tui.addChild(component);
      tui.start();
      await terminal.flush();

      assert.ok(component.markdownLineCount > 0);
      const inputRow = component.markdownLineCount;
      assert.strictEqual(getCellItalic(terminal, inputRow, 0), 0);
      tui.stop();
    });
  });

  describe("Spacing after code blocks", () => {
    it("should have only one blank line between code block and following paragraph", () => {
      const markdown = new Markdown(
        `hello world

\`\`\`js
const hello = "world";
\`\`\`

again, hello world`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      const closingBackticksIndex = plainLines.indexOf("```");
      assert.ok(closingBackticksIndex !== -1, "Should have closing backticks");

      const afterBackticks = plainLines.slice(closingBackticksIndex + 1);
      const emptyLineCount = afterBackticks.findIndex((line) => line !== "");

      assert.strictEqual(
        emptyLineCount,
        1,
        `Expected 1 empty line after code block, but found ${emptyLineCount}. Lines after backticks: ${JSON.stringify(afterBackticks.slice(0, 5))}`,
      );
    });
  });

  describe("Spacing after dividers", () => {
    it("should have only one blank line between divider and following paragraph", () => {
      const markdown = new Markdown(
        `hello world

---

again, hello world`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      const dividerIndex = plainLines.findIndex((line) => line.includes("─"));
      assert.ok(dividerIndex !== -1, "Should have divider");

      const afterDivider = plainLines.slice(dividerIndex + 1);
      const emptyLineCount = afterDivider.findIndex((line) => line !== "");

      assert.strictEqual(
        emptyLineCount,
        1,
        `Expected 1 empty line after divider, but found ${emptyLineCount}. Lines after divider: ${JSON.stringify(afterDivider.slice(0, 5))}`,
      );
    });
  });

  describe("Spacing after headings", () => {
    it("should have only one blank line between heading and following paragraph", () => {
      const markdown = new Markdown(
        `# Hello

This is a paragraph`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      const headingIndex = plainLines.findIndex((line) =>
        line.includes("Hello"),
      );
      assert.ok(headingIndex !== -1, "Should have heading");

      const afterHeading = plainLines.slice(headingIndex + 1);
      const emptyLineCount = afterHeading.findIndex((line) => line !== "");

      assert.strictEqual(
        emptyLineCount,
        1,
        `Expected 1 empty line after heading, but found ${emptyLineCount}. Lines after heading: ${JSON.stringify(afterHeading.slice(0, 5))}`,
      );
    });
  });

  describe("Spacing after blockquotes", () => {
    it("should have only one blank line between blockquote and following paragraph", () => {
      const markdown = new Markdown(
        `hello world

> This is a quote

again, hello world`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      const quoteIndex = plainLines.findIndex((line) =>
        line.includes("This is a quote"),
      );
      assert.ok(quoteIndex !== -1, "Should have blockquote");

      const afterQuote = plainLines.slice(quoteIndex + 1);
      const emptyLineCount = afterQuote.findIndex((line) => line !== "");

      assert.strictEqual(
        emptyLineCount,
        1,
        `Expected 1 empty line after blockquote, but found ${emptyLineCount}. Lines after quote: ${JSON.stringify(afterQuote.slice(0, 5))}`,
      );
    });
  });

  describe("Blockquotes with multiline content", () => {
    it("should apply consistent styling to all lines in lazy continuation blockquote", () => {
      // Markdown "lazy continuation" - second line without > is still part of the quote
      const markdown = new Markdown(
        `>Foo
bar`,
        0,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.magenta(text), // This should NOT be applied to blockquotes
        },
      );

      const lines = markdown.render(80);

      // Both lines should have the quote border
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const quotedLines = plainLines.filter((line) => line.startsWith("│ "));
      assert.strictEqual(
        quotedLines.length,
        2,
        `Expected 2 quoted lines, got: ${JSON.stringify(plainLines)}`,
      );

      // Both lines should have italic (from theme.quote styling)
      const fooLine = lines.find((line) => line.includes("Foo"));
      const barLine = lines.find((line) => line.includes("bar"));
      assert.ok(fooLine, "Should have Foo line");
      assert.ok(barLine, "Should have bar line");

      // Check that both have italic (\x1b[3m) - blockquotes use theme styling, not default message color
      assert.ok(
        fooLine?.includes("\x1b[3m"),
        `Foo line should have italic: ${fooLine}`,
      );
      assert.ok(
        barLine?.includes("\x1b[3m"),
        `bar line should have italic: ${barLine}`,
      );

      // Blockquotes should NOT have the default message color (magenta)
      assert.ok(
        !fooLine?.includes("\x1b[35m"),
        `Foo line should NOT have magenta color: ${fooLine}`,
      );
      assert.ok(
        !barLine?.includes("\x1b[35m"),
        `bar line should NOT have magenta color: ${barLine}`,
      );
    });

    it("should apply consistent styling to explicit multiline blockquote", () => {
      const markdown = new Markdown(
        `>Foo
>bar`,
        0,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.cyan(text), // This should NOT be applied to blockquotes
        },
      );

      const lines = markdown.render(80);

      // Both lines should have the quote border
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const quotedLines = plainLines.filter((line) => line.startsWith("│ "));
      assert.strictEqual(
        quotedLines.length,
        2,
        `Expected 2 quoted lines, got: ${JSON.stringify(plainLines)}`,
      );

      // Both lines should have italic (from theme.quote styling)
      const fooLine = lines.find((line) => line.includes("Foo"));
      const barLine = lines.find((line) => line.includes("bar"));
      assert.ok(
        fooLine?.includes("\x1b[3m"),
        `Foo line should have italic: ${fooLine}`,
      );
      assert.ok(
        barLine?.includes("\x1b[3m"),
        `bar line should have italic: ${barLine}`,
      );

      // Blockquotes should NOT have the default message color (cyan)
      assert.ok(
        !fooLine?.includes("\x1b[36m"),
        `Foo line should NOT have cyan color: ${fooLine}`,
      );
      assert.ok(
        !barLine?.includes("\x1b[36m"),
        `bar line should NOT have cyan color: ${barLine}`,
      );
    });

    it("should render list content inside blockquotes", () => {
      const markdown = new Markdown(
        `> 1. bla bla
> - nested bullet`,
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const quotedLines = plainLines.filter((line) => line.startsWith("│ "));

      assert.ok(
        quotedLines.some((line) => line.includes("1. bla bla")),
        `Missing ordered list item: ${JSON.stringify(quotedLines)}`,
      );
      assert.ok(
        quotedLines.some((line) => line.includes("- nested bullet")),
        `Missing unordered list item: ${JSON.stringify(quotedLines)}`,
      );
    });

    it("should wrap long blockquote lines and add border to each wrapped line", () => {
      const longText =
        "This is a very long blockquote line that should wrap to multiple lines when rendered";
      const markdown = new Markdown(
        `> ${longText}`,
        0,
        0,
        defaultMarkdownTheme,
      );

      // Render at narrow width to force wrapping
      const lines = markdown.render(30);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // Filter to non-empty lines (exclude trailing blank line after blockquote)
      const contentLines = plainLines.filter((line) => line.length > 0);

      // Should have multiple lines due to wrapping
      assert.ok(
        contentLines.length > 1,
        `Expected multiple wrapped lines, got: ${JSON.stringify(contentLines)}`,
      );

      // Every content line should start with the quote border
      for (const line of contentLines) {
        assert.ok(
          line.startsWith("│ "),
          `Wrapped line should have quote border: "${line}"`,
        );
      }

      // All content should be preserved
      const allText = contentLines.join(" ");
      assert.ok(allText.includes("very long"), "Should preserve 'very long'");
      assert.ok(allText.includes("blockquote"), "Should preserve 'blockquote'");
      assert.ok(allText.includes("multiple"), "Should preserve 'multiple'");
    });

    it("should properly indent wrapped blockquote lines with styling", () => {
      const markdown = new Markdown(
        "> This is styled text that is long enough to wrap",
        0,
        0,
        defaultMarkdownTheme,
        {
          color: (text) => chalk.yellow(text), // This should NOT be applied to blockquotes
          italic: true,
        },
      );

      const lines = markdown.render(25);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(),
      );

      // Filter to non-empty lines
      const contentLines = plainLines.filter((line) => line.length > 0);

      // All lines should have the quote border
      for (const line of contentLines) {
        assert.ok(
          line.startsWith("│ "),
          `Line should have quote border: "${line}"`,
        );
      }

      // Check that italic is applied (from theme.quote)
      const allOutput = lines.join("\n");
      assert.ok(allOutput.includes("\x1b[3m"), "Should have italic");

      // Blockquotes should NOT have the default message color (yellow)
      assert.ok(
        !allOutput.includes("\x1b[33m"),
        "Should NOT have yellow color from default style",
      );
    });

    it("should render inline formatting inside blockquotes and reapply quote styling after", () => {
      const markdown = new Markdown(
        "> Quote with **bold** and `code`",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );

      // Should have the quote border
      assert.ok(
        plainLines.some((line) => line.startsWith("│ ")),
        "Should have quote border",
      );

      // Content should be preserved
      const allPlain = plainLines.join(" ");
      assert.ok(
        allPlain.includes("Quote with"),
        "Should preserve 'Quote with'",
      );
      assert.ok(allPlain.includes("bold"), "Should preserve 'bold'");
      assert.ok(allPlain.includes("code"), "Should preserve 'code'");

      const allOutput = lines.join("\n");

      // Should have bold styling (\x1b[1m)
      assert.ok(allOutput.includes("\x1b[1m"), "Should have bold styling");

      // Should have code styling (yellow = \x1b[33m from defaultMarkdownTheme)
      assert.ok(
        allOutput.includes("\x1b[33m"),
        "Should have code styling (yellow)",
      );

      // Should have italic from quote styling (\x1b[3m)
      assert.ok(
        allOutput.includes("\x1b[3m"),
        "Should have italic from quote styling",
      );
    });
  });

  describe("Links", () => {
    it("should not duplicate URL for autolinked emails", () => {
      const markdown = new Markdown(
        "Contact user@example.com for help",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join(" ");

      // Should contain the email once, not duplicated with mailto:
      assert.ok(
        joinedPlain.includes("user@example.com"),
        "Should contain email",
      );
      assert.ok(
        !joinedPlain.includes("mailto:"),
        "Should not show mailto: prefix for autolinked emails",
      );
    });

    it("should not duplicate URL for bare URLs", () => {
      const markdown = new Markdown(
        "Visit https://example.com for more",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join(" ");

      // URL should appear only once
      const urlCount = (joinedPlain.match(/https:\/\/example\.com/g) || [])
        .length;
      assert.strictEqual(urlCount, 1, "URL should appear exactly once");
    });

    it("should show URL for explicit markdown links with different text", () => {
      const markdown = new Markdown(
        "[click here](https://example.com)",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join(" ");

      // Should show both link text and URL
      assert.ok(joinedPlain.includes("click here"), "Should contain link text");
      assert.ok(
        joinedPlain.includes("(https://example.com)"),
        "Should show URL in parentheses",
      );
    });

    it("should show URL for explicit mailto links with different text", () => {
      const markdown = new Markdown(
        "[Email me](mailto:test@example.com)",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join(" ");

      // Should show both link text and mailto URL
      assert.ok(joinedPlain.includes("Email me"), "Should contain link text");
      assert.ok(
        joinedPlain.includes("(mailto:test@example.com)"),
        "Should show mailto URL in parentheses",
      );
    });
  });

  describe("HTML-like tags in text", () => {
    it("should render content with HTML-like tags as text", () => {
      // When the model emits something like <thinking>content</thinking> in regular text,
      // marked might treat it as HTML and hide the content
      const markdown = new Markdown(
        "This is text with <thinking>hidden content</thinking> that should be visible",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join(" ");

      // The content inside the tags should be visible
      assert.ok(
        joinedPlain.includes("hidden content") ||
          joinedPlain.includes("<thinking>"),
        "Should render HTML-like tags or their content as text, not hide them",
      );
    });

    it("should render HTML tags in code blocks correctly", () => {
      const markdown = new Markdown(
        "```html\n<div>Some HTML</div>\n```",
        0,
        0,
        defaultMarkdownTheme,
      );

      const lines = markdown.render(80);
      const plainLines = lines.map((line) =>
        line.replace(/\x1b\[[0-9;]*m/g, ""),
      );
      const joinedPlain = plainLines.join("\n");

      // HTML in code blocks should be visible
      assert.ok(
        joinedPlain.includes("<div>") && joinedPlain.includes("</div>"),
        "Should render HTML in code blocks",
      );
    });
  });
});
