#![ cfg( feature = "enabled" ) ]

//! Tests for `DecoratedText` type — covers all public invariants.
//!
//! ## Test Matrix
//!
//! | ID | Invariant Tested |
//! |----|-----------------|
//! | t01 | `From<String>` produces `color: None` |
//! | t02 | `From<&str>` produces `color: None` |
//! | t03 | `.with_color()` sets color field |
//! | t04 | `.render()` uncolored returns raw text with no escape codes |
//! | t05 | `.render()` colored starts with code and ends with `\x1b[0m` |
//! | t06 | `From<DecoratedText> for String` equals `.render()` |
//! | t07 | `Display` output equals `.render()` |
//! | t08 | `Default` produces empty text, no color |
//! | t09 | Chaining: full round-trip with color |
//! | t10 | Round-trip: `String -> DT -> String` preserves text when uncolored |
//! | t11 | Empty text uncolored: `.render()` returns `""` |
//! | t12 | `is_colored()` returns true only when color is set |
//! | t13 | `is_empty()` checks text field, not render output |
//! | t14 | `.with_color()` second call overrides first |
//! | t15 | `.render()` on empty colored text emits `color + reset` with no content |
//! | t16 | `.render()` on multiline uncolored preserves `\n` verbatim |
//! | t17 | `.render()` on multiline colored emits exactly ONE reset — not per-line |
//! | t18 | Serde round-trip: serialize → deserialize preserves both fields |
//! | t19 | `Clone` produces equal value for both plain and colored |
//! | t20 | `PartialEq`/`Eq` — same values equal, different values unequal |
//! | t21 | `Debug` format includes type name and field values |
//! | t22 | `Color::Yellow.to_ansi()` returns `"\x1b[33m"` (spot-check SGR code) |
//! | t23 | All 16 4-bit `Color` variants produce correct SGR codes |
//! | t24 | `Color::Ansi256(n).to_ansi()` returns `"\x1b[38;5;N m"` format |
//! | t25 | `Color::Rgb(r,g,b).to_ansi()` returns `"\x1b[38;2;r;g;bm"` format |
//! | t26 | `Color` derives `Clone`, `Copy`, `PartialEq`, `Debug` |
//! | t27 | `with_color_named` round-trip: `render()` equals explicit `with_color(to_ansi())` |
//! | t28 | `with_color_named` sets `is_colored()` true |
//! | t29 | `render_html()` on plain text returns escaped text, no `<span>` (`html_support`) |
//! | t30 | `render_html()` with named yellow → `<span style="color: yellow">` (`html_support`) |
//! | t31 | `render_html()` with `Rgb` named → `<span style="color: rgb(r, g, b)">` (`html_support`) |
//! | t32 | `render_html()` escapes `<`, `>`, `&` in text content (`html_support`) |
//! | t33 | `render_html()` with raw-string color produces no `<span>` (design boundary, `html_support`) |
//! | t34 | `render_html()` with `Ansi256` named → `<span style="color: var(--ansi256-N)">` (`html_support`) |
//! | t35 | `with_color` after `with_color_named` clears `named_color` → `render_html()` produces no span (`html_support`) |
//! | t36 | `render_html()` on empty text with named color → span with empty body (`html_support`) |
//! | t37 | `render_html()` does NOT escape `"` or `'` (only `&`, `<`, `>` are HTML body escapes) (`html_support`) |
//! | t38 | All 8 Bright variants produce same `to_css()` keyword as their normal counterparts (`html_support`) |
//! | t39 | `Color::Ansi256(0)` and `Ansi256(255)` boundary values in `to_css()` (`html_support`) |
//! | t40 | `Color::Rgb(0,0,0)` and `Rgb(255,255,255)` boundary values in `to_css()` (`html_support`) |
//! | t41 | `render_html()` preserves Unicode text unchanged (`html_support`) |
//! | t42 | `with_color_named` called twice — second call's color wins in `render_html()` (`html_support`) |
//! | t43 | All 8 bright 4-bit `Color` variants produce correct SGR 90-97 codes |
//! | t44 | `with_color("")` — `is_colored()` = true but render emits text + RESET, no color prefix |
//! | t45 | `render()` on uncolored text containing embedded `\x1b` escapes passes through verbatim |

use color_tools::DecoratedText;

// =============================================================================
// t01 — From<String> produces color: None
// =============================================================================

#[ test ]
fn t01_from_string_no_color()
{
  let ct = DecoratedText::from( "hello".to_string() );
  assert_eq!( ct.text, "hello" );
  assert_eq!( ct.color, None, "From<String> must not set a color" );
}

// =============================================================================
// t02 — From<&str> produces color: None
// =============================================================================

#[ test ]
fn t02_from_str_no_color()
{
  let ct : DecoratedText = "world".into();
  assert_eq!( ct.text, "world" );
  assert_eq!( ct.color, None, "From<&str> must not set a color" );
}

// =============================================================================
// t03 — .with_color() sets color field
// =============================================================================

#[ test ]
fn t03_with_color_sets_field()
{
  let ct = DecoratedText::from( "x" ).with_color( "\x1b[31m" );
  assert_eq!( ct.color, Some( "\x1b[31m".to_string() ) );
  assert_eq!( ct.text, "x" );
}

// =============================================================================
// t04 — .render() uncolored: raw text, no escape codes
// =============================================================================

#[ test ]
fn t04_render_uncolored_no_escape()
{
  let ct = DecoratedText::from( "plain" );
  let rendered = ct.render();
  assert_eq!( rendered, "plain", "uncolored render must equal raw text" );
  assert!( !rendered.contains( '\x1b' ), "no escape codes in uncolored render" );
}

// =============================================================================
// t05 — .render() colored: starts with code, ends with reset
// =============================================================================

#[ test ]
fn t05_render_colored_has_reset()
{
  let ct = DecoratedText::from( "warn" ).with_color( "\x1b[33m" );
  let rendered = ct.render();
  assert!( rendered.starts_with( "\x1b[33m" ), "must start with color code" );
  assert!( rendered.contains( "warn" ), "must contain the text" );
  assert!( rendered.ends_with( "\x1b[0m" ), "must end with reset" );
}

// =============================================================================
// t06 — From<DecoratedText> for String delegates to .render()
// =============================================================================

#[ test ]
fn t06_from_ct_to_string_is_render()
{
  let ct = DecoratedText::from( "msg" ).with_color( "\x1b[32m" );
  let expected = ct.render();
  let actual : String = ct.into();
  assert_eq!( actual, expected );
}

// =============================================================================
// t07 — Display output equals .render()
// =============================================================================

#[ test ]
fn t07_display_equals_render()
{
  let ct = DecoratedText::from( "display-test" ).with_color( "\x1b[34m" );
  assert_eq!( format!( "{ct}" ), ct.render() );
}

// =============================================================================
// t08 — Default: empty text, no color, render returns ""
// =============================================================================

#[ test ]
fn t08_default_empty()
{
  let ct = DecoratedText::default();
  assert_eq!( ct.text, "" );
  assert_eq!( ct.color, None );
  assert_eq!( ct.render(), "" );
}

// =============================================================================
// t09 — Chaining: color attach then render produces correct escape sequence
// =============================================================================

#[ test ]
fn t09_chain_color_render()
{
  let result = DecoratedText::from( "x" ).with_color( "\x1b[31m" ).render();
  assert_eq!( result, "\x1b[31mx\x1b[0m" );
}

// =============================================================================
// t10 — Round-trip: String → DecoratedText → String preserves text (uncolored)
// =============================================================================

#[ test ]
fn t10_roundtrip_uncolored()
{
  let original = "hello".to_string();
  let ct = DecoratedText::from( original.clone() );
  let back : String = ct.into();
  assert_eq!( back, original );
}

// =============================================================================
// t11 — Empty text uncolored: render returns ""
// =============================================================================

#[ test ]
fn t11_empty_text_render()
{
  let ct = DecoratedText::from( "" );
  assert_eq!( ct.render(), "" );
}

// =============================================================================
// t12 — is_colored() returns true only when color is set
// =============================================================================

#[ test ]
fn t12_is_colored()
{
  let plain = DecoratedText::from( "x" );
  assert!( !plain.is_colored(), "uncolored must return false" );
  let colored = plain.with_color( "\x1b[35m" );
  assert!( colored.is_colored(), "colored must return true" );
}

// =============================================================================
// t13 — is_empty() checks text field, not render output
// =============================================================================

#[ test ]
fn t13_is_empty_checks_text()
{
  let empty_uncolored = DecoratedText::from( "" );
  assert!( empty_uncolored.is_empty(), "empty uncolored must be empty" );

  // Even with color set, empty text is considered empty
  let empty_colored = DecoratedText::from( "" ).with_color( "\x1b[33m" );
  assert!( empty_colored.is_empty(), "empty colored must also be empty" );

  let nonempty = DecoratedText::from( "x" );
  assert!( !nonempty.is_empty(), "non-empty text must not be empty" );
}

// =============================================================================
// t14 — .with_color() second call overrides the first
// =============================================================================

#[ test ]
fn t14_with_color_override()
{
  let ct = DecoratedText::from( "text" )
    .with_color( "\x1b[31m" )   // red — first call
    .with_color( "\x1b[33m" );  // yellow — second call must win
  assert_eq!(
    ct.color,
    Some( "\x1b[33m".to_string() ),
    "second with_color call must override the first",
  );
  assert!(
    ct.render().starts_with( "\x1b[33m" ),
    "render must use the overriding color",
  );
  assert!(
    !ct.render().contains( "\x1b[31m" ),
    "overridden color must not appear in render output",
  );
}

// =============================================================================
// t15 — .render() on empty colored text produces color prefix followed by reset
//
// is_empty() == true causes the formatter to skip rendering the detail line.
// render() itself does not check is_empty(); it always applies the color wrap.
// This test documents that design distinction.
// =============================================================================

#[ test ]
fn t15_render_empty_colored_text()
{
  let ct = DecoratedText::from( "" ).with_color( "\x1b[33m" );
  assert!( ct.is_empty(), "empty colored text must report is_empty == true" );
  assert_eq!(
    ct.render(),
    "\x1b[33m\x1b[0m",
    "render of empty colored text must produce color+reset with no content between them",
  );
}

// =============================================================================
// t16 — .render() on multiline uncolored text preserves newlines verbatim
// =============================================================================

#[ test ]
fn t16_render_multiline_uncolored()
{
  let ct = DecoratedText::from( "line1\nline2\nline3" );
  let rendered = ct.render();
  assert_eq!( rendered, "line1\nline2\nline3", "uncolored render must preserve newlines verbatim" );
  assert!( !rendered.contains( '\x1b' ), "no ANSI codes must appear in uncolored multiline render" );
}

// =============================================================================
// t17 — .render() on multiline colored text places exactly ONE reset at the end
//
// render() wraps the whole text block — it is NOT per-line aware.
// Per-line ANSI wrapping is the formatter's responsibility (see table.rs Algorithm 3).
// This test documents that design boundary explicitly.
// =============================================================================

#[ test ]
fn t17_render_multiline_colored_single_reset()
{
  let ct = DecoratedText::from( "line1\nline2" ).with_color( "\x1b[33m" );
  let rendered = ct.render();
  assert!( rendered.starts_with( "\x1b[33m" ), "must begin with color prefix" );
  assert!( rendered.ends_with( "\x1b[0m" ), "must end with ANSI reset" );
  assert!( rendered.contains( "line1\nline2" ), "text content including newline must be preserved" );
  assert_eq!(
    rendered.matches( "\x1b[0m" ).count(),
    1,
    "render() produces exactly ONE ANSI reset for the whole text — per-line wrapping is the formatter's job",
  );
}

// =============================================================================
// t18 — Serde round-trip: serialize → deserialize preserves both fields
//
// Tests both plain (color: None) and colored (color: Some) variants.
// Requires the `serde_support` feature to be enabled.
// =============================================================================

#[ cfg( feature = "serde_support" ) ]
#[ test ]
fn t18_serde_roundtrip()
{
  // Plain variant: color field must survive as None
  let plain = DecoratedText::from( "hello" );
  let json = serde_json::to_string( &plain ).expect( "serialize plain must succeed" );
  let restored : DecoratedText = serde_json::from_str( &json ).expect( "deserialize plain must succeed" );
  assert_eq!( restored.text, plain.text, "text field must survive serde round-trip" );
  assert_eq!( restored.color, plain.color, "color field must survive serde round-trip as None" );

  // Colored variant: color field must survive as Some(...)
  let colored = DecoratedText::from( "warn" ).with_color( "\x1b[33m" );
  let json = serde_json::to_string( &colored ).expect( "serialize colored must succeed" );
  let restored : DecoratedText = serde_json::from_str( &json ).expect( "deserialize colored must succeed" );
  assert_eq!( restored.text, colored.text, "text field must survive serde round-trip" );
  assert_eq!( restored.color, colored.color, "color field must survive serde round-trip as Some" );
  assert_eq!( restored.render(), colored.render(), "render output must be identical after round-trip" );
}

// =============================================================================
// t19 — Clone: cloned value equals original for both variants
// =============================================================================

#[ test ]
fn t19_clone()
{
  let plain = DecoratedText::from( "hello" );
  assert_eq!( plain.clone(), plain, "clone of plain must equal original" );

  let colored = DecoratedText::from( "x" ).with_color( "\x1b[31m" );
  assert_eq!( colored.clone(), colored, "clone of colored must equal original" );
}

// =============================================================================
// t20 — PartialEq/Eq: same values equal, different values unequal
// =============================================================================

#[ test ]
fn t20_partial_eq()
{
  let a = DecoratedText::from( "same" );
  let b = DecoratedText::from( "same" );
  assert_eq!( a, b, "same text and color must be equal" );

  let c = DecoratedText::from( "same" ).with_color( "\x1b[31m" );
  assert_ne!( a, c, "plain vs colored must differ" );

  let d = DecoratedText::from( "diff" );
  assert_ne!( a, d, "different text must differ" );
}

// =============================================================================
// t21 — Debug: format output contains field values
// =============================================================================

#[ test ]
fn t21_debug_format()
{
  let ct = DecoratedText::from( "hello" ).with_color( "\x1b[33m" );
  let dbg = format!( "{ct:?}" );
  assert!( dbg.contains( "hello" ), "Debug must show text field" );
  assert!( dbg.contains( "DecoratedText" ), "Debug must show type name" );
}

// =============================================================================
// t22 — Color::Yellow produces exact 4-bit SGR sequence
// =============================================================================

#[ test ]
fn t22_color_yellow_to_ansi()
{
  let got = color_tools::Color::Yellow.to_ansi();
  assert_eq!( got, "\x1b[33m", "Yellow must produce SGR code 33" );
}

// =============================================================================
// t23 — Color covers all 8 standard 4-bit SGR codes
// =============================================================================

#[ test ]
fn t23_color_4bit_sgr_codes()
{
  use color_tools::Color;
  assert_eq!( Color::Black.to_ansi(),   "\x1b[30m" );
  assert_eq!( Color::Red.to_ansi(),     "\x1b[31m" );
  assert_eq!( Color::Green.to_ansi(),   "\x1b[32m" );
  assert_eq!( Color::Yellow.to_ansi(),  "\x1b[33m" );
  assert_eq!( Color::Blue.to_ansi(),    "\x1b[34m" );
  assert_eq!( Color::Magenta.to_ansi(), "\x1b[35m" );
  assert_eq!( Color::Cyan.to_ansi(),    "\x1b[36m" );
  assert_eq!( Color::White.to_ansi(),   "\x1b[37m" );
}

// =============================================================================
// t24 — Color::Ansi256 produces correct 256-color SGR format
// =============================================================================

#[ test ]
fn t24_color_ansi256_format()
{
  use color_tools::Color;
  assert_eq!( Color::Ansi256( 208 ).to_ansi(), "\x1b[38;5;208m" );
  assert_eq!( Color::Ansi256( 0 ).to_ansi(),   "\x1b[38;5;0m" );
  assert_eq!( Color::Ansi256( 255 ).to_ansi(), "\x1b[38;5;255m" );
}

// =============================================================================
// t25 — Color::Rgb produces correct 24-bit true-color SGR format
// =============================================================================

#[ test ]
fn t25_color_rgb_format()
{
  use color_tools::Color;
  assert_eq!( Color::Rgb( 255, 165, 0 ).to_ansi(),     "\x1b[38;2;255;165;0m" );
  assert_eq!( Color::Rgb( 0, 0, 0 ).to_ansi(),         "\x1b[38;2;0;0;0m" );
  assert_eq!( Color::Rgb( 255, 255, 255 ).to_ansi(),   "\x1b[38;2;255;255;255m" );
}

// =============================================================================
// t26 — Color derives: Debug, Clone, Copy, PartialEq, Eq
// =============================================================================

#[ test ]
fn t26_color_derives()
{
  use color_tools::Color;
  let a = Color::Yellow;
  let b = a; // Copy — no clone needed
  assert_eq!( a, b );
  assert_ne!( Color::Red, Color::Green );
  let dbg = format!( "{a:?}" );
  assert!( dbg.contains( "Yellow" ) );
}

// =============================================================================
// t27 — with_color_named round-trip: render() matches with_color(raw)
// =============================================================================

#[ test ]
fn t27_with_color_named_round_trip()
{
  use color_tools::{ DecoratedText, Color };
  let named = DecoratedText::from( "warn" ).with_color_named( Color::Yellow );
  let raw   = DecoratedText::from( "warn" ).with_color( "\x1b[33m" );
  assert_eq!(
    named.render(),
    raw.render(),
    "with_color_named must produce same render as with_color"
  );
  assert_eq!( named.render(), "\x1b[33mwarn\x1b[0m" );
}

// =============================================================================
// t28 — with_color_named: result is_colored and not is_empty
// =============================================================================

#[ test ]
fn t28_with_color_named_is_colored()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "text" ).with_color_named( Color::Red );
  assert!( ct.is_colored(), "with_color_named must set is_colored" );
  assert!( !ct.is_empty(),  "non-empty text must not be empty" );
}

// =============================================================================
// t29 — render_html: plain text returns HTML-escaped content, no span
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t29_render_html_plain_no_span()
{
  use color_tools::DecoratedText;
  let ct = DecoratedText::from( "hello world" );
  assert_eq!( ct.render_html(), "hello world" );
}

// =============================================================================
// t30 — render_html: named Yellow color produces span with CSS yellow
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t30_render_html_yellow_span()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "warn" ).with_color_named( Color::Yellow );
  assert_eq!( ct.render_html(), "<span style=\"color: yellow\">warn</span>" );
}

// =============================================================================
// t31 — render_html: Rgb color produces rgb(...) CSS value
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t31_render_html_rgb_span()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "ok" ).with_color_named( Color::Rgb( 255, 165, 0 ) );
  assert_eq!( ct.render_html(), "<span style=\"color: rgb(255, 165, 0)\">ok</span>" );
}

// =============================================================================
// t32 — render_html: text with HTML special chars is escaped
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t32_render_html_escapes_special_chars()
{
  use color_tools::DecoratedText;
  let ct = DecoratedText::from( "<b>test</b> & more" );
  assert_eq!( ct.render_html(), "&lt;b&gt;test&lt;/b&gt; &amp; more" );
}

// =============================================================================
// t33 — render_html: raw-string color produces plain text (no span)
//
// Design boundary: with_color(raw_str) cannot produce a <span> because CSS
// requires semantic Color, not raw ANSI bytes. Only with_color_named sets
// named_color; with_color(raw_str) leaves named_color as None.
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t33_render_html_raw_color_no_span()
{
  use color_tools::DecoratedText;
  let ct = DecoratedText::from( "text" ).with_color( "\x1b[33m" );
  assert_eq!( ct.render_html(), "text" );
  assert!( !ct.render_html().contains( "<span" ), "raw-string color must not produce span" );
}

// =============================================================================
// t34 — render_html: 256-color produces CSS custom property
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t34_render_html_ansi256_css_var()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "info" ).with_color_named( Color::Ansi256( 208 ) );
  assert_eq!( ct.render_html(), "<span style=\"color: var(--ansi256-208)\">info</span>" );
}

// =============================================================================
// t35 — with_color after with_color_named must clear named_color
//
// Root cause of bug: with_color() did not clear the named_color field, so
// render_html() would still produce a span from the old named color even after
// the caller overrode the color with a raw ANSI string.
// test_kind: bug_reproducer(issue-none)
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t35_with_color_after_with_color_named_clears_span()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "text" )
    .with_color_named( Color::Yellow )
    .with_color( "\x1b[31m" );
  // render() must use the raw ANSI red
  assert!( ct.render().starts_with( "\x1b[31m" ), "raw color must be used by render()" );
  // render_html() must produce NO span — raw ANSI string carries no CSS info,
  // and named_color must have been cleared by with_color
  assert_eq!( ct.render_html(), "text",
    "with_color after with_color_named must clear named_color" );
}

// =============================================================================
// t36 — render_html: named color + empty text produces empty span
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t36_render_html_empty_text_with_named_color()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "" ).with_color_named( Color::Blue );
  assert_eq!( ct.render_html(), "<span style=\"color: blue\"></span>" );
}

// =============================================================================
// t37 — render_html does NOT escape " or ' (only &, <, > are HTML body escapes)
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t37_render_html_does_not_escape_quotes()
{
  use color_tools::DecoratedText;
  let ct = DecoratedText::from( "say \"hi\" and 'bye'" );
  // In HTML body text, double and single quotes need no escaping
  assert_eq!( ct.render_html(), "say \"hi\" and 'bye'" );
}

// =============================================================================
// t38 — All 8 Bright Color variants map to same CSS keyword as normal variant
//
// CSS has no "bright" semantic equivalents; the distinction is terminal-only
// (SGR 90–97 vs 30–37). render_html() intentionally uses the same keyword.
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t38_bright_variants_css_same_as_normal()
{
  use color_tools::Color;
  assert_eq!( Color::BrightBlack.to_css(),   Color::Black.to_css()   );
  assert_eq!( Color::BrightRed.to_css(),     Color::Red.to_css()     );
  assert_eq!( Color::BrightGreen.to_css(),   Color::Green.to_css()   );
  assert_eq!( Color::BrightYellow.to_css(),  Color::Yellow.to_css()  );
  assert_eq!( Color::BrightBlue.to_css(),    Color::Blue.to_css()    );
  assert_eq!( Color::BrightMagenta.to_css(), Color::Magenta.to_css() );
  assert_eq!( Color::BrightCyan.to_css(),    Color::Cyan.to_css()    );
  assert_eq!( Color::BrightWhite.to_css(),   Color::White.to_css()   );
}

// =============================================================================
// t39 — Ansi256 boundary values: 0 and 255
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t39_ansi256_boundary_values_css()
{
  use color_tools::Color;
  assert_eq!( Color::Ansi256( 0 ).to_css(),   "var(--ansi256-0)"   );
  assert_eq!( Color::Ansi256( 255 ).to_css(), "var(--ansi256-255)" );
}

// =============================================================================
// t40 — Rgb boundary values: (0,0,0) and (255,255,255)
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t40_rgb_boundary_values_css()
{
  use color_tools::Color;
  assert_eq!( Color::Rgb( 0, 0, 0 ).to_css(),         "rgb(0, 0, 0)"         );
  assert_eq!( Color::Rgb( 255, 255, 255 ).to_css(), "rgb(255, 255, 255)" );
}

// =============================================================================
// t41 — render_html preserves Unicode text unchanged
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t41_render_html_preserves_unicode()
{
  use color_tools::DecoratedText;
  let ct = DecoratedText::from( "héllo 日本語 😀" );
  assert_eq!( ct.render_html(), "héllo 日本語 😀" );
}

// =============================================================================
// t42 — with_color_named called twice: second call's color wins
// =============================================================================

#[ cfg( feature = "html_support" ) ]
#[ test ]
fn t42_with_color_named_twice_second_wins()
{
  use color_tools::{ DecoratedText, Color };
  let ct = DecoratedText::from( "x" )
    .with_color_named( Color::Red )
    .with_color_named( Color::Blue );
  assert_eq!( ct.render_html(), "<span style=\"color: blue\">x</span>" );
}

// =============================================================================
// t43 — All 8 bright 4-bit Color variants produce correct SGR 90-97 codes
//
// t23 covers the 8 normal variants (SGR 30-37); this test covers the 8 bright
// variants (SGR 90-97). Together they prove all 16 4-bit Color variants are
// correctly mapped.
// =============================================================================

#[ test ]
fn t43_color_bright_4bit_sgr_codes()
{
  use color_tools::Color;
  assert_eq!( Color::BrightBlack.to_ansi(),   "\x1b[90m" );
  assert_eq!( Color::BrightRed.to_ansi(),     "\x1b[91m" );
  assert_eq!( Color::BrightGreen.to_ansi(),   "\x1b[92m" );
  assert_eq!( Color::BrightYellow.to_ansi(),  "\x1b[93m" );
  assert_eq!( Color::BrightBlue.to_ansi(),    "\x1b[94m" );
  assert_eq!( Color::BrightMagenta.to_ansi(), "\x1b[95m" );
  assert_eq!( Color::BrightCyan.to_ansi(),    "\x1b[96m" );
  assert_eq!( Color::BrightWhite.to_ansi(),   "\x1b[97m" );
}

// =============================================================================
// t44 — with_color("") sets is_colored() = true but emits text + RESET only
//
// Design boundary: `is_colored()` checks `self.color.is_some()`, so `Some("")`
// returns true. `render()` then emits `"" + text + "\x1b[0m"` — a RESET with no
// preceding color code. Callers that check `is_colored()` to infer whether any
// actual color is present will be misled. This test documents the distinction.
// =============================================================================

#[ test ]
fn t44_with_color_empty_string_design_boundary()
{
  let ct = DecoratedText::from( "text" ).with_color( "" );

  // is_colored() tests Some/None; an empty string is still Some
  assert!(
    ct.is_colored(),
    "with_color(\"\") stores Some(\"\"), so is_colored must report true"
  );
  // render() emits color(="") + text + RESET — no visible color, but reset present
  assert_eq!(
    ct.render(),
    "text\x1b[0m",
    "with_color(\"\") must produce text + RESET with no color prefix"
  );
  // is_empty tests text, not the color or render output
  assert!( !ct.is_empty(), "non-empty text must not report is_empty" );
}

// =============================================================================
// t45 — render() on uncolored text with embedded ANSI escapes passes verbatim
//
// render() is a pure string concatenation; it does not parse or transform the
// text field. Embedded ANSI sequences must survive unchanged.
// =============================================================================

#[ test ]
fn t45_render_preserves_embedded_escapes()
{
  let text_with_escapes = "\x1b[31mred\x1b[0m normal";
  let ct = DecoratedText::from( text_with_escapes );

  // Plain render: text returned as-is with no outer wrapping
  assert_eq!(
    ct.render(),
    text_with_escapes,
    "plain render must pass embedded escapes verbatim"
  );
  assert!(
    !ct.is_colored(),
    "no outer color was attached"
  );
}
