using ExtractMethod.Tooling;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace BeforeAfter.Tests.ExtractMethod;
///
/// Tests for yak em 04 (codegen): generate the refactoring from the em 03
/// suggestion — new method (promoted signature, parameter names from the
/// source symbols) + rewritten call site + transformed tree — and the
/// round-trip check (the transformed tree compiles with zero diagnostics).
///
/// Fixture line anchors: the em 01/02/03 anchors (ScoreReads 56..58,
/// Summarize 68..76 / partial 68..72, Heaviest 87..93) plus the em 04
/// additions Accumulate for-loop 127..130 and Pairwise 141..147.
///
public class RefactoringTests
{
// ---------------------------------------------------------------------
// Summarize partial (68..72): the declared-inside `total` promotes to the
// plain return of the new method; at the call site it is re-declared from
// the result. The new body gains the appended `return total;`.
// ---------------------------------------------------------------------
[Fact]
public void Summarize_partial_selection_generates_returning_method_and_declaration_call()
{
var (method, calls, tree) = Generate(68, 72);
Assert.Equal("Extract", method.Identifier.ValueText);
Assert.Equal("int", method.ReturnType.ToString());
Assert.Equal(new[] { "int limit" }, method.ParameterList.Parameters.Select(p => p.ToString()));
var body = (BlockSyntax)method.Body!;
Assert.Equal(3, body.Statements.Count); // total declaration, for-loop, appended return
Assert.Equal("return total;", body.Statements[2].ToString());
// `total` was declared inside the selection: re-declared at the call
// site from the result.
Assert.Single(calls);
Assert.Equal("int total = Extract(limit);", calls[0].ToString());
VerifyZeroDiagnostics(tree);
}
// ---------------------------------------------------------------------
// Summarize full body (68..76): the selection ends the (non-void)
// enclosing method and the trailing `return message;` is nameable — the
// call site becomes `return Extract(limit);` and NO extra return is
// appended to the new body (the existing one is kept).
// ---------------------------------------------------------------------
[Fact]
public void Summarize_full_body_selection_becomes_return_call()
{
var (method, calls, tree) = Generate(68, 76);
Assert.Equal("Extract", method.Identifier.ValueText);
Assert.Equal("string", method.ReturnType.ToString());
Assert.Equal(new[] { "int limit" }, method.ParameterList.Parameters.Select(p => p.ToString()));
var body = (BlockSyntax)method.Body!;
Assert.Equal(5, body.Statements.Count); // total, for, scaled, message, return — unchanged
Assert.Equal("return message;", body.Statements[^1].ToString());
Assert.Single(calls);
Assert.Equal("return Extract(limit);", calls[0].ToString());
VerifyZeroDiagnostics(tree);
}
// ---------------------------------------------------------------------
// Heaviest full body (87..93): the trailing `return best;` names the
// single return slot, which matches the enclosing method's return type —
// again a `return Extract(...)` call site, with the widgets/count
// parameters as plain in-params.
// ---------------------------------------------------------------------
[Fact]
public void Heaviest_full_body_selection_becomes_return_call()
{
var (method, calls, tree) = Generate(87, 93);
Assert.Equal("Widget", method.ReturnType.ToString());
Assert.Equal(new[] { "int count", "List widgets" },
method.ParameterList.Parameters.Select(p => p.ToString()));
var body = (BlockSyntax)method.Body!;
Assert.Equal(3, body.Statements.Count);
Assert.Equal("return best;", body.Statements[^1].ToString());
Assert.Single(calls);
Assert.Equal("return Extract(count, widgets);", calls[0].ToString());
VerifyZeroDiagnostics(tree);
}
// ---------------------------------------------------------------------
// ScoreReads (56..58): the enclosing method returns int but the selection
// ends with the COMPOSITE `return score + bonus;` — not nameable in v1,
// so the promoted signature is void and codegen must REFUSE (the report
// note already says: extract a local first, then re-run).
// ---------------------------------------------------------------------
[Fact]
public void ScoreReads_refuses_when_the_composite_trailing_return_is_not_nameable()
{
var (tree, compilation) = DemoFixture.Load();
var model = compilation.GetSemanticModel(tree);
var resolved = SelectionResolver.Resolve(tree, 56, 58);
Assert.True(resolved.Succeeded, resolved.Error);
var suggestion = DataFlowClassifier.Classify(model, resolved);
var signature = SignatureBuilder.Build(model, resolved, suggestion);
Assert.Equal("void", signature.ReturnType);
var ex = Assert.Throws(
() => RefactoringGenerator.Generate(model, resolved, signature));
Assert.Contains("extract it into a local first", ex.Message, StringComparison.Ordinal);
}
// ---------------------------------------------------------------------
// Accumulate for-loop (127..130): `acc` is declared BEFORE the selection,
// written inside and read after — the promoted signature keeps it as a
// ref parameter (the write-back flows the value out; void return, no
// return slot) and the call site is a plain call with `ref acc`.
// ---------------------------------------------------------------------
[Fact]
public void Accumulate_forloop_selection_calls_with_ref_write_back()
{
var (method, calls, tree) = Generate(127, 130);
Assert.Equal("void", method.ReturnType.ToString());
Assert.Equal(new[] { "ref int acc", "int n" },
method.ParameterList.Parameters.Select(p => p.ToString()));
var body = (BlockSyntax)method.Body!;
Assert.Single(body.Statements); // the for-loop; no return appended (void)
Assert.Single(calls);
Assert.Equal("Extract(ref acc, n);", calls[0].ToString());
VerifyZeroDiagnostics(tree);
}
// ---------------------------------------------------------------------
// Pairwise (141..147): two declared-inside locals both written inside and
// read after — the promoted signature returns the tuple `(int, int)` and
// the call site deconstructs it; the new body gains `return (x, y);`.
// ---------------------------------------------------------------------
[Fact]
public void Pairwise_selection_generates_tuple_return_and_deconstruction_call()
{
var (method, calls, tree) = Generate(141, 147);
Assert.Equal("(int, int)", method.ReturnType.ToString());
Assert.Equal(new[] { "int n" }, method.ParameterList.Parameters.Select(p => p.ToString()));
var body = (BlockSyntax)method.Body!;
Assert.Equal(4, body.Statements.Count); // x, y, for-loop, appended tuple return
Assert.Equal("return (x, y);", body.Statements[^1].ToString());
Assert.Single(calls);
Assert.Equal("var (x, y) = Extract(n);", calls[0].ToString());
VerifyZeroDiagnostics(tree);
}
// ---------------------------------------------------------------------
// The unified diff, pinned exactly for a minimal edit (hunk header,
// context, deletion before insertion).
// ---------------------------------------------------------------------
[Fact]
public void UnifiedDiff_pins_the_hunk_format()
{
var diff = UnifiedDiff.Diff(
"a\nb\nc\n", "a\nX\nc\nd\n", "Demo.cs", "Demo.cs (generated)");
Assert.Equal(
"--- Demo.cs\n" +
"+++ Demo.cs (generated)\n" +
"@@ -1,3 +1,4 @@\n" +
" a\n" +
"-b\n" +
"+X\n" +
" c\n" +
"+d\n",
diff);
}
// ---------------------------------------------------------------------
// Identical texts produce a header with no hunks.
// ---------------------------------------------------------------------
[Fact]
public void UnifiedDiff_of_identical_texts_is_header_only()
{
Assert.Equal("--- a\n+++ b\n", UnifiedDiff.Diff("same\n", "same\n", "a", "b"));
}
// ---------------------------------------------------------------------
private static (MethodDeclarationSyntax Method, List Calls, SyntaxTree Tree)
Generate(int startLine, int endLine)
{
var (tree, compilation) = DemoFixture.Load();
var model = compilation.GetSemanticModel(tree);
var resolved = SelectionResolver.Resolve(tree, startLine, endLine);
Assert.True(resolved.Succeeded, resolved.Error);
var suggestion = DataFlowClassifier.Classify(model, resolved);
var signature = SignatureBuilder.Build(model, resolved, suggestion);
var refactoring = RefactoringGenerator.Generate(model, resolved, signature);
return (refactoring.NewMethod, refactoring.CallStatements.ToList(), refactoring.TransformedTree);
}
///
/// The yak's round-trip check: the transformed tree must compile with
/// ZERO diagnostics under the same scratch compilation the input file
/// compiles under.
///
private static void VerifyZeroDiagnostics(SyntaxTree tree)
{
var compilation = CompilationLoader.CreateCompilation(tree, "DemoFixture");
var diagnostics = compilation.GetDiagnostics().ToList();
Assert.True(diagnostics.Count == 0, string.Join("\n", diagnostics));
}
}