// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Differencing;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.EditAndContinue.UnitTests;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue.UnitTests;

[UseExportProvider]
public sealed class CSharpEditAndContinueAnalyzerTests
{
    private static readonly TestComposition s_composition = FeaturesTestCompositions.Features;

    #region Helpers

    private static TestWorkspace CreateWorkspace()
        => new(composition: s_composition);

    private static Solution AddDefaultTestProject(Solution solution, string source)
        => solution.
            AddTestProject("proj", LanguageNames.CSharp).
            AddTestDocument(source, path: Path.Combine(TempRoot.Root, "test.cs")).Project.Solution;

    private static void TestSpans(string source, Func<SyntaxNode, bool> hasLabel)
    {
        var tree = SyntaxFactory.ParseSyntaxTree(source);

        foreach (var expected in GetExpectedSpans(source))
        {
            var expectedText = source.Substring(expected.Start, expected.Length);
            var token = tree.GetRoot().FindToken(expected.Start);
            var node = token.Parent;
            while (!hasLabel(node))
            {
                node = node.Parent;
            }

            var actual = CSharpEditAndContinueAnalyzer.GetDiagnosticSpan(node, EditKind.Update);
            var actualText = source.Substring(actual.Start, actual.Length);

            Assert.True(expected == actual,
                $"{Environment.NewLine}Expected span: '{expectedText}' {expected}" +
                $"{Environment.NewLine}Actual span: '{actualText}' {actual}");
        }
    }

    private static IEnumerable<TextSpan> GetExpectedSpans(string source)
    {
        const string StartTag = "/*<span>*/";
        const string EndTag = "/*</span>*/";
        var i = 0;

        while (true)
        {
            var start = source.IndexOf(StartTag, i, StringComparison.Ordinal);
            if (start == -1)
            {
                break;
            }

            start += StartTag.Length;
            var end = source.IndexOf(EndTag, start + 1, StringComparison.Ordinal);
            yield return new TextSpan(start, end - start);
            i = end + 1;
        }
    }

    private static void TestErrorSpansAllKinds(Func<SyntaxKind, bool> hasLabel)
    {
        var unhandledKinds = new List<SyntaxKind>();
        foreach (var kind in Enum.GetValues<SyntaxKind>().Where(hasLabel))
        {
            TextSpan? span;
            try
            {
                span = CSharpEditAndContinueAnalyzer.TryGetDiagnosticSpanImpl(kind, null, EditKind.Update);
            }
            catch (NullReferenceException)
            {
                // expected, we passed null node
                continue;
            }

            // unexpected:
            if (span == null)
            {
                unhandledKinds.Add(kind);
            }
        }

        AssertEx.Equal(Array.Empty<SyntaxKind>(), unhandledKinds);
    }

    private static async Task<DocumentAnalysisResults> AnalyzeDocumentAsync(
        Project oldProject,
        Document newDocument,
        ActiveStatementsMap activeStatementMap = null,
        EditAndContinueCapabilities capabilities = EditAndContinueTestVerifier.Net5RuntimeCapabilities,
        ImmutableArray<ActiveStatementLineSpan> newActiveStatementSpans = default)
    {
        var analyzer = oldProject.Services.GetRequiredService<IEditAndContinueAnalyzer>();
        var baseActiveStatements = AsyncLazy.Create(activeStatementMap ?? ActiveStatementsMap.Empty);
        var lazyCapabilities = AsyncLazy.Create(capabilities);
        var log = new TraceLog("Test");
        return await analyzer.AnalyzeDocumentAsync(newDocument.Id, oldProject, newDocument.Project, baseActiveStatements, newActiveStatementSpans.NullToEmpty(), lazyCapabilities, log, CancellationToken.None);
    }

    #endregion

    [Fact]
    public void ErrorSpans_TopLevel()
        => TestSpans("""
            /*<span>*/extern alias A;/*</span>*/
            /*<span>*/using Z = Goo.Bar;/*</span>*/

            [assembly: /*<span>*/A(1,2,3,4)/*</span>*/, /*<span>*/B/*</span>*/]

            /*<span>*/namespace N.M/*</span>*/ { }

            [A, B]
            /*<span>*/struct S<[A]T>/*</span>*/ : B 
                /*<span>*/where T : new, struct/*</span>*/ { }

            [A, B]
            /*<span>*/public abstract partial class C/*</span>*/ { }

            [A, B]
            /*<span>*/public abstract partial record R/*</span>*/ { }

            [A, B]
            /*<span>*/public abstract partial record struct R/*</span>*/ { }

            /*<span>*/interface I/*</span>*/ : J, K, L { }

            [A]
            /*<span>*/enum E1/*</span>*/ { }

            /*<span>*/enum E2/*</span>*/ : uint { }

            /*<span>*/public enum E3/*</span>*/
            { 
                Q,
                [A]R = 3
            }

            [A]
            /*<span>*/public delegate void D1<T>()/*</span>*/ where T : struct;

            /*<span>*/delegate C<T> D2()/*</span>*/;

            [Attrib]
            /*<span>*/public class Z/*</span>*/
            {
                /*<span>*/int f/*</span>*/;
                [A]/*<span>*/int f = 1/*</span>*/;
                /*<span>*/public static readonly int f/*</span>*/;

                /*<span>*/int M1()/*</span>*/ { }
                [A]/*<span>*/int M2()/*</span>*/ { }
                [A]/*<span>*/int M3<T1, T2>()/*</span>*/ where T1 : bar where T2 : baz { }

                [A]/*<span>*/abstract C<T> M4()/*</span>*/;
                int M5([A]/*<span>*/Z d = 2345/*</span>*/, /*<span>*/ref int x/*</span>*/, /*<span>*/params int[] x/*</span>*/) { return 1; }

                [A]/*<span>*/event A E1/*</span>*/;
                [A]/*<span>*/public event A E2/*</span>*/;

                [A]/*<span>*/public abstract event A E3/*</span>*/ { /*<span>*/add/*</span>*/; /*<span>*/remove/*</span>*/; }
                [A]/*<span>*/public abstract event A E4/*</span>*/ { [A, B]/*<span>*/add/*</span>*/ { } [A]/*<span>*/internal remove/*</span>*/ { } }

                [A]/*<span>*/int P/*</span>*/ { get; set; }
                [A]/*<span>*/internal string P/*</span>*/ { /*<span>*/internal get/*</span>*/ { } [A]/*<span>*/set/*</span>*/ { }}

                [A]/*<span>*/internal string this[int a, int b]/*</span>*/ { /*<span>*/get/*</span>*/ { } /*<span>*/set/*</span>*/ { } }
                [A]/*<span>*/string this[[A]int a = 123]/*</span>*/ { get { } set { } }

                [A]/*<span>*/public static explicit operator int(Z d)/*</span>*/ { return 1; }
                [A]/*<span>*/operator double(Z d)/*</span>*/ { return 1; }

                [A]/*<span>*/public static operator +(Z d, int x)/*</span>*/ { return 1; }
                [A]/*<span>*/operator +(Z d, int x)/*</span>*/ { return 1; }

            }
            """, SyntaxComparer.TopLevel.HasLabel);

    [Fact]
    public void ErrorSpans_StatementLevel_Update()
    {
        // TODO: test
        // /*<span>*/F($$from a in b from c in d select a.x);/*</span>*/
        // /*<span>*/F(from a in b $$from c in d select a.x);/*</span>*/
        TestSpans("""
            class C
            {
                void M()
                {
                    /*<span>*/{/*</span>*/}
                    /*<span>*/using (expr)/*</span>*/ {}
                    /*<span>*/fixed (int* a = expr)/*</span>*/ {}
                    /*<span>*/lock (expr)/*</span>*/ {}
                    /*<span>*/yield break;/*</span>*/
                    /*<span>*/yield return 1;/*</span>*/
                    /*<span>*/try/*</span>*/ {} catch { };
                    try {} /*<span>*/catch/*</span>*/ { };
                    try {} /*<span>*/finally/*</span>*/ { };
                    /*<span>*/if (expr)/*</span>*/ { };
                    if (expr) { } /*<span>*/else/*</span>*/ { };
                    /*<span>*/while (expr)/*</span>*/ { };
                    /*<span>*/do/*</span>*/ {} while (expr);
                    /*<span>*/for (;;)/*</span>*/ { };
                    /*<span>*/foreach (var a in b)/*</span>*/ { };
                    /*<span>*/switch (expr)/*</span>*/ { case 1: break; };
                    switch (expr) { case 1: /*<span>*/goto case 1;/*</span>*/ };
                    switch (expr) { case 1: /*<span>*/goto case default;/*</span>*/ };
                    /*<span>*/label/*</span>*/: Goo();
                    /*<span>*/checked/*</span>*/ { };
                    /*<span>*/unchecked/*</span>*/ { };
                    /*<span>*/unsafe/*</span>*/ { };
                    /*<span>*/return expr;/*</span>*/
                    /*<span>*/throw expr;/*</span>*/
                    /*<span>*/break;/*</span>*/
                    /*<span>*/continue;/*</span>*/
                    /*<span>*/goto label;/*</span>*/
                    /*<span>*/expr;/*</span>*/
                    /*<span>*/int a;/*</span>*/
                    F(/*<span>*/(x)/*</span>*/ => x);
                    F(/*<span>*/x/*</span>*/ => x);
                    F(/*<span>*/delegate/*</span>*/(x) { });
                    F(from a in b /*<span>*/select/*</span>*/ a.x);
                    F(from a in b /*<span>*/let/*</span>*/ x = expr select expr);
                    F(from a in b /*<span>*/where/*</span>*/ expr select expr);
                    F(from a in b /*<span>*/join/*</span>*/ c in d on e equals f select g);
                    F(from a in b orderby /*<span>*/a/*</span>*/ select b);
                    F(from a in b orderby a, /*<span>*/b descending/*</span>*/ select b);
                    F(from a in b /*<span>*/group/*</span>*/ a by b select d);
                }
            }
            """, SyntaxComparer.Statement.HasLabel);
    }

    /// <summary>
    /// Verifies that <see cref="CSharpEditAndContinueAnalyzer.TryGetDiagnosticSpanImpl"/> handles all <see cref="SyntaxKind"/>s.
    /// </summary>
    [Fact]
    public void ErrorSpansAllKinds()
    {
        TestErrorSpansAllKinds(SyntaxComparer.Statement.HasLabel);
        TestErrorSpansAllKinds(SyntaxComparer.TopLevel.HasLabel);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_InsignificantChangesInMethodBody()
    {
        var source1 = """
            class C
            {
                public static void Main()
                {
                    /* comment */ System.Console.WriteLine(1);
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1);
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var oldText = await oldDocument.GetTextAsync();
        var oldSyntaxRoot = await oldDocument.GetSyntaxRootAsync();
        var documentId = oldDocument.Id;
        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));
        var newDocument = newSolution.GetDocument(documentId);
        var newText = await newDocument.GetTextAsync();
        var newSyntaxRoot = await newDocument.GetSyntaxRootAsync();

        var oldStatementSource = "System.Console.WriteLine(1);";
        var oldStatementPosition = source1.IndexOf(oldStatementSource, StringComparison.Ordinal);
        var oldStatementTextSpan = new TextSpan(oldStatementPosition, oldStatementSource.Length);
        var oldStatementSpan = oldText.Lines.GetLinePositionSpan(oldStatementTextSpan);
        var oldStatementSyntax = oldSyntaxRoot.FindNode(oldStatementTextSpan);

        var baseActiveStatements = new ActiveStatementsMap(
            ImmutableDictionary.CreateRange(
            [
                KeyValuePair.Create(newDocument.FilePath, ImmutableArray.Create(
                    new ActiveStatement(
                        new ActiveStatementId(0),
                        ActiveStatementFlags.LeafFrame,
                        new SourceFileSpan(newDocument.FilePath, oldStatementSpan),
                        instructionId: default)))
            ]),
            ActiveStatementsMap.Empty.InstructionMap);

        var result = await AnalyzeDocumentAsync(oldProject, newDocument, baseActiveStatements);

        Assert.True(result.HasChanges);

        var syntaxMaps = result.SemanticEdits[0].SyntaxMaps;
        Assert.True(syntaxMaps.HasMap);

        var newStatementSpan = result.ActiveStatements[0].Span;
        var newStatementTextSpan = newText.Lines.GetTextSpan(newStatementSpan);
        var newStatementSyntax = newSyntaxRoot.FindNode(newStatementTextSpan);

        var oldStatementSyntaxMapped = syntaxMaps.MatchingNodes(newStatementSyntax);
        Assert.Same(oldStatementSyntax, oldStatementSyntaxMapped);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_SyntaxError_Change()
    {
        var source1 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1) // syntax error
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(2) // syntax error
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;
        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));

        var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId));

        Assert.True(result.HasChanges);
        Assert.True(result.AnalysisBlocked);
        Assert.NotNull(result.SyntaxError);
        Assert.False(result.HasBlockingRudeEdits);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_SyntaxError_NoChange()
    {
        var source = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1) // syntax error
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();

        var result = await AnalyzeDocumentAsync(oldProject, oldDocument);

        Assert.False(result.HasChanges);
        Assert.False(result.AnalysisBlocked);
        Assert.Null(result.SyntaxError);
        Assert.False(result.HasBlockingRudeEdits);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_SyntaxError_NoChange2()
    {
        var source1 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1) // syntax error
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1) // syntax error
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;

        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));

        var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId));

        Assert.False(result.HasChanges);
        Assert.False(result.AnalysisBlocked);
        Assert.Null(result.SyntaxError);
        Assert.False(result.HasBlockingRudeEdits);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_Features_NoChange()
    {
        var source = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1);
                }
            }
            """;
        var experimentalFeatures = new Dictionary<string, string>(); // no experimental features to enable
        var experimental = TestOptions.Regular.WithFeatures(experimentalFeatures);

        using var workspace = CreateWorkspace();

        var oldSolution = workspace.CurrentSolution.
            AddTestProject("proj", LanguageNames.CSharp).WithParseOptions(experimental).
            AddTestDocument(source, path: Path.Combine(TempRoot.Root, "test.cs")).Project.Solution;

        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;

        var result = await AnalyzeDocumentAsync(oldProject, oldDocument);

        Assert.False(result.HasChanges);
        Assert.False(result.AnalysisBlocked);
        Assert.False(result.HasBlockingRudeEdits);
        Assert.Null(result.SyntaxError);
        Assert.True(result.RudeEdits.IsEmpty);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_Features_Change()
    {
        // these are all the experimental features currently implemented
        var experimentalFeatures = Array.Empty<string>();

        foreach (var feature in experimentalFeatures)
        {
            var source1 = """
                class C
                {
                    public static void Main()
                    {
                        System.Console.WriteLine(1);
                    }
                }
                """;
            var source2 = """
                class C
                {
                    public static void Main()
                    {
                        System.Console.WriteLine(2);
                    }
                }
                """;

            var featuresToEnable = new Dictionary<string, string>() { { feature, "enabled" } };
            var experimental = TestOptions.Regular.WithFeatures(featuresToEnable);

            using var workspace = TestWorkspace.CreateCSharp(
                source1, parseOptions: experimental, compilationOptions: null);

            var oldSolution = workspace.CurrentSolution;
            var oldProject = oldSolution.Projects.Single();
            var oldDocument = oldProject.Documents.Single();
            var documentId = oldDocument.Id;

            var newSolution = workspace.CurrentSolution.WithDocumentText(documentId, SourceText.From(source2));

            var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId));

            Assert.True(result.HasChanges);
            Assert.True(result.AnalysisBlocked);
            Assert.False(result.HasBlockingRudeEdits);
            Assert.Equal(RudeEditKind.ExperimentalFeaturesEnabled, result.RudeEdits.Single().Kind);
        }
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_SemanticError_NoChange()
    {
        var source = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1);
                    Bar(); // semantic error
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source);

        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;

        var result = await AnalyzeDocumentAsync(oldProject, oldDocument);

        Assert.False(result.HasChanges);
        Assert.False(result.HasBlockingRudeEdits);
        Assert.False(result.AnalysisBlocked);
        Assert.Null(result.SyntaxError);
    }

    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/10683")]
    public async Task AnalyzeDocumentAsync_SemanticErrorInMethodBody_Change()
    {
        var source1 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1);
                    Bar(); // semantic error
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(2);
                    Bar(); // semantic error
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;

        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));

        var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId));

        Assert.True(result.HasChanges);

        // no declaration errors (error in method body is only reported when emitting):
        Assert.False(result.HasBlockingRudeEdits);
        Assert.False(result.AnalysisBlocked);
        Assert.Null(result.SyntaxError);
    }

    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/10683")]
    public async Task AnalyzeDocumentAsync_SemanticErrorInDeclaration_Change()
    {
        var source1 = """
            class C
            {
                public static void Main(Bar x)
                {
                    System.Console.WriteLine(1);
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main(Bar x)
                {
                    System.Console.WriteLine(2);
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var oldDocument = oldProject.Documents.Single();
        var documentId = oldDocument.Id;

        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));

        var result = await AnalyzeDocumentAsync(oldProject, newSolution.GetDocument(documentId));

        Assert.True(result.HasChanges);

        // No errors reported: EnC analyzer is resilient against semantic errors.
        // They will be reported by 1) compiler diagnostic analyzer 2) when emitting delta - if still present.
        Assert.False(result.AnalysisBlocked);
        Assert.False(result.HasBlockingRudeEdits);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_AddingNewFileHavingRudeEdits()
    {
        var source1 = """
            namespace N
            {
                class C
                {
                    public static void Main()
                    {
                    }
                }
            }
            """;
        var source2 = """
            namespace N
            {
                public class D
                {
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);

        // fork the solution to introduce a change
        var oldProject = oldSolution.Projects.Single();
        var newDocId = DocumentId.CreateNewId(oldProject.Id);
        var newSolution = oldSolution.AddDocument(newDocId, "goo.cs", SourceText.From(source2), filePath: Path.Combine(TempRoot.Root, "goo.cs"));

        var newProject = newSolution.Projects.Single();
        var changes = newProject.GetChanges(oldProject);

        Assert.Equal(2, newProject.Documents.Count());
        Assert.Equal(0, changes.GetChangedDocuments().Count());
        Assert.Equal(1, changes.GetAddedDocuments().Count());

        var changedDocuments = changes.GetChangedDocuments().Concat(changes.GetAddedDocuments());

        var result = new List<DocumentAnalysisResults>();
        foreach (var changedDocumentId in changedDocuments)
        {
            result.Add(await AnalyzeDocumentAsync(oldProject, newProject.GetDocument(changedDocumentId)));
        }

        Assert.True(result.IsSingle());
        Assert.Empty(result.Single().RudeEdits);
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_AddingNewFile()
    {
        var source1 = """
            namespace N
            {
                class C
                {
                    public static void Main()
                    {
                    }
                }
            }
            """;
        var source2 = """
            class D
            {
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);

        var oldProject = oldSolution.Projects.Single();
        var newDocId = DocumentId.CreateNewId(oldProject.Id);
        var newSolution = oldSolution.AddDocument(newDocId, "goo.cs", SourceText.From(source2), filePath: Path.Combine(TempRoot.Root, "goo.cs"));

        var newProject = newSolution.Projects.Single();
        var changes = newProject.GetChanges(oldProject);

        Assert.Equal(2, newProject.Documents.Count());
        Assert.Equal(0, changes.GetChangedDocuments().Count());
        Assert.Equal(1, changes.GetAddedDocuments().Count());

        var changedDocuments = changes.GetChangedDocuments().Concat(changes.GetAddedDocuments());

        var result = new List<DocumentAnalysisResults>();
        foreach (var changedDocumentId in changedDocuments)
        {
            result.Add(await AnalyzeDocumentAsync(oldProject, newProject.GetDocument(changedDocumentId)));
        }

        Assert.True(result.IsSingle());
        Assert.Empty(result.Single().RudeEdits);
    }

    [Theory, CombinatorialData]
    public async Task AnalyzeDocumentAsync_InternalError(bool outOfMemory)
    {
        var source1 = @"class C {}";
        var source2 = @"class C { int x; }";

        var filePath = Path.Combine(TempRoot.Root, "src.cs");

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);
        var oldProject = oldSolution.Projects.Single();
        var documentId = DocumentId.CreateNewId(oldProject.Id);
        var newSolution = oldSolution.AddDocument(documentId, "goo.cs", SourceText.From(source2), filePath: filePath);
        var newProject = newSolution.Projects.Single();
        var newDocument = newProject.GetDocument(documentId);
        var newSyntaxTree = await newDocument.GetSyntaxTreeAsync().ConfigureAwait(false);

        var baseActiveStatements = AsyncLazy.Create(ActiveStatementsMap.Empty);
        var capabilities = AsyncLazy.Create(EditAndContinueTestVerifier.Net5RuntimeCapabilities);

        var analyzer = Assert.IsType<CSharpEditAndContinueAnalyzer>(oldProject.Services.GetRequiredService<IEditAndContinueAnalyzer>());
        analyzer.GetTestAccessor().FaultInjector = node =>
        {
            if (node is CompilationUnitSyntax)
            {
                throw outOfMemory ? new OutOfMemoryException() : new NullReferenceException("NullRef!");
            }
        };

        var log = new TraceLog("Test");
        var result = await analyzer.AnalyzeDocumentAsync(newDocument.Id, oldProject, newDocument.Project, baseActiveStatements, [], capabilities, log, CancellationToken.None);

        var expectedDiagnostic = outOfMemory
            ? $"ENC0089: {string.Format(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_because_the_file_is_too_big, filePath)}"
            // Because the error message that is formatted into this template string includes a stacktrace with newlines, we need to replicate that behavior
            // here so that any trailing punctuation is removed from the translated template string.
            : $"ENC0080: {string.Format(FeaturesResources.Modifying_source_file_0_requires_restarting_the_application_due_to_internal_error_1, filePath, "System.NullReferenceException: NullRef!\n")}".Split('\n').First();

        AssertEx.Equal([expectedDiagnostic], result.RudeEdits.Select(d => d.ToDiagnostic(newSyntaxTree))
            .Select(d => $"{d.Id}: {d.GetMessage().Split([Environment.NewLine], StringSplitOptions.None).First()}"));
    }

    [Fact]
    public async Task AnalyzeDocumentAsync_NotSupportedByRuntime()
    {
        var source1 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(1);
                }
            }
            """;
        var source2 = """
            class C
            {
                public static void Main()
                {
                    System.Console.WriteLine(2);
                }
            }
            """;

        using var workspace = CreateWorkspace();
        var oldSolution = AddDefaultTestProject(workspace.CurrentSolution, source1);

        var oldProject = oldSolution.Projects.Single();
        var documentId = oldProject.Documents.Single().Id;
        var newSolution = oldSolution.WithDocumentText(documentId, SourceText.From(source2));
        var newDocument = newSolution.GetDocument(documentId);

        var result = await AnalyzeDocumentAsync(oldProject, newDocument, capabilities: EditAndContinueCapabilities.None);

        Assert.Equal(RudeEditKind.NotSupportedByRuntime, result.RudeEdits.Single().Kind);
    }
}
