Skip to main content

perl_tdd_support/tdd/
tdd_workflow.rs

1//! TDD (Test-Driven Development) workflow integration for LSP
2//!
3//! Provides a complete red-green-refactor cycle support with
4//! automatic test generation, continuous testing, and refactoring suggestions.
5
6use crate::ast::Node;
7
8// Re-use Diagnostic from tdd_basic to avoid duplication
9use crate::tdd_basic::{Diagnostic, DiagnosticSeverity};
10use crate::test_generator::{CoverageReport, TestResults, TestRunner};
11use crate::test_generator::{RefactoringSuggester, RefactoringSuggestion};
12use crate::test_generator::{TestCase, TestFramework, TestGenerator};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16
17/// TDD workflow manager
18pub struct TddWorkflow {
19    /// Test generator
20    generator: TestGenerator,
21    /// Test runner
22    runner: TestRunner,
23    /// Refactoring suggester
24    suggester: RefactoringSuggester,
25    /// Current workflow state
26    state: WorkflowState,
27    /// Test results cache
28    test_cache: HashMap<PathBuf, TestResults>,
29    /// Coverage tracking
30    coverage_tracker: CoverageTracker,
31    /// Configuration
32    config: TddConfig,
33}
34
35/// Represents the current phase of the TDD (Test-Driven Development) workflow cycle.
36///
37/// Tracks the developer's position in the red-green-refactor cycle, enabling context-aware
38/// suggestions and automation during test development and implementation.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum WorkflowState {
41    /// Writing test (Red phase)
42    Red,
43    /// Making test pass (Green phase)
44    Green,
45    /// Refactoring code (Refactor phase)
46    Refactor,
47    /// Not in TDD cycle
48    Idle,
49}
50
51/// Configuration options for TDD workflow automation and behavior.
52///
53/// Customizes how the TDD workflow manager generates tests, runs them, and provides
54/// feedback. All settings have sensible defaults suitable for typical Perl development.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TddConfig {
57    /// Automatically generate tests for new code
58    pub auto_generate_tests: bool,
59    /// Run tests on save
60    pub test_on_save: bool,
61    /// Show coverage inline
62    pub show_inline_coverage: bool,
63    /// Test framework preference
64    pub test_framework: String,
65    /// Test file naming pattern
66    pub test_file_pattern: String,
67    /// Minimum coverage threshold
68    pub coverage_threshold: f64,
69    /// Enable continuous testing
70    pub continuous_testing: bool,
71    /// Auto-suggest refactorings after green
72    pub auto_suggest_refactorings: bool,
73}
74
75impl Default for TddConfig {
76    fn default() -> Self {
77        Self {
78            auto_generate_tests: true,
79            test_on_save: true,
80            show_inline_coverage: true,
81            test_framework: "Test::More".to_string(),
82            test_file_pattern: "t/{name}.t".to_string(),
83            coverage_threshold: 80.0,
84            continuous_testing: true,
85            auto_suggest_refactorings: true,
86        }
87    }
88}
89
90/// Coverage tracking for TDD
91pub struct CoverageTracker {
92    /// Line coverage data
93    line_coverage: HashMap<PathBuf, Vec<LineCoverage>>,
94    /// Branch coverage data (architectural placeholder for future implementation)
95    #[allow(dead_code)]
96    branch_coverage: HashMap<PathBuf, Vec<BranchCoverage>>,
97    /// Overall coverage percentage
98    total_coverage: f64,
99}
100
101/// Line-level code coverage information
102#[derive(Debug, Clone)]
103pub struct LineCoverage {
104    /// Line number in the source file
105    pub line: usize,
106    /// Number of times this line was executed
107    pub hits: usize,
108    /// Whether this line is considered covered
109    pub covered: bool,
110}
111
112/// Branch-level code coverage information
113#[derive(Debug, Clone)]
114pub struct BranchCoverage {
115    /// Line number where the branch occurs
116    pub line: usize,
117    /// Unique identifier for this branch within the line
118    pub branch_id: usize,
119    /// Whether this branch was taken at least once
120    pub taken: bool,
121    /// Number of times this branch was executed
122    pub hits: usize,
123}
124
125impl TddWorkflow {
126    /// Create a new TDD workflow manager with the given configuration
127    pub fn new(config: TddConfig) -> Self {
128        let framework = match config.test_framework.as_str() {
129            "Test2::V0" => TestFramework::Test2V0,
130            "Test::Simple" => TestFramework::TestSimple,
131            "Test::Class" => TestFramework::TestClass,
132            _ => TestFramework::TestMore,
133        };
134
135        Self {
136            generator: TestGenerator::new(framework),
137            runner: TestRunner::new(),
138            suggester: RefactoringSuggester::new(),
139            state: WorkflowState::Idle,
140            test_cache: HashMap::new(),
141            coverage_tracker: CoverageTracker::new(),
142            config,
143        }
144    }
145
146    /// Start a new TDD cycle
147    pub fn start_cycle(&mut self, test_name: &str) -> TddCycleResult {
148        self.state = WorkflowState::Red;
149
150        TddCycleResult {
151            phase: "Red".to_string(),
152            message: format!("Starting TDD cycle for '{}'", test_name),
153            actions: vec![
154                TddAction::GenerateTest(test_name.to_string()),
155                TddAction::CreateTestFile(self.get_test_file_path(test_name)),
156            ],
157        }
158    }
159
160    /// Generate tests for the given code
161    pub fn generate_tests(&self, ast: &Node, source: &str) -> Vec<TestCase> {
162        self.generator.generate_tests(ast, source)
163    }
164
165    /// Generate a specific test type
166    pub fn generate_test_for_function(
167        &self,
168        function_name: &str,
169        params: &[String],
170        test_type: TestType,
171    ) -> TestCase {
172        let test_name = format!("test_{}_{:?}", function_name, test_type);
173        let description = format!("{:?} test for {}", test_type, function_name);
174
175        let code = match test_type {
176            TestType::Basic => self.generate_basic_test(function_name, params),
177            TestType::EdgeCase => self.generate_edge_case_test(function_name, params),
178            TestType::ErrorHandling => self.generate_error_test(function_name, params),
179            TestType::Performance => self.generate_performance_test(function_name),
180            TestType::Integration => self.generate_integration_test(function_name, params),
181        };
182
183        TestCase {
184            name: test_name,
185            description,
186            code,
187            is_todo: matches!(test_type, TestType::Integration | TestType::Performance),
188        }
189    }
190
191    fn generate_basic_test(&self, name: &str, params: &[String]) -> String {
192        let args = params
193            .iter()
194            .enumerate()
195            .map(|(i, _)| format!("'test_value_{}'", i))
196            .collect::<Vec<_>>()
197            .join(", ");
198
199        format!(
200            "use Test::More;\n\n\
201             subtest '{}' => sub {{\n    \
202             my $result = {}({});\n    \
203             ok(defined $result, 'Returns defined value');\n    \
204             # PENDING: Add specific assertions\n\
205             }};\n\n\
206             done_testing();\n",
207            name, name, args
208        )
209    }
210
211    fn generate_edge_case_test(&self, name: &str, params: &[String]) -> String {
212        let undef_args = repeated_edge_case_args(params, "undef");
213        let empty_args = repeated_edge_case_args(params, "''");
214        let special_args = repeated_edge_case_args(params, "\"\\n\\t\\0\"");
215
216        format!(
217            "use Test::More;\n\n\
218             subtest '{} edge cases' => sub {{\n    \
219             # Test with undef\n    \
220             eval {{ {}({}) }};\n    \
221             ok(!$@, 'Handles undef');\n    \n    \
222             # Test with empty values\n    \
223             eval {{ {}({}) }};\n    \
224             ok(!$@, 'Handles empty string');\n    \n    \
225             # Test with special characters\n    \
226             eval {{ {}({}) }};\n    \
227             ok(!$@, 'Handles special characters');\n\
228             }};\n\n\
229             done_testing();\n",
230            name, name, undef_args, name, empty_args, name, special_args
231        )
232    }
233
234    fn generate_error_test(&self, name: &str, _params: &[String]) -> String {
235        format!(
236            "use Test::More;\n\
237             use Test::Exception;\n\n\
238             subtest '{} error handling' => sub {{\n    \
239             # Test that errors are caught\n    \
240             dies_ok {{ {}(undef, undef, undef) }} 'Dies on invalid input';\n    \n    \
241             # Test error message\n    \
242             throws_ok {{ {}() }} qr/required/, 'Correct error message';\n\
243             }};\n\n\
244             done_testing();\n",
245            name, name, name
246        )
247    }
248
249    fn generate_performance_test(&self, name: &str) -> String {
250        format!(
251            "use Test::More;\n\
252             use Benchmark qw(timethis);\n\n\
253             subtest '{} performance' => sub {{\n    \
254             my $iterations = 10000;\n    \
255             my $result = timethis($iterations, sub {{ {}() }});\n    \
256             \n    \
257             # Check performance threshold\n    \
258             my $rate = $result->iters / $result->cpu_a;\n    \
259             cmp_ok($rate, '>', 1000, 'Performance exceeds 1000 ops/sec');\n\
260             }};\n\n\
261             done_testing();\n",
262            name, name
263        )
264    }
265
266    fn generate_integration_test(&self, name: &str, _params: &[String]) -> String {
267        format!(
268            "use Test::More;\n\n\
269             subtest '{} integration' => sub {{\n    \
270             # PENDING: Set up test environment\n    \
271             # PENDING: Call {} with real dependencies\n    \
272             # PENDING: Verify integration points\n    \
273             pass('Integration test placeholder');\n\
274             }};\n\n\
275             done_testing();\n",
276            name, name
277        )
278    }
279
280    /// Run tests and update state
281    pub fn run_tests(&mut self, test_files: &[PathBuf]) -> TddCycleResult {
282        let file_strings: Vec<String> =
283            test_files.iter().map(|p| p.to_string_lossy().to_string()).collect();
284
285        let results = self.runner.run_tests(&file_strings);
286
287        // Cache results
288        for file in test_files {
289            self.test_cache.insert(file.clone(), results.clone());
290        }
291
292        // Update state based on the red-green-refactor cycle. Passing tests move the
293        // workflow to Green; entering Refactor is an explicit next step so callers
294        // can observe the complete cycle instead of skipping directly to Refactor.
295        let (new_state, message) = if results.failed > 0 {
296            (WorkflowState::Red, format!("{} tests failed", results.failed))
297        } else if results.todo > 0 {
298            (WorkflowState::Green, format!("All tests pass, {} TODOs remaining", results.todo))
299        } else {
300            (WorkflowState::Green, "All tests pass! Ready to refactor".to_string())
301        };
302
303        self.state = new_state.clone();
304
305        let mut actions = vec![];
306
307        // Suggest refactorings once the suite is green, but keep the phase explicit.
308        if new_state == WorkflowState::Green && self.config.auto_suggest_refactorings {
309            actions.push(TddAction::SuggestRefactorings);
310        }
311
312        TddCycleResult { phase: format!("{:?}", new_state), message, actions }
313    }
314
315    /// Move from green into the refactoring phase.
316    pub fn start_refactor(&mut self) -> TddCycleResult {
317        self.state = WorkflowState::Refactor;
318
319        TddCycleResult {
320            phase: "Refactor".to_string(),
321            message: "Refactoring phase - improve code while keeping tests green".to_string(),
322            actions: vec![TddAction::SuggestRefactorings, TddAction::UpdateCoverage],
323        }
324    }
325
326    /// Complete the current TDD cycle and return to idle.
327    pub fn complete_cycle(&mut self) -> TddCycleResult {
328        self.state = WorkflowState::Idle;
329
330        TddCycleResult {
331            phase: "Idle".to_string(),
332            message: "TDD cycle complete".to_string(),
333            actions: vec![TddAction::RunTests],
334        }
335    }
336
337    /// Get refactoring suggestions
338    pub fn get_refactoring_suggestions(
339        &mut self,
340        ast: &Node,
341        source: &str,
342    ) -> Vec<RefactoringSuggestion> {
343        self.suggester.analyze(ast, source)
344    }
345
346    /// Get current test coverage
347    pub fn get_coverage(&self) -> Option<CoverageReport> {
348        self.runner.get_coverage()
349    }
350
351    /// Update coverage data
352    pub fn update_coverage(&mut self, file: PathBuf, coverage: Vec<LineCoverage>) {
353        self.coverage_tracker.line_coverage.insert(file, coverage);
354        self.coverage_tracker.calculate_total_coverage();
355    }
356
357    /// Get inline coverage annotations
358    pub fn get_inline_coverage(&self, file: &Path) -> Vec<CoverageAnnotation> {
359        let mut annotations = Vec::new();
360
361        if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
362            for line_cov in coverage {
363                if !line_cov.covered {
364                    annotations.push(CoverageAnnotation {
365                        line: line_cov.line,
366                        message: "Not covered by tests".to_string(),
367                        severity: AnnotationSeverity::Warning,
368                    });
369                } else if line_cov.hits == 0 {
370                    annotations.push(CoverageAnnotation {
371                        line: line_cov.line,
372                        message: "Never executed".to_string(),
373                        severity: AnnotationSeverity::Info,
374                    });
375                }
376            }
377        }
378
379        annotations
380    }
381
382    /// Check if coverage meets threshold
383    pub fn check_coverage_threshold(&self) -> bool {
384        self.coverage_tracker.total_coverage >= self.config.coverage_threshold
385    }
386
387    /// Get test file path for a given module
388    fn get_test_file_path(&self, name: &str) -> PathBuf {
389        let pattern = &self.config.test_file_pattern;
390        let path_str = pattern.replace("{name}", name);
391        PathBuf::from(path_str)
392    }
393
394    /// Get workflow status
395    pub fn get_status(&self) -> WorkflowStatus {
396        WorkflowStatus {
397            state: self.state.clone(),
398            coverage: self.coverage_tracker.total_coverage,
399            tests_passing: self.test_cache.values().all(|r| r.failed == 0),
400            suggestions_available: true, // Would check actual suggestions
401        }
402    }
403
404    /// Generate diagnostics for uncovered code
405    pub fn generate_coverage_diagnostics(&self, file: &Path) -> Vec<Diagnostic> {
406        let mut diagnostics = Vec::new();
407
408        if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
409            for line_cov in coverage {
410                if !line_cov.covered {
411                    diagnostics.push(Diagnostic {
412                        range: (line_cov.line, line_cov.line),
413                        severity: DiagnosticSeverity::Warning,
414                        code: Some("tdd.uncovered".to_string()),
415                        message: "Line not covered by tests".to_string(),
416                        related_information: vec![],
417                        tags: vec![],
418                    });
419                }
420            }
421        }
422
423        diagnostics
424    }
425}
426
427fn repeated_edge_case_args(params: &[String], edge_case: &str) -> String {
428    match params.len() {
429        0 => String::new(),
430        len => std::iter::repeat_n(edge_case, len).collect::<Vec<_>>().join(", "),
431    }
432}
433
434impl CoverageTracker {
435    fn new() -> Self {
436        Self { line_coverage: HashMap::new(), branch_coverage: HashMap::new(), total_coverage: 0.0 }
437    }
438
439    fn calculate_total_coverage(&mut self) {
440        let mut total_lines = 0;
441        let mut covered_lines = 0;
442
443        for coverage in self.line_coverage.values() {
444            for line in coverage {
445                total_lines += 1;
446                if line.covered {
447                    covered_lines += 1;
448                }
449            }
450        }
451
452        self.total_coverage =
453            if total_lines > 0 { (covered_lines as f64 / total_lines as f64) * 100.0 } else { 0.0 };
454    }
455}
456
457/// Result of a TDD cycle action
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct TddCycleResult {
460    /// Current phase of the TDD cycle (Red, Green, or Refactor)
461    pub phase: String,
462    /// Human-readable message describing the cycle state
463    pub message: String,
464    /// Recommended actions to take next
465    pub actions: Vec<TddAction>,
466}
467
468/// Actions that can be taken during a TDD cycle
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub enum TddAction {
471    /// Generate a test with the given name
472    GenerateTest(String),
473    /// Create a new test file at the specified path
474    CreateTestFile(PathBuf),
475    /// Execute the test suite
476    RunTests,
477    /// Request refactoring suggestions for the code
478    SuggestRefactorings,
479    /// Refresh code coverage data
480    UpdateCoverage,
481    /// Display test failure details
482    ShowFailures,
483}
484
485/// Types of tests that can be generated
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub enum TestType {
488    /// Simple happy-path test with typical inputs
489    Basic,
490    /// Tests for boundary conditions and unusual inputs
491    EdgeCase,
492    /// Tests for error conditions and exception handling
493    ErrorHandling,
494    /// Benchmarks and performance regression tests
495    Performance,
496    /// Tests involving multiple components or external dependencies
497    Integration,
498}
499
500/// Inline annotation for displaying coverage information in the editor
501#[derive(Debug, Clone)]
502pub struct CoverageAnnotation {
503    /// Line number to annotate
504    pub line: usize,
505    /// Description of the coverage status
506    pub message: String,
507    /// Severity level for display styling
508    pub severity: AnnotationSeverity,
509}
510
511/// Severity levels for coverage and diagnostic annotations
512#[derive(Debug, Clone)]
513pub enum AnnotationSeverity {
514    /// Critical issue that must be addressed
515    Error,
516    /// Potential problem that should be reviewed
517    Warning,
518    /// Informational message
519    Info,
520    /// Subtle suggestion or hint
521    Hint,
522}
523
524/// Summary of the current TDD workflow status
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct WorkflowStatus {
527    /// Current phase of the TDD cycle
528    pub state: WorkflowState,
529    /// Overall code coverage percentage
530    pub coverage: f64,
531    /// Whether all tests are currently passing
532    pub tests_passing: bool,
533    /// Whether refactoring suggestions are available
534    pub suggestions_available: bool,
535}
536
537/// LSP integration for TDD workflow
538#[cfg(feature = "lsp-compat")]
539pub mod lsp_integration {
540    use super::*;
541    use lsp_types::{
542        CodeAction, CodeActionKind, Command, Diagnostic as LspDiagnostic, DiagnosticSeverity,
543        Position, Range,
544    };
545
546    /// Convert TDD actions to LSP code actions
547    pub fn tdd_actions_to_code_actions(
548        actions: Vec<TddAction>,
549        _uri: &url::Url,
550    ) -> Vec<CodeAction> {
551        actions
552            .into_iter()
553            .map(|action| match action {
554                TddAction::GenerateTest(name) => CodeAction {
555                    title: format!("Generate test for '{}'", name),
556                    kind: Some(CodeActionKind::REFACTOR),
557                    command: Some(Command {
558                        title: "Generate Test".to_string(),
559                        command: "perl.tdd.generateTest".to_string(),
560                        arguments: Some(vec![serde_json::json!(name)]),
561                    }),
562                    ..Default::default()
563                },
564                TddAction::RunTests => CodeAction {
565                    title: "Run tests".to_string(),
566                    kind: Some(CodeActionKind::new("test.run")),
567                    command: Some(Command {
568                        title: "Run Tests".to_string(),
569                        command: "perl.tdd.runTests".to_string(),
570                        arguments: None,
571                    }),
572                    ..Default::default()
573                },
574                TddAction::SuggestRefactorings => CodeAction {
575                    title: "Get refactoring suggestions".to_string(),
576                    kind: Some(CodeActionKind::REFACTOR),
577                    command: Some(Command {
578                        title: "Suggest Refactorings".to_string(),
579                        command: "perl.tdd.suggestRefactorings".to_string(),
580                        arguments: None,
581                    }),
582                    ..Default::default()
583                },
584                _ => CodeAction {
585                    title: format!("{:?}", action),
586                    kind: Some(CodeActionKind::EMPTY),
587                    ..Default::default()
588                },
589            })
590            .collect()
591    }
592
593    /// Convert coverage annotations to LSP diagnostics
594    pub fn coverage_to_diagnostics(annotations: Vec<CoverageAnnotation>) -> Vec<LspDiagnostic> {
595        annotations
596            .into_iter()
597            .map(|ann| LspDiagnostic {
598                range: Range {
599                    start: Position { line: ann.line as u32, character: 0 },
600                    end: Position { line: ann.line as u32, character: 999 },
601                },
602                severity: Some(match ann.severity {
603                    AnnotationSeverity::Error => DiagnosticSeverity::ERROR,
604                    AnnotationSeverity::Warning => DiagnosticSeverity::WARNING,
605                    AnnotationSeverity::Info => DiagnosticSeverity::INFORMATION,
606                    AnnotationSeverity::Hint => DiagnosticSeverity::HINT,
607                }),
608                code: Some(lsp_types::NumberOrString::String("coverage".to_string())),
609                source: Some("TDD".to_string()),
610                message: ann.message,
611                ..Default::default()
612            })
613            .collect()
614    }
615
616    /// Create status bar message for TDD state
617    pub fn create_status_message(status: &WorkflowStatus) -> String {
618        format!(
619            "TDD: {:?} | Coverage: {:.1}% | Tests: {} | Refactor: {}",
620            status.state,
621            status.coverage,
622            if status.tests_passing { "✓" } else { "✗" },
623            if status.suggestions_available { "💡" } else { "" }
624        )
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631    use crate::ast::NodeKind;
632    use crate::ast::SourceLocation;
633
634    #[test]
635    fn test_tdd_workflow_cycle() {
636        let config = TddConfig::default();
637        let mut workflow = TddWorkflow::new(config);
638
639        // Start a new cycle
640        let result = workflow.start_cycle("calculate_sum");
641        assert_eq!(workflow.state, WorkflowState::Red);
642        assert!(result.message.contains("calculate_sum"));
643    }
644
645    #[test]
646    fn test_generate_tests() {
647        let config = TddConfig::default();
648        let workflow = TddWorkflow::new(config);
649
650        let ast = Node::new(
651            NodeKind::Subroutine {
652                name: Some("multiply".to_string()),
653                name_span: Some(SourceLocation { start: 4, end: 12 }),
654                signature: None,
655                body: Box::new(Node::new(
656                    NodeKind::Block { statements: vec![] },
657                    SourceLocation { start: 0, end: 0 },
658                )),
659                attributes: vec![],
660                prototype: None,
661            },
662            SourceLocation { start: 0, end: 0 },
663        );
664
665        let tests = workflow.generate_tests(&ast, "sub multiply { }");
666        assert!(!tests.is_empty());
667    }
668
669    #[test]
670    fn test_coverage_tracking() {
671        let config = TddConfig::default();
672        let mut workflow = TddWorkflow::new(config);
673
674        let coverage = vec![
675            LineCoverage { line: 1, hits: 5, covered: true },
676            LineCoverage { line: 2, hits: 0, covered: false },
677            LineCoverage { line: 3, hits: 10, covered: true },
678        ];
679
680        workflow.update_coverage(PathBuf::from("test.pl"), coverage);
681
682        let annotations = workflow.get_inline_coverage(&PathBuf::from("test.pl"));
683        assert_eq!(annotations.len(), 1); // One uncovered line
684        assert_eq!(annotations[0].line, 2);
685    }
686
687    #[test]
688    fn test_coverage_resets_to_zero_when_all_inputs_empty() -> Result<(), Box<dyn std::error::Error>>
689    {
690        let config = TddConfig::default();
691        let mut workflow = TddWorkflow::new(config);
692
693        workflow.update_coverage(
694            PathBuf::from("a.pl"),
695            vec![LineCoverage { line: 1, hits: 1, covered: true }],
696        );
697        assert!(workflow.check_coverage_threshold());
698
699        workflow.update_coverage(PathBuf::from("a.pl"), vec![]);
700
701        assert!(!workflow.check_coverage_threshold());
702        assert_eq!(workflow.get_status().coverage, 0.0);
703        Ok(())
704    }
705
706    #[test]
707    fn test_refactoring_suggestions() {
708        let config = TddConfig::default();
709        let mut workflow = TddWorkflow::new(config);
710
711        // Create a subroutine with 8 parameters to trigger TooManyParameters suggestion
712        let parameters: Vec<Node> = (0..8)
713            .map(|i| {
714                Node::new(
715                    NodeKind::MandatoryParameter {
716                        variable: Box::new(Node::new(
717                            NodeKind::Variable {
718                                sigil: "$".to_string(),
719                                name: format!("param{}", i),
720                            },
721                            SourceLocation { start: 0, end: 0 },
722                        )),
723                    },
724                    SourceLocation { start: 0, end: 0 },
725                )
726            })
727            .collect();
728
729        let ast = Node::new(
730            NodeKind::Subroutine {
731                name: Some("complex_function".to_string()),
732                name_span: Some(SourceLocation { start: 4, end: 20 }),
733                signature: Some(Box::new(Node::new(
734                    NodeKind::Signature { parameters },
735                    SourceLocation { start: 0, end: 0 },
736                ))),
737                body: Box::new(Node::new(
738                    NodeKind::Block { statements: vec![] },
739                    SourceLocation { start: 0, end: 0 },
740                )),
741                attributes: vec![],
742                prototype: None,
743            },
744            SourceLocation { start: 0, end: 0 },
745        );
746
747        let suggestions = workflow.get_refactoring_suggestions(&ast, "sub complex_function { }");
748
749        // Should suggest refactoring for too many parameters
750        assert!(
751            suggestions.iter().any(
752                |s| s.category == crate::test_generator::RefactoringCategory::TooManyParameters
753            )
754        );
755    }
756
757    #[test]
758    fn test_specific_test_generation() {
759        let config = TddConfig::default();
760        let workflow = TddWorkflow::new(config);
761
762        let test = workflow.generate_test_for_function(
763            "validate_email",
764            &["$email".to_string()],
765            TestType::EdgeCase,
766        );
767
768        assert!(test.code.contains("edge cases"));
769        assert!(test.code.contains("undef"));
770        assert!(test.code.contains("empty"));
771    }
772
773    #[test]
774    fn test_edge_case_generation_preserves_signature_arity() {
775        let config = TddConfig::default();
776        let workflow = TddWorkflow::new(config);
777
778        let test = workflow.generate_test_for_function(
779            "validate_contact",
780            &["$name".to_string(), "$email".to_string()],
781            TestType::EdgeCase,
782        );
783
784        assert!(test.code.contains("validate_contact(undef, undef)"));
785        assert!(test.code.contains("validate_contact('', '')"));
786    }
787}