Skip to main content

perl_dap_variables/
renderer.rs

1//! Variable renderer for DAP protocol.
2//!
3//! This module provides the [`VariableRenderer`] trait and [`PerlVariableRenderer`]
4//! implementation for converting Perl values into DAP-compatible variable representations.
5
6use crate::PerlValue;
7use serde::{Deserialize, Serialize};
8
9/// A rendered variable for the DAP protocol.
10///
11/// This struct represents a variable in a format suitable for the Debug Adapter Protocol,
12/// supporting lazy expansion of complex data structures.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct RenderedVariable {
16    /// The name of the variable (e.g., "$foo", "@bar", "%hash")
17    pub name: String,
18
19    /// The string representation of the value
20    pub value: String,
21
22    /// The type of the variable (e.g., "SCALAR", "ARRAY", "HASH")
23    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
24    pub type_name: Option<String>,
25
26    /// Reference ID for lazy expansion (0 = not expandable)
27    pub variables_reference: i64,
28
29    /// Number of named children (for objects/hashes)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub named_variables: Option<i64>,
32
33    /// Number of indexed children (for arrays)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub indexed_variables: Option<i64>,
36
37    /// Presentation hint for the UI
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub presentation_hint: Option<VariablePresentationHint>,
40
41    /// Memory address (if available)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub memory_reference: Option<String>,
44}
45
46/// Presentation hints for variable display in the DAP UI.
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct VariablePresentationHint {
50    /// The kind of variable (e.g., "property", "method", "class")
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub kind: Option<String>,
53
54    /// Attributes (e.g., "static", "constant", "readOnly")
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub attributes: Option<Vec<String>>,
57
58    /// Visibility (e.g., "public", "private", "protected")
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub visibility: Option<String>,
61}
62
63impl RenderedVariable {
64    /// Creates a new rendered variable with the given name and value.
65    #[must_use]
66    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
67        Self {
68            name: name.into(),
69            value: value.into(),
70            type_name: None,
71            variables_reference: 0,
72            named_variables: None,
73            indexed_variables: None,
74            presentation_hint: None,
75            memory_reference: None,
76        }
77    }
78
79    /// Sets the type name for this variable.
80    #[must_use]
81    pub fn with_type(mut self, type_name: impl Into<String>) -> Self {
82        self.type_name = Some(type_name.into());
83        self
84    }
85
86    /// Sets the variables reference for lazy expansion.
87    #[must_use]
88    pub fn with_reference(mut self, reference: i64) -> Self {
89        self.variables_reference = reference;
90        self
91    }
92
93    /// Sets the indexed variables count (for arrays).
94    #[must_use]
95    pub fn with_indexed_variables(mut self, count: i64) -> Self {
96        self.indexed_variables = Some(count);
97        self
98    }
99
100    /// Sets the named variables count (for hashes/objects).
101    #[must_use]
102    pub fn with_named_variables(mut self, count: i64) -> Self {
103        self.named_variables = Some(count);
104        self
105    }
106
107    /// Returns true if this variable can be expanded.
108    #[must_use]
109    pub fn is_expandable(&self) -> bool {
110        self.variables_reference != 0
111    }
112}
113
114/// Trait for rendering Perl values into DAP variables.
115///
116/// Implementations of this trait convert [`PerlValue`] instances into
117/// [`RenderedVariable`] structures suitable for the DAP protocol.
118pub trait VariableRenderer {
119    /// Render a Perl value into a DAP variable.
120    ///
121    /// # Arguments
122    ///
123    /// * `name` - The variable name (e.g., "$foo")
124    /// * `value` - The Perl value to render
125    ///
126    /// # Returns
127    ///
128    /// A [`RenderedVariable`] suitable for the DAP protocol.
129    fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable;
130
131    /// Render a Perl value with a specific variables reference ID.
132    ///
133    /// This is used when the value is expandable and needs a reference ID
134    /// for lazy loading of children.
135    ///
136    /// # Arguments
137    ///
138    /// * `name` - The variable name
139    /// * `value` - The Perl value to render
140    /// * `reference_id` - The variables reference ID for expansion
141    fn render_with_reference(
142        &self,
143        name: &str,
144        value: &PerlValue,
145        reference_id: i64,
146    ) -> RenderedVariable;
147
148    /// Render the children of an expandable value.
149    ///
150    /// # Arguments
151    ///
152    /// * `value` - The parent value to expand
153    /// * `start` - The starting index for pagination (0-based)
154    /// * `count` - The maximum number of children to return
155    ///
156    /// # Returns
157    ///
158    /// A vector of rendered child variables.
159    fn render_children(
160        &self,
161        value: &PerlValue,
162        start: usize,
163        count: usize,
164    ) -> Vec<RenderedVariable>;
165}
166
167/// Default Perl variable renderer implementation.
168///
169/// This renderer follows Perl conventions for variable display:
170/// - Strings are quoted
171/// - Arrays show element count
172/// - Hashes show key count
173/// - References show the referent type
174/// - Objects show class name
175#[derive(Debug, Default)]
176pub struct PerlVariableRenderer {
177    /// Maximum string length before truncation
178    max_string_length: usize,
179    /// Maximum array elements to show in preview
180    max_array_preview: usize,
181    /// Maximum hash pairs to show in preview
182    max_hash_preview: usize,
183}
184
185impl PerlVariableRenderer {
186    /// Creates a new Perl variable renderer with default settings.
187    #[must_use]
188    pub fn new() -> Self {
189        Self { max_string_length: 100, max_array_preview: 3, max_hash_preview: 3 }
190    }
191
192    /// Sets the maximum string length before truncation.
193    #[must_use]
194    pub fn with_max_string_length(mut self, length: usize) -> Self {
195        self.max_string_length = length;
196        self
197    }
198
199    /// Sets the maximum array elements in preview.
200    #[must_use]
201    pub fn with_max_array_preview(mut self, count: usize) -> Self {
202        self.max_array_preview = count;
203        self
204    }
205
206    /// Sets the maximum hash pairs in preview.
207    #[must_use]
208    pub fn with_max_hash_preview(mut self, count: usize) -> Self {
209        self.max_hash_preview = count;
210        self
211    }
212
213    /// Formats a scalar string value with quoting and truncation.
214    fn format_string(&self, s: &str) -> String {
215        let truncated = if s.len() > self.max_string_length {
216            // Find a char boundary at or before max_string_length to avoid
217            // panicking on multi-byte UTF-8 sequences.
218            let mut end = self.max_string_length;
219            while end > 0 && !s.is_char_boundary(end) {
220                end -= 1;
221            }
222            format!("{}...", &s[..end])
223        } else {
224            s.to_string()
225        };
226
227        // Escape special characters and quote
228        let escaped = truncated
229            .replace('\\', "\\\\")
230            .replace('"', "\\\"")
231            .replace('\n', "\\n")
232            .replace('\r', "\\r")
233            .replace('\t', "\\t");
234
235        format!("\"{}\"", escaped)
236    }
237
238    /// Formats an array value for preview.
239    fn format_array_preview(&self, elements: &[PerlValue]) -> String {
240        if elements.is_empty() {
241            return "[]".to_string();
242        }
243
244        let preview: Vec<String> = elements
245            .iter()
246            .take(self.max_array_preview)
247            .map(|v| self.format_value_brief(v))
248            .collect();
249
250        let suffix = if elements.len() > self.max_array_preview {
251            format!(", ... ({} total)", elements.len())
252        } else {
253            String::new()
254        };
255
256        format!("[{}{}]", preview.join(", "), suffix)
257    }
258
259    /// Formats a hash value for preview.
260    fn format_hash_preview(&self, pairs: &[(String, PerlValue)]) -> String {
261        if pairs.is_empty() {
262            return "{}".to_string();
263        }
264
265        let preview: Vec<String> = pairs
266            .iter()
267            .take(self.max_hash_preview)
268            .map(|(k, v)| format!("{} => {}", k, self.format_value_brief(v)))
269            .collect();
270
271        let suffix = if pairs.len() > self.max_hash_preview {
272            format!(", ... ({} keys)", pairs.len())
273        } else {
274            String::new()
275        };
276
277        format!("{{{}{}}}", preview.join(", "), suffix)
278    }
279
280    /// Returns the chain of backslash prefixes for nested references.
281    ///
282    /// For `\\\42` (ref to ref to ref to scalar) this returns `"\\\\"` (three backslashes).
283    /// Stops counting after 10 levels to avoid runaway recursion on cyclic-like structures.
284    fn ref_prefix_chain(&self, value: &PerlValue) -> String {
285        let mut depth = 0u32;
286        let mut current = value;
287        while let PerlValue::Reference(inner) = current {
288            depth += 1;
289            current = inner;
290            if depth >= 10 {
291                break;
292            }
293        }
294        "\\".repeat(depth as usize)
295    }
296
297    /// Formats the ultimate target of a reference chain briefly.
298    ///
299    /// Skips intermediate `Reference` wrappers to show the leaf value.
300    fn format_deref_target_brief(&self, value: &PerlValue) -> String {
301        let mut current = value;
302        let mut safety = 0u32;
303        while let PerlValue::Reference(inner) = current {
304            current = inner;
305            safety += 1;
306            if safety >= 10 {
307                break;
308            }
309        }
310        match current {
311            PerlValue::Reference(_) => "REF(...)".to_string(),
312            other => self.format_non_ref_brief(other),
313        }
314    }
315
316    /// Formats the ultimate target of a reference chain fully.
317    fn format_deref_target(&self, value: &PerlValue) -> String {
318        let mut current = value;
319        let mut safety = 0u32;
320        while let PerlValue::Reference(inner) = current {
321            current = inner;
322            safety += 1;
323            if safety >= 10 {
324                break;
325            }
326        }
327        match current {
328            PerlValue::Reference(_) => "REF(...)".to_string(),
329            other => self.format_value(other),
330        }
331    }
332
333    /// Formats a non-reference value briefly (used by deref target formatting).
334    fn format_non_ref_brief(&self, value: &PerlValue) -> String {
335        match value {
336            PerlValue::Reference(_) => "REF".to_string(),
337            other => self.format_value_brief(other),
338        }
339    }
340
341    /// Formats a value briefly (for use in previews).
342    fn format_value_brief(&self, value: &PerlValue) -> String {
343        match value {
344            PerlValue::Undef => "undef".to_string(),
345            PerlValue::Scalar(s) => self.format_string(s),
346            PerlValue::Number(n) => n.to_string(),
347            PerlValue::Integer(i) => i.to_string(),
348            PerlValue::Array(elements) => format!("ARRAY({})", elements.len()),
349            PerlValue::Hash(pairs) => format!("HASH({})", pairs.len()),
350            PerlValue::Reference(inner) => {
351                let prefix = self.ref_prefix_chain(value);
352                format!("{}{}", prefix, self.format_deref_target_brief(inner))
353            }
354            PerlValue::Object { class, .. } => format!("{}=...", class),
355            PerlValue::Code { name } => {
356                name.as_ref().map_or_else(|| "CODE(...)".to_string(), |n| format!("\\&{}", n))
357            }
358            PerlValue::Glob(name) => format!("*{}", name),
359            PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
360            PerlValue::Tied { class, .. } => format!("TIED({})", class),
361            PerlValue::Truncated { summary, .. } => summary.clone(),
362            PerlValue::Error(msg) => format!("<error: {}>", msg),
363        }
364    }
365
366    /// Formats a full value (for the value field).
367    fn format_value(&self, value: &PerlValue) -> String {
368        match value {
369            PerlValue::Undef => "undef".to_string(),
370            PerlValue::Scalar(s) => self.format_string(s),
371            PerlValue::Number(n) => n.to_string(),
372            PerlValue::Integer(i) => i.to_string(),
373            PerlValue::Array(elements) => self.format_array_preview(elements),
374            PerlValue::Hash(pairs) => self.format_hash_preview(pairs),
375            PerlValue::Reference(inner) => {
376                let prefix = self.ref_prefix_chain(value);
377                format!("{}{}", prefix, self.format_deref_target(inner))
378            }
379            PerlValue::Object { class, value } => {
380                format!("{}={}", class, self.format_value_brief(value))
381            }
382            PerlValue::Code { name } => {
383                name.as_ref().map_or_else(|| "sub { ... }".to_string(), |n| format!("\\&{}", n))
384            }
385            PerlValue::Glob(name) => format!("*{}", name),
386            PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
387            PerlValue::Tied { class, value } => {
388                if let Some(v) = value {
389                    format!("TIED({}) = {}", class, self.format_value_brief(v))
390                } else {
391                    format!("TIED({})", class)
392                }
393            }
394            PerlValue::Truncated { summary, total_count } => {
395                if let Some(count) = total_count {
396                    format!("{} ({} total)", summary, count)
397                } else {
398                    summary.clone()
399                }
400            }
401            PerlValue::Error(msg) => format!("<error: {}>", msg),
402        }
403    }
404}
405
406impl VariableRenderer for PerlVariableRenderer {
407    fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable {
408        let formatted_value = self.format_value(value);
409        let type_name = value.type_name().to_string();
410
411        let mut rendered = RenderedVariable::new(name, formatted_value).with_type(type_name);
412
413        // Set child counts for expandable types
414        match value {
415            PerlValue::Array(elements) => {
416                rendered.indexed_variables = Some(elements.len() as i64);
417            }
418            PerlValue::Hash(pairs) => {
419                rendered.named_variables = Some(pairs.len() as i64);
420            }
421            PerlValue::Object { class, value: inner } => {
422                rendered.type_name = Some(class.clone());
423                match inner.as_ref() {
424                    PerlValue::Hash(pairs) => {
425                        rendered.named_variables = Some(pairs.len() as i64);
426                    }
427                    PerlValue::Array(elements) => {
428                        rendered.indexed_variables = Some(elements.len() as i64);
429                    }
430                    _ => {}
431                }
432            }
433            _ => {}
434        }
435
436        rendered
437    }
438
439    fn render_with_reference(
440        &self,
441        name: &str,
442        value: &PerlValue,
443        reference_id: i64,
444    ) -> RenderedVariable {
445        let mut rendered = self.render(name, value);
446
447        if value.is_expandable() {
448            rendered.variables_reference = reference_id;
449        }
450
451        rendered
452    }
453
454    fn render_children(
455        &self,
456        value: &PerlValue,
457        start: usize,
458        count: usize,
459    ) -> Vec<RenderedVariable> {
460        match value {
461            PerlValue::Array(elements) => elements
462                .iter()
463                .enumerate()
464                .skip(start)
465                .take(count)
466                .map(|(i, v)| self.render(&format!("[{}]", i), v))
467                .collect(),
468            PerlValue::Hash(pairs) => {
469                pairs.iter().skip(start).take(count).map(|(k, v)| self.render(k, v)).collect()
470            }
471            PerlValue::Reference(inner) => {
472                vec![self.render("$_", inner)]
473            }
474            PerlValue::Object { value: inner, .. } => self.render_children(inner, start, count),
475            PerlValue::Tied { value: Some(inner), .. } => self.render_children(inner, start, count),
476            _ => vec![],
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn test_render_scalar() {
487        let renderer = PerlVariableRenderer::new();
488        let value = PerlValue::Scalar("hello".to_string());
489        let rendered = renderer.render("$x", &value);
490
491        assert_eq!(rendered.name, "$x");
492        assert_eq!(rendered.value, "\"hello\"");
493        assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
494        assert_eq!(rendered.variables_reference, 0);
495    }
496
497    #[test]
498    fn test_render_integer() {
499        let renderer = PerlVariableRenderer::new();
500        let value = PerlValue::Integer(42);
501        let rendered = renderer.render("$n", &value);
502
503        assert_eq!(rendered.name, "$n");
504        assert_eq!(rendered.value, "42");
505        assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
506    }
507
508    #[test]
509    fn test_render_array() {
510        let renderer = PerlVariableRenderer::new();
511        let value = PerlValue::Array(vec![
512            PerlValue::Integer(1),
513            PerlValue::Integer(2),
514            PerlValue::Integer(3),
515        ]);
516        let rendered = renderer.render("@arr", &value);
517
518        assert_eq!(rendered.name, "@arr");
519        assert!(rendered.value.starts_with('['));
520        assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
521        assert_eq!(rendered.indexed_variables, Some(3));
522    }
523
524    #[test]
525    fn test_render_hash() {
526        let renderer = PerlVariableRenderer::new();
527        let value = PerlValue::Hash(vec![
528            ("key1".to_string(), PerlValue::Scalar("value1".to_string())),
529            ("key2".to_string(), PerlValue::Integer(42)),
530        ]);
531        let rendered = renderer.render("%hash", &value);
532
533        assert_eq!(rendered.name, "%hash");
534        assert!(rendered.value.starts_with('{'));
535        assert_eq!(rendered.type_name, Some("HASH".to_string()));
536        assert_eq!(rendered.named_variables, Some(2));
537    }
538
539    #[test]
540    fn test_render_with_reference() {
541        let renderer = PerlVariableRenderer::new();
542        let value = PerlValue::Array(vec![PerlValue::Integer(1)]);
543        let rendered = renderer.render_with_reference("@arr", &value, 42);
544
545        assert_eq!(rendered.variables_reference, 42);
546        assert!(rendered.is_expandable());
547    }
548
549    #[test]
550    fn test_render_children_array() {
551        let renderer = PerlVariableRenderer::new();
552        let value = PerlValue::Array(vec![
553            PerlValue::Integer(10),
554            PerlValue::Integer(20),
555            PerlValue::Integer(30),
556        ]);
557        let children = renderer.render_children(&value, 0, 10);
558
559        assert_eq!(children.len(), 3);
560        assert_eq!(children[0].name, "[0]");
561        assert_eq!(children[0].value, "10");
562        assert_eq!(children[1].name, "[1]");
563        assert_eq!(children[2].name, "[2]");
564    }
565
566    #[test]
567    fn test_render_children_hash() {
568        let renderer = PerlVariableRenderer::new();
569        let value = PerlValue::Hash(vec![
570            ("foo".to_string(), PerlValue::Integer(1)),
571            ("bar".to_string(), PerlValue::Integer(2)),
572        ]);
573        let children = renderer.render_children(&value, 0, 10);
574
575        assert_eq!(children.len(), 2);
576        assert_eq!(children[0].name, "foo");
577        assert_eq!(children[1].name, "bar");
578    }
579
580    #[test]
581    fn test_render_object() {
582        let renderer = PerlVariableRenderer::new();
583        let value = PerlValue::Object {
584            class: "My::Class".to_string(),
585            value: Box::new(PerlValue::Hash(vec![(
586                "attr".to_string(),
587                PerlValue::Scalar("value".to_string()),
588            )])),
589        };
590        let rendered = renderer.render("$obj", &value);
591
592        assert_eq!(rendered.name, "$obj");
593        assert!(rendered.value.contains("My::Class"));
594        assert_eq!(rendered.type_name, Some("My::Class".to_string()));
595        assert_eq!(rendered.named_variables, Some(1));
596    }
597
598    #[test]
599    fn test_string_truncation() {
600        let renderer = PerlVariableRenderer::new().with_max_string_length(10);
601        let value = PerlValue::Scalar("this is a very long string".to_string());
602        let rendered = renderer.render("$s", &value);
603
604        assert!(rendered.value.contains("..."));
605        assert!(rendered.value.len() < 30);
606    }
607
608    #[test]
609    fn test_string_escaping() {
610        let renderer = PerlVariableRenderer::new();
611        let value = PerlValue::Scalar("line1\nline2\ttab".to_string());
612        let rendered = renderer.render("$s", &value);
613
614        assert!(rendered.value.contains("\\n"));
615        assert!(rendered.value.contains("\\t"));
616    }
617
618    #[test]
619    fn test_render_undef() {
620        let renderer = PerlVariableRenderer::new();
621        let value = PerlValue::Undef;
622        let rendered = renderer.render("$x", &value);
623
624        assert_eq!(rendered.value, "undef");
625        assert_eq!(rendered.type_name, Some("undef".to_string()));
626    }
627
628    #[test]
629    fn test_render_reference() {
630        let renderer = PerlVariableRenderer::new();
631        let value = PerlValue::Reference(Box::new(PerlValue::Integer(42)));
632        let rendered = renderer.render("$ref", &value);
633
634        assert_eq!(rendered.name, "$ref");
635        assert!(rendered.value.contains("42"));
636        assert_eq!(rendered.type_name, Some("REF".to_string()));
637    }
638
639    #[test]
640    fn test_render_code() {
641        let renderer = PerlVariableRenderer::new();
642        let value = PerlValue::Code { name: Some("my_sub".to_string()) };
643        let rendered = renderer.render("$code", &value);
644
645        assert!(rendered.value.contains("my_sub"));
646        assert_eq!(rendered.type_name, Some("CODE".to_string()));
647    }
648
649    // ---------------------------------------------------------------
650    // Circular reference detection and large structure handling tests
651    // ---------------------------------------------------------------
652
653    /// Simulates a self-referential hash like `{ a => \$self }`.
654    ///
655    /// Since `PerlValue` uses `Box` (no `Rc`), true cycles cannot exist at
656    /// the type level.  The Perl debugger would itself truncate such cycles,
657    /// so we model the leaf as a `Truncated` variant representing the
658    /// back-reference the debugger would emit.
659    #[test]
660    fn test_render_self_referential_hash() {
661        let renderer = PerlVariableRenderer::new();
662
663        // Simulate: my $self = { a => \$self }
664        // The debugger would show the back-reference as a truncated/circular marker.
665        let circular_marker =
666            PerlValue::Truncated { summary: "HASH(0x...circular)".to_string(), total_count: None };
667        let value = PerlValue::Hash(vec![(
668            "a".to_string(),
669            PerlValue::Reference(Box::new(circular_marker)),
670        )]);
671
672        let rendered = renderer.render("$self", &value);
673
674        assert_eq!(rendered.name, "$self");
675        assert_eq!(rendered.type_name, Some("HASH".to_string()));
676        assert_eq!(rendered.named_variables, Some(1));
677        // The value preview should contain the circular marker, not panic or hang
678        assert!(rendered.value.contains("circular"));
679
680        // Children should also render safely
681        let children = renderer.render_children(&value, 0, 10);
682        assert_eq!(children.len(), 1);
683        assert_eq!(children[0].name, "a");
684        assert!(children[0].value.contains("circular"));
685    }
686
687    /// Deep nesting of >100 reference levels should produce bounded output.
688    ///
689    /// The renderer caps reference chain traversal at 10 levels, emitting
690    /// `REF(...)` for anything deeper.
691    #[test]
692    fn test_render_deep_nesting_over_100_levels_bounded() {
693        let renderer = PerlVariableRenderer::new();
694
695        // Build a tower of 150 nested references: \\\...\42
696        let mut value = PerlValue::Integer(42);
697        for _ in 0..150 {
698            value = PerlValue::Reference(Box::new(value));
699        }
700
701        let rendered = renderer.render("$deep", &value);
702
703        assert_eq!(rendered.name, "$deep");
704        assert_eq!(rendered.type_name, Some("REF".to_string()));
705
706        // The output must be bounded — the renderer stops at 10 backslash
707        // prefixes and then emits REF(...) for the remainder.
708        // Count backslash prefixes in the value string.
709        let backslash_prefix_count = rendered.value.chars().take_while(|&c| c == '\\').count();
710        assert!(
711            backslash_prefix_count <= 10,
712            "backslash prefix count {} should be <= 10",
713            backslash_prefix_count,
714        );
715        // The value should contain the REF(...) truncation marker
716        assert!(
717            rendered.value.contains("REF(...)"),
718            "deeply nested ref should contain REF(...), got: {}",
719            rendered.value,
720        );
721
722        // Total output length should be reasonable (not exponential)
723        assert!(
724            rendered.value.len() < 200,
725            "rendered value length {} should be < 200",
726            rendered.value.len(),
727        );
728    }
729
730    /// A reference chain of exactly 10 levels should still reach the leaf.
731    #[test]
732    fn test_render_reference_chain_at_depth_limit() {
733        let renderer = PerlVariableRenderer::new();
734
735        let mut value = PerlValue::Scalar("leaf".to_string());
736        for _ in 0..10 {
737            value = PerlValue::Reference(Box::new(value));
738        }
739
740        let rendered = renderer.render("$ref10", &value);
741        // At exactly 10, the chain traversal stops and formats the leaf.
742        // The output should contain 10 backslashes and then the leaf value.
743        assert!(rendered.value.contains("leaf") || rendered.value.contains("REF(...)"));
744        assert!(rendered.value.len() < 200);
745    }
746
747    /// Large array with >10K elements should truncate in preview.
748    #[test]
749    fn test_render_large_array_over_10k_elements_truncates() {
750        let renderer = PerlVariableRenderer::new();
751
752        let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
753        let value = PerlValue::Array(elements);
754
755        let rendered = renderer.render("@big", &value);
756
757        assert_eq!(rendered.name, "@big");
758        assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
759        assert_eq!(rendered.indexed_variables, Some(10_001));
760
761        // Preview should show only max_array_preview (default 3) elements
762        // plus a "... (N total)" suffix
763        assert!(
764            rendered.value.contains("10001 total"),
765            "should show total count, got: {}",
766            rendered.value,
767        );
768        assert!(rendered.value.starts_with('['));
769        assert!(rendered.value.ends_with(']'));
770
771        // Preview string should be bounded — not contain all 10K values
772        assert!(
773            rendered.value.len() < 500,
774            "preview length {} should be < 500",
775            rendered.value.len(),
776        );
777    }
778
779    /// `render_children` paginates large arrays correctly.
780    #[test]
781    fn test_render_children_large_array_pagination() {
782        let renderer = PerlVariableRenderer::new();
783
784        let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
785        let value = PerlValue::Array(elements);
786
787        // Request a window of 100 starting at index 5000
788        let children = renderer.render_children(&value, 5000, 100);
789        assert_eq!(children.len(), 100);
790        assert_eq!(children[0].name, "[5000]");
791        assert_eq!(children[0].value, "5000");
792        assert_eq!(children[99].name, "[5099]");
793        assert_eq!(children[99].value, "5099");
794
795        // Request past the end — should return only what's available
796        let tail = renderer.render_children(&value, 10_000, 100);
797        assert_eq!(tail.len(), 1);
798        assert_eq!(tail[0].name, "[10000]");
799    }
800
801    /// Large hash with >5K pairs should truncate in preview.
802    #[test]
803    fn test_render_large_hash_over_5k_pairs_truncates() {
804        let renderer = PerlVariableRenderer::new();
805
806        let pairs: Vec<(String, PerlValue)> =
807            (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
808        let value = PerlValue::Hash(pairs);
809
810        let rendered = renderer.render("%big", &value);
811
812        assert_eq!(rendered.name, "%big");
813        assert_eq!(rendered.type_name, Some("HASH".to_string()));
814        assert_eq!(rendered.named_variables, Some(5_001));
815
816        // Preview should show only max_hash_preview (default 3) pairs
817        // plus a "... (N keys)" suffix
818        assert!(
819            rendered.value.contains("5001 keys"),
820            "should show key count, got: {}",
821            rendered.value,
822        );
823        assert!(rendered.value.starts_with('{'));
824        assert!(rendered.value.ends_with('}'));
825
826        // Preview string should be bounded
827        assert!(
828            rendered.value.len() < 500,
829            "preview length {} should be < 500",
830            rendered.value.len(),
831        );
832    }
833
834    /// `render_children` paginates large hashes correctly.
835    #[test]
836    fn test_render_children_large_hash_pagination() {
837        let renderer = PerlVariableRenderer::new();
838
839        let pairs: Vec<(String, PerlValue)> =
840            (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
841        let value = PerlValue::Hash(pairs);
842
843        // Request a window of 50 starting at index 2500
844        let children = renderer.render_children(&value, 2500, 50);
845        assert_eq!(children.len(), 50);
846        assert_eq!(children[0].name, "key_2500");
847        assert_eq!(children[0].value, "2500");
848
849        // Request past the end
850        let tail = renderer.render_children(&value, 5000, 100);
851        assert_eq!(tail.len(), 1);
852        assert_eq!(tail[0].name, "key_5000");
853    }
854
855    /// Blessed object backed by a hash — the standard Perl OO pattern.
856    #[test]
857    fn test_render_blessed_object_hash_based() {
858        let renderer = PerlVariableRenderer::new();
859
860        let value = PerlValue::Object {
861            class: "HTTP::Response".to_string(),
862            value: Box::new(PerlValue::Hash(vec![
863                ("_rc".to_string(), PerlValue::Integer(200)),
864                ("_content".to_string(), PerlValue::Scalar("OK".to_string())),
865                (
866                    "_headers".to_string(),
867                    PerlValue::Hash(vec![(
868                        "Content-Type".to_string(),
869                        PerlValue::Scalar("text/html".to_string()),
870                    )]),
871                ),
872            ])),
873        };
874
875        let rendered = renderer.render("$resp", &value);
876
877        assert_eq!(rendered.name, "$resp");
878        assert_eq!(rendered.type_name, Some("HTTP::Response".to_string()));
879        assert_eq!(rendered.named_variables, Some(3));
880        assert!(rendered.value.contains("HTTP::Response"));
881
882        // Children should be the hash keys
883        let children = renderer.render_children(&value, 0, 10);
884        assert_eq!(children.len(), 3);
885        assert_eq!(children[0].name, "_rc");
886        assert_eq!(children[0].value, "200");
887        assert_eq!(children[1].name, "_content");
888        assert!(children[1].value.contains("OK"));
889    }
890
891    /// Blessed object backed by an array (inside-out objects).
892    #[test]
893    fn test_render_blessed_object_array_based() {
894        let renderer = PerlVariableRenderer::new();
895
896        let value = PerlValue::Object {
897            class: "My::InsideOut".to_string(),
898            value: Box::new(PerlValue::Array(vec![
899                PerlValue::Scalar("field_a".to_string()),
900                PerlValue::Integer(99),
901            ])),
902        };
903
904        let rendered = renderer.render("$io", &value);
905
906        assert_eq!(rendered.type_name, Some("My::InsideOut".to_string()));
907        assert_eq!(rendered.indexed_variables, Some(2));
908        assert!(rendered.value.contains("My::InsideOut"));
909
910        let children = renderer.render_children(&value, 0, 10);
911        assert_eq!(children.len(), 2);
912        assert_eq!(children[0].name, "[0]");
913        assert_eq!(children[1].name, "[1]");
914    }
915
916    /// Blessed object backed by a scalar (e.g., URI, overloaded stringification).
917    #[test]
918    fn test_render_blessed_object_scalar_based() {
919        let renderer = PerlVariableRenderer::new();
920
921        // URI objects are blessed scalar refs: bless \("https://example.com"), "URI"
922        let value = PerlValue::Object {
923            class: "URI".to_string(),
924            value: Box::new(PerlValue::Scalar("https://example.com".to_string())),
925        };
926
927        let rendered = renderer.render("$uri", &value);
928
929        assert_eq!(rendered.type_name, Some("URI".to_string()));
930        // Scalar-backed objects don't expose named or indexed children
931        assert_eq!(rendered.named_variables, None);
932        assert_eq!(rendered.indexed_variables, None);
933        assert!(rendered.value.contains("URI"));
934        assert!(rendered.value.contains("https://example.com"));
935    }
936
937    /// Blessed object with deeply nested class name (Perl namespaces).
938    #[test]
939    fn test_render_blessed_object_deep_namespace() {
940        let renderer = PerlVariableRenderer::new();
941
942        let value = PerlValue::Object {
943            class: "Very::Deep::Nested::Package::Name".to_string(),
944            value: Box::new(PerlValue::Hash(vec![])),
945        };
946
947        let rendered = renderer.render("$obj", &value);
948
949        assert_eq!(rendered.type_name, Some("Very::Deep::Nested::Package::Name".to_string()),);
950        assert!(rendered.value.contains("Very::Deep::Nested::Package::Name"));
951        assert_eq!(rendered.named_variables, Some(0));
952    }
953
954    /// Blessed object with `render_with_reference` gets a reference ID
955    /// and is expandable.
956    #[test]
957    fn test_render_blessed_object_with_reference() {
958        let renderer = PerlVariableRenderer::new();
959
960        let value = PerlValue::Object {
961            class: "DBI::db".to_string(),
962            value: Box::new(PerlValue::Hash(vec![(
963                "Driver".to_string(),
964                PerlValue::Scalar("SQLite".to_string()),
965            )])),
966        };
967
968        let rendered = renderer.render_with_reference("$dbh", &value, 99);
969
970        assert_eq!(rendered.variables_reference, 99);
971        assert!(rendered.is_expandable());
972        assert_eq!(rendered.type_name, Some("DBI::db".to_string()));
973    }
974
975    /// Simulates a hash whose value is a reference back to a parent —
976    /// the pattern `{ parent => \%grandparent, child => \%self }` where
977    /// the debugger truncates the cycle.
978    #[test]
979    fn test_render_hash_with_multiple_back_references() {
980        let renderer = PerlVariableRenderer::new();
981
982        let grandparent_marker = PerlValue::Truncated {
983            summary: "HASH(0xaaa...circular)".to_string(),
984            total_count: Some(5),
985        };
986        let self_marker = PerlValue::Truncated {
987            summary: "HASH(0xbbb...circular)".to_string(),
988            total_count: Some(3),
989        };
990
991        let value = PerlValue::Hash(vec![
992            ("parent".to_string(), PerlValue::Reference(Box::new(grandparent_marker))),
993            ("child".to_string(), PerlValue::Reference(Box::new(self_marker))),
994            ("name".to_string(), PerlValue::Scalar("node".to_string())),
995        ]);
996
997        let rendered = renderer.render("$node", &value);
998
999        assert_eq!(rendered.named_variables, Some(3));
1000        // Should not panic or produce unbounded output
1001        assert!(rendered.value.len() < 500);
1002
1003        let children = renderer.render_children(&value, 0, 10);
1004        assert_eq!(children.len(), 3);
1005        // The circular references render as REF type with truncated target
1006        assert!(children[0].value.contains("circular"));
1007        assert!(children[1].value.contains("circular"));
1008        assert!(children[2].value.contains("node"));
1009    }
1010
1011    /// Empty array and hash render as `[]` and `{}` respectively.
1012    #[test]
1013    fn test_render_empty_collections() {
1014        let renderer = PerlVariableRenderer::new();
1015
1016        let empty_arr = PerlValue::Array(vec![]);
1017        let rendered = renderer.render("@empty", &empty_arr);
1018        assert_eq!(rendered.value, "[]");
1019        assert_eq!(rendered.indexed_variables, Some(0));
1020
1021        let empty_hash = PerlValue::Hash(vec![]);
1022        let rendered = renderer.render("%empty", &empty_hash);
1023        assert_eq!(rendered.value, "{}");
1024        assert_eq!(rendered.named_variables, Some(0));
1025    }
1026
1027    /// Verifies that configuring lower preview limits still works for
1028    /// large structures.
1029    #[test]
1030    fn test_render_large_array_with_custom_preview_limit() {
1031        let renderer =
1032            PerlVariableRenderer::new().with_max_array_preview(1).with_max_hash_preview(1);
1033
1034        let elements: Vec<PerlValue> = (0..100).map(PerlValue::Integer).collect();
1035        let value = PerlValue::Array(elements);
1036
1037        let rendered = renderer.render("@arr", &value);
1038        // With preview=1, should show one element then "... (100 total)"
1039        assert!(rendered.value.contains("100 total"));
1040        // Only the first element should appear before the ellipsis
1041        assert!(rendered.value.starts_with("[0"));
1042    }
1043
1044    /// Verifies that configuring lower preview limits works for large hashes.
1045    #[test]
1046    fn test_render_large_hash_with_custom_preview_limit() {
1047        let renderer = PerlVariableRenderer::new().with_max_hash_preview(1);
1048
1049        let pairs: Vec<(String, PerlValue)> =
1050            (0..100).map(|i| (format!("k{}", i), PerlValue::Integer(i))).collect();
1051        let value = PerlValue::Hash(pairs);
1052
1053        let rendered = renderer.render("%h", &value);
1054        assert!(rendered.value.contains("100 keys"));
1055        assert!(rendered.value.starts_with('{'));
1056    }
1057}