// 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.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.Diagnostics)]
    public class DiagnosticDataTests
    {
        [Fact]
        public async Task DiagnosticData_GetText()
        {
            var code = "";
            await VerifyTextSpanAsync(code, 10, 10, 20, 20, new TextSpan(0, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText1()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 30, 30, 40, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText2()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 0, 30, 40, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText3()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 0, 30, 0, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText4()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 1, 30, 1, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText5()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 1, 30, 1, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText6()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 1, 30, 2, 40, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText7()
        {
            var code = @"
";

            await VerifyTextSpanAsync(code, 1, 0, 1, 2, new TextSpan(code.Length, 0));
        }

        [Fact]
        public async Task DiagnosticData_GetText8()
        {
            var code = @"
namespace B
{
    class A
    {
    }
}
";

            await VerifyTextSpanAsync(code, 3, 10, 3, 11, new TextSpan(28, 1));
        }

        private static async Task VerifyTextSpanAsync(string code, int startLine, int startColumn, int endLine, int endColumn, TextSpan span)
        {
            using var workspace = new TestWorkspace(composition: EditorTestCompositions.EditorFeatures);
            var document = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp).AddDocument("TestDocument", code);

            var data = new DiagnosticData(
                id: "test1",
                category: "Test",
                message: "test1 message",
                severity: DiagnosticSeverity.Info,
                defaultSeverity: DiagnosticSeverity.Info,
                isEnabledByDefault: false,
                warningLevel: 1,
                projectId: document.Project.Id,
                customTags: ImmutableArray<string>.Empty,
                properties: ImmutableDictionary<string, string>.Empty,
                location: new DiagnosticDataLocation(new("originalFile1", new(startLine, startColumn), new(endLine, endColumn)), document.Id),
                language: document.Project.Language);

            var text = await document.GetTextAsync();
            var actual = data.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text);

            Assert.Equal(span, actual);
        }

        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/46377")]
        public async Task DiagnosticData_ExternalAdditionalLocationIsPreserved()
        {
            using var workspace = new TestWorkspace(composition: EditorTestCompositions.EditorFeatures);

            var additionalDocument = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
                .AddDocument("test.cs", "", filePath: "test.cs")
                .Project.AddAdditionalDocument("AdditionalDocument.txt", "First line in file", filePath: "AdditionalDocument.txt");
            var document = additionalDocument.Project.Documents.Single();

            var externalAdditionalLocation = new DiagnosticDataLocation(
                new(additionalDocument.Name, new(0, 0), new(0, 1)), additionalDocument.Id);

            var diagnosticData = new DiagnosticData(
                id: "test1",
                category: "Test",
                message: "test1 message",
                severity: DiagnosticSeverity.Info,
                defaultSeverity: DiagnosticSeverity.Info,
                isEnabledByDefault: true,
                warningLevel: 1,
                projectId: document.Project.Id,
                customTags: ImmutableArray<string>.Empty,
                properties: ImmutableDictionary<string, string>.Empty,
                location: new DiagnosticDataLocation(new FileLinePositionSpan(document.FilePath, span: default), document.Id),
                additionalLocations: ImmutableArray.Create(externalAdditionalLocation),
                language: document.Project.Language);

            var diagnostic = await diagnosticData.ToDiagnosticAsync(document.Project, CancellationToken.None);
            var roundTripDiagnosticData = DiagnosticData.Create(diagnostic, document);

            var roundTripAdditionalLocation = Assert.Single(roundTripDiagnosticData.AdditionalLocations);
            Assert.Equal(externalAdditionalLocation.DocumentId, roundTripAdditionalLocation.DocumentId);
            Assert.Equal(externalAdditionalLocation.UnmappedFileSpan, roundTripAdditionalLocation.UnmappedFileSpan);
        }

        [Fact]
        public async Task DiagnosticData_SourceGeneratedDocumentLocationIsPreserved()
        {
            var content = @"
namespace B
{
    class A
    {
    }
}
";
            using var workspace = TestWorkspace.CreateCSharp(files: Array.Empty<string>(), sourceGeneratedFiles: new[] { content }, composition: EditorTestCompositions.EditorFeatures);
            var hostDocument = workspace.Documents.Single();
            Assert.True(hostDocument.IsSourceGenerated);

            var documentId = hostDocument.Id;
            var project = workspace.CurrentSolution.GetRequiredProject(documentId.ProjectId);
            var document = await project.GetSourceGeneratedDocumentAsync(documentId, CancellationToken.None);

            await VerifyTextSpanAsync(content, 3, 10, 3, 11, new TextSpan(28, 1));
            var location = new DiagnosticDataLocation(
                new(document.FilePath, new(3, 10), new(3, 11)), documentId);

            var diagnosticData = new DiagnosticData(
                id: "test1",
                category: "Test",
                message: "test1 message",
                severity: DiagnosticSeverity.Info,
                defaultSeverity: DiagnosticSeverity.Info,
                isEnabledByDefault: true,
                warningLevel: 1,
                projectId: documentId.ProjectId,
                customTags: ImmutableArray<string>.Empty,
                properties: ImmutableDictionary<string, string>.Empty,
                location: location,
                additionalLocations: ImmutableArray<DiagnosticDataLocation>.Empty,
                language: project.Language);

            var diagnostic = await diagnosticData.ToDiagnosticAsync(project, CancellationToken.None);
            var roundTripDiagnosticData = DiagnosticData.Create(diagnostic, document);

            var roundTripLocation = roundTripDiagnosticData.DataLocation;
            Assert.NotNull(roundTripDiagnosticData.DataLocation);
            Assert.Equal(location.DocumentId, roundTripLocation.DocumentId);
            Assert.Equal(location.UnmappedFileSpan, roundTripLocation.UnmappedFileSpan);
        }

        [Theory, CombinatorialData]
        [WorkItem("https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_workitems/edit/1676229")]
        public async Task DiagnosticData_SourceFileAdditionalLocationIsPreserved(bool testDifferentProject, bool testRemovedDocument)
        {
            using var workspace = new TestWorkspace(composition: EditorTestCompositions.EditorFeatures);

            var firstDocument = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
                .AddDocument("test.cs", "class C1 { }", filePath: "test.cs");
            Document secondDocument;
            if (testDifferentProject)
            {
                secondDocument = firstDocument.Project.Solution.AddProject("TestProject2", "TestProject2", LanguageNames.CSharp)
                    .AddDocument("test2.cs", "class C2 { }", filePath: "test2.cs");
            }
            else
            {
                secondDocument = firstDocument.Project.AddDocument("test2.cs", "class C2 { }", filePath: "test2.cs");
            }

            firstDocument = secondDocument.Project.Solution.GetRequiredDocument(firstDocument.Id);

            var additionalLocation = new DiagnosticDataLocation(
                new(secondDocument.Name, new(0, 0), new(0, 1)), secondDocument.Id);

            var diagnosticData = new DiagnosticData(
                id: "test1",
                category: "Test",
                message: "test1 message",
                severity: DiagnosticSeverity.Info,
                defaultSeverity: DiagnosticSeverity.Info,
                isEnabledByDefault: true,
                warningLevel: 1,
                projectId: firstDocument.Project.Id,
                customTags: ImmutableArray<string>.Empty,
                properties: ImmutableDictionary<string, string>.Empty,
                location: new DiagnosticDataLocation(new FileLinePositionSpan(firstDocument.FilePath, span: default), firstDocument.Id),
                additionalLocations: ImmutableArray.Create(additionalLocation),
                language: firstDocument.Project.Language);

            var diagnostic = await diagnosticData.ToDiagnosticAsync(firstDocument.Project, CancellationToken.None);

            if (testRemovedDocument)
            {
                firstDocument = firstDocument.Project.Solution
                    .RemoveDocument(secondDocument.Id)
                    .GetRequiredDocument(firstDocument.Id);
            }

            var roundTripDiagnosticData = DiagnosticData.Create(diagnostic, firstDocument);

            var roundTripAdditionalLocation = Assert.Single(roundTripDiagnosticData.AdditionalLocations);
            var expectedAdditionalDocumentId = !testRemovedDocument ? additionalLocation.DocumentId : null;
            Assert.Equal(expectedAdditionalDocumentId, roundTripAdditionalLocation.DocumentId);
            Assert.Equal(additionalLocation.UnmappedFileSpan, roundTripAdditionalLocation.UnmappedFileSpan);
        }
    }
}
