// 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.

using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Analyzers.MatchFolderAndNamespace;
using Microsoft.CodeAnalysis.CSharp.CodeFixes.MatchFolderAndNamespace;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UnitTests.MatchFolderAndNamespace;

using VerifyCS = CSharpCodeFixVerifier<
    CSharpMatchFolderAndNamespaceDiagnosticAnalyzer,
    CSharpChangeNamespaceToMatchFolderCodeFixProvider>;

public sealed class CSharpMatchFolderAndNamespaceTests
{
    private static readonly string Directory = "/0/";

    // DefaultNamespace gets exposed as RootNamespace in the build properties
    private const string DefaultNamespace = "Test.Root.Namespace";

    private static readonly string EditorConfig = $"""
        is_global=true
        build_property.ProjectDir = {Directory}
        build_property.RootNamespace = {DefaultNamespace}
        """;

    private static string CreateFolderPath(params string[] folders)
        => Path.Combine(Directory, Path.Combine(folders));

    private static Task RunTestAsync(string fileName, string fileContents, string? directory = null, string? editorConfig = null, string? fixedCode = null, string? defaultNamespace = null)
    {
        var filePath = Path.Combine(directory ?? Directory, fileName);
        fixedCode ??= fileContents;

        return RunTestAsync(
            new[] { (filePath, fileContents) },
            new[] { (filePath, fixedCode) },
            editorConfig,
            defaultNamespace);
    }

    private static Task RunTestAsync(IEnumerable<(string, string)> originalSources, IEnumerable<(string, string)>? fixedSources = null, string? editorconfig = null, string? defaultNamespace = null)
    {
        // When a namespace isn't provided we will fallback on our default
        defaultNamespace ??= DefaultNamespace;

        var testState = new VerifyCS.Test
        {
            EditorConfig = editorconfig ?? EditorConfig,
            CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck,
            LanguageVersion = LanguageVersion.CSharp10,
        };

        foreach (var (fileName, content) in originalSources)
            testState.TestState.Sources.Add((fileName, content));

        fixedSources ??= [];
        foreach (var (fileName, content) in fixedSources)
            testState.FixedState.Sources.Add((fileName, content));

        // If empty string was provided as the namespace, then we will not set a default
        if (defaultNamespace.Length > 0)
        {
            testState.SolutionTransforms.Add((solution, projectId) =>
            {
                var project = solution.GetRequiredProject(projectId);
                return project.WithDefaultNamespace(defaultNamespace).Solution;
            });
        }

        return testState.RunAsync();
    }

    [Fact]
    public Task InvalidFolderName1_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["3B", "C"]);
        return RunTestAsync(
            "File1.cs",
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public Task InvalidFolderName1_NoDiagnostic_FileScopedNamespace()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["3B", "C"]);
        return RunTestAsync(
            "File1.cs",
            """
            namespace A.B;

            class Class1
            {
            }
            """,
            directory: folder);
    }

    [Fact]
    public Task InvalidFolderName2_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["B.3C", "D"]);
        return RunTestAsync(
            "File1.cs",
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public Task InvalidFolderName3_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath([".folder", "..subfolder", "name"]);
        return RunTestAsync(
            "File1.cs",
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public Task CaseInsensitiveMatch_NoDiagnostic()
    {
        var folder = CreateFolderPath(["A", "B"]);
        return RunTestAsync(
            "File1.cs",
            $$"""
            namespace {{DefaultNamespace}}.a.b
            {
                class Class1
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public async Task CodeStyleOptionIsFalse()
    {
        var folder = CreateFolderPath("B", "C");
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """,
            directory: folder,
            editorConfig: EditorConfig + """

            dotnet_style_namespace_match_folder = false
            """);
    }

    [Fact]
    public async Task SingleDocumentNoReference()
    {
        var folder = CreateFolderPath("B", "C");
        var fixedCode =
            $$"""
            namespace {{DefaultNamespace}}.B.C
            {
                class Class1
                {
                }
            }
            """;
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace [|A.B|]
            {
                class Class1
                {
                }
            }
            """,
            directory: folder,
            fixedCode: fixedCode);
    }

    [Fact]
    public async Task SingleDocumentNoReference_FileScopedNamespace()
    {
        var folder = CreateFolderPath("B", "C");
        var fixedCode =
            $$"""
            namespace {{DefaultNamespace}}.B.C;

            class Class1
            {
            }
            """;
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace [|A.B|];

            class Class1
            {
            }
            """,
            directory: folder,
            fixedCode: fixedCode);
    }

    [Fact, WorkItem("https://github.com/dotnet/vscode-csharp/issues/9550")]
    public async Task FileBasedProgram_NoDiagnostic()
    {
        // The namespace does not match the folder structure, but the document is part of a file-based
        // program, so IDE0130 should not fire.
        var folder = CreateFolderPath("B", "C");
        var filePath = Path.Combine(folder, "Class1.cs");

        var testState = new VerifyCS.Test
        {
            EditorConfig = EditorConfig,
            CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck,
            LanguageVersion = LanguageVersion.CSharp10,
        };

        testState.TestState.Sources.Add((filePath, """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """));

        testState.SolutionTransforms.Add((solution, projectId) =>
        {
            var project = solution.GetRequiredProject(projectId);
            var parseOptions = (CSharpParseOptions)project.ParseOptions!;
            return solution
                .WithProjectParseOptions(projectId, parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]))
                .GetRequiredProject(projectId)
                .WithDefaultNamespace(DefaultNamespace)
                .Solution;
        });

        await testState.RunAsync();
    }

    [Fact]
    public async Task SingleDocumentNoReference_NoDefaultNamespace()
    {
        var editorConfig = $"""
            is_global=true
            build_property.ProjectDir = {Directory}
            """;

        var folder = CreateFolderPath("B", "C");
        var fixedCode =
            $$"""
            namespace B.C
            {
                class Class1
                {
                }
            }
            """;
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace [|A.B|]
            {
                class Class1
                {
                }
            }
            """,
            directory: folder,
            fixedCode: fixedCode,
            editorConfig: editorConfig,
            // passing empty string means that a default namespace isn't set on the test Project
            defaultNamespace: string.Empty);
    }

    [Fact]
    public async Task SingleDocumentNoReference_NoDefaultNamespace_FileScopedNamespace()
    {
        var editorConfig = $"""
            is_global=true
            build_property.ProjectDir = {Directory}
            """;

        var folder = CreateFolderPath("B", "C");
        var fixedCode =
            """
            namespace B.C;

            class Class1
            {
            }
            """;
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace [|A.B|];

            class Class1
            {
            }
            """,
            directory: folder,
            fixedCode: fixedCode,
            editorConfig: editorConfig,
            // passing empty string means that a default namespace isn't set on the test Project
            defaultNamespace: string.Empty);
    }

    [Fact]
    public async Task NamespaceWithSpaces_NoDiagnostic()
    {
        var folder = CreateFolderPath("A", "B");
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: $$"""
            namespace {{DefaultNamespace}}.A    .     B
            {
                class Class1
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public async Task NestedNamespaces_NoDiagnostic()
    {
        // The code fix doesn't currently support nested namespaces for sync, so
        // diagnostic does not report.

        var folder = CreateFolderPath("B", "C");
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: """
            namespace A.B
            {
                namespace C.D
                {
                    class CDClass
                    {
                    }
                }

                class ABClass
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public async Task PartialTypeWithMultipleDeclarations_NoDiagnostic()
    {
        // The code fix doesn't currently support nested namespaces for sync, so
        // diagnostic does not report.

        var folder = CreateFolderPath("B", "C");
        var code1 =
            """
            namespace A.B
            {
                partial class ABClass
                {
                    void M1() {}
                }
            }
            """;

        var code2 =
            """
            namespace A.B
            {
                partial class ABClass
                {
                    void M2() {}
                }
            }
            """;

        var sources = new[]
        {
            (Path.Combine(folder, "ABClass1.cs"), code1),
            (Path.Combine(folder, "ABClass2.cs"), code2),
        };

        await RunTestAsync(sources);
    }

    [Fact]
    public async Task FileNotInProjectFolder_NoDiagnostic()
    {
        // Default directory is Test\Directory for the project,
        // putting the file outside the directory should have no
        // diagnostic shown.

        var folder = Path.Combine("B", "C");
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: $$"""
            namespace A.B
            {
                class ABClass
                {
                }
            }
            """,
            directory: folder);
    }

    [Fact]
    public async Task SingleDocumentLocalReference()
    {
        var @namespace = "Bar.Baz";

        var folder = CreateFolderPath("A", "B", "C");
        await RunTestAsync(
            "Class1.cs",
            $$"""
            namespace [|{{@namespace}}|]
            {
                delegate void D1();

                interface Class1
                {
                    void M1();
                }

                class Class2 : {{@namespace}}.Class1
                {
                    {{@namespace}}.D1 d;

                    void {{@namespace}}.Class1.M1() { }
                }
            }
            """,
            folder,
            fixedCode: $$"""
            namespace {{DefaultNamespace}}.A.B.C
            {
                delegate void D1();

                interface Class1
                {
                    void M1();
                }

                class Class2 : Class1
                {
                    D1 d;

                    void Class1.M1() { }
                }
            }
            """);
    }

    [Fact]
    public async Task ChangeUsingsInMultipleContainers()
    {
        var declaredNamespace = "Bar.Baz";

        var folder = CreateFolderPath("A", "B", "C");
        var code1 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class1
                {
                }
            }
            """;

        var code2 =
            $$"""
            namespace NS1
            {
                using {{declaredNamespace}};

                class Class2
                {
                    Class1 c2;
                }

                namespace NS2
                {
                    using {{declaredNamespace}};

                    class Class2
                    {
                        Class1 c1;
                    }
                }
            }
            """;

        var fixed1 =
            $$"""
            namespace {{DefaultNamespace}}.A.B.C
            {
                class Class1
                {
                }
            }
            """;

        var fixed2 =
            $$"""
            namespace NS1
            {
                using {{DefaultNamespace}}.A.B.C;

                class Class2
                {
                    Class1 c2;
                }

                namespace NS2
                {
                    class Class2
                    {
                        Class1 c1;
                    }
                }
            }
            """;

        var originalSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), code1),
            ("Class2.cs", code2)
        };

        var fixedSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), fixed1),
            ("Class2.cs", fixed2)
        };

        await RunTestAsync(originalSources, fixedSources);
    }

    [Fact]
    public async Task DocumentAtRoot_NoDiagnostic()
    {
        var folder = CreateFolderPath();
        await RunTestAsync(
            "File1.cs",
            $$"""
            namespace {{DefaultNamespace}}
            {
                class C { }
            }
            """,
            folder);
    }

    [Fact]
    public async Task DocumentAtRoot_ChangeNamespace()
    {
        var folder = CreateFolderPath();
        var fixedCode =
            $$"""
            namespace {{DefaultNamespace}}
            {
                class C { }
            }
            """;

        await RunTestAsync(
            "File1.cs",
            $$"""
            namespace [|{{DefaultNamespace}}.Test|]
            {
                class C { }
            }
            """,
            folder,
            fixedCode: fixedCode);
    }

    [Fact]
    public async Task ChangeNamespace_WithAliasReferencesInOtherDocument()
    {
        var declaredNamespace = $"Bar.Baz";

        var folder = CreateFolderPath("A", "B", "C");
        var code1 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class1
                {
                }
            }
            """;

        var code2 = $$"""
            using System;
            using {{declaredNamespace}};
            using Class1Alias = {{declaredNamespace}}.Class1;

            namespace Foo
            {
                class RefClass
                {
                    private Class1Alias c1;
                }
            }
            """;

        var fixed1 =
            $$"""
            namespace {{DefaultNamespace}}.A.B.C
            {
                class Class1
                {
                }
            }
            """;

        var fixed2 =
            $$"""
            using System;
            using Class1Alias = {{DefaultNamespace}}.A.B.C.Class1;

            namespace Foo
            {
                class RefClass
                {
                    private Class1Alias c1;
                }
            }
            """;

        var originalSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), code1),
            ("Class2.cs", code2)
        };

        var fixedSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), fixed1),
            ("Class2.cs", fixed2)
        };

        await RunTestAsync(originalSources, fixedSources);
    }

    [Fact]
    public async Task FixAll()
    {
        var declaredNamespace = "Bar.Baz";

        var folder1 = CreateFolderPath("A", "B", "C");
        var fixedNamespace1 = $"{DefaultNamespace}.A.B.C";

        var folder2 = CreateFolderPath("Second", "Folder", "Path");
        var fixedNamespace2 = $"{DefaultNamespace}.Second.Folder.Path";

        var folder3 = CreateFolderPath("Third", "Folder", "Path");
        var fixedNamespace3 = $"{DefaultNamespace}.Third.Folder.Path";

        var code1 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class1
                {
                    Class2 C2 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var fixed1 =
            $$"""
            using {{fixedNamespace2}};
            using {{fixedNamespace3}};

            namespace {{fixedNamespace1}}
            {
                class Class1
                {
                    Class2 C2 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var code2 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class2
                {
                    Class1 C1 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var fixed2 =
            $$"""
            using {{fixedNamespace1}};
            using {{fixedNamespace3}};

            namespace {{fixedNamespace2}}
            {
                class Class2
                {
                    Class1 C1 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var code3 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class3
                {
                    Class1 C1 { get; }
                    Class2 C2 { get; }
                }
            }
            """;

        var fixed3 =
            $$"""
            using {{fixedNamespace1}};
            using {{fixedNamespace2}};

            namespace {{fixedNamespace3}}
            {
                class Class3
                {
                    Class1 C1 { get; }
                    Class2 C2 { get; }
                }
            }
            """;

        var sources = new[]
        {
            (Path.Combine(folder1, "Class1.cs"), code1),
            (Path.Combine(folder2, "Class2.cs"), code2),
            (Path.Combine(folder3, "Class3.cs"), code3),
        };

        var fixedSources = new[]
        {
            (Path.Combine(folder1, "Class1.cs"), fixed1),
            (Path.Combine(folder2, "Class2.cs"), fixed2),
            (Path.Combine(folder3, "Class3.cs"), fixed3),
        };

        await RunTestAsync(sources, fixedSources);
    }

    [Fact]
    public async Task FixAll_MultipleProjects()
    {
        var declaredNamespace = "Bar.Baz";

        var folder1 = CreateFolderPath("A", "B", "C");
        var fixedNamespace1 = $"{DefaultNamespace}.A.B.C";

        var folder2 = CreateFolderPath("Second", "Folder", "Path");
        var fixedNamespace2 = $"{DefaultNamespace}.Second.Folder.Path";

        var folder3 = CreateFolderPath("Third", "Folder", "Path");
        var fixedNamespace3 = $"{DefaultNamespace}.Third.Folder.Path";

        var code1 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                public class Class1
                {
                    Class2 C2 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var fixed1 =
            $$"""
            using {{fixedNamespace2}};
            using {{fixedNamespace3}};

            namespace {{fixedNamespace1}}
            {
                public class Class1
                {
                    Class2 C2 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var code2 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class2
                {
                    Class1 C1 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var fixed2 =
            $$"""
            using {{fixedNamespace1}};
            using {{fixedNamespace3}};

            namespace {{fixedNamespace2}}
            {
                class Class2
                {
                    Class1 C1 { get; }
                    Class3 C3 { get; }
                }
            }
            """;

        var code3 =
            $$"""
            namespace [|{{declaredNamespace}}|]
            {
                class Class3
                {
                    Class1 C1 { get; }
                    Class2 C2 { get; }
                }
            }
            """;

        var fixed3 =
            $$"""
            using {{fixedNamespace1}};
            using {{fixedNamespace2}};

            namespace {{fixedNamespace3}}
            {
                class Class3
                {
                    Class1 C1 { get; }
                    Class2 C2 { get; }
                }
            }
            """;

        var project2Directory = "/Project2/";
        var project2folder = Path.Combine(project2Directory, "A", "B", "C");
        var project2EditorConfig = $"""
            is_global=true
            build_property.ProjectDir = {project2Directory}
            build_property.RootNamespace = {DefaultNamespace}
            """;

        var project2Source =
            $$"""
            using {{declaredNamespace}};

            namespace [|Project2.Test|]
            {
                class P
                {
                    Class1 _c1;
                }
            }
            """;

        var project2FixedSource =
            $$"""
            namespace {{fixedNamespace1}}
            {
                class P
                {
                    Class1 _c1;
                }
            }
            """;

        var testState = new VerifyCS.Test
        {
            EditorConfig = EditorConfig,
            CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck | CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInProjectCheck,
            LanguageVersion = LanguageVersion.CSharp10,
            TestState =
            {
                Sources =
                {
                    (Path.Combine(folder1, "Class1.cs"), code1),
                    (Path.Combine(folder2, "Class2.cs"), code2),
                    (Path.Combine(folder3, "Class3.cs"), code3),
                },
                AdditionalProjects =
                {
                    ["Project2"] =
                    {
                        AdditionalProjectReferences = { "TestProject" },
                        Sources = { (Path.Combine(project2folder, "P.cs"), project2Source) },
                        AnalyzerConfigFiles = { (Path.Combine(project2Directory, ".editorconfig"), project2EditorConfig) },
                    },
                },
            },
            FixedState =
            {
                Sources =
                {
                    (Path.Combine(folder1, "Class1.cs"), fixed1),
                    (Path.Combine(folder2, "Class2.cs"), fixed2),
                    (Path.Combine(folder3, "Class3.cs"), fixed3),
                },
                AdditionalProjects =
                {
                    ["Project2"] =
                    {
                        AdditionalProjectReferences = { "TestProject" },
                        Sources = { (Path.Combine(project2folder, "P.cs"), project2FixedSource) },
                        AnalyzerConfigFiles = { (Path.Combine(project2Directory, ".editorconfig"), project2EditorConfig) },
                    }
                }
            }
        };

        testState.SolutionTransforms.Add((solution, projectId) =>
        {
            foreach (var id in solution.ProjectIds)
            {
                var project = solution.GetRequiredProject(id);
                solution = project.WithDefaultNamespace(DefaultNamespace).Solution;
            }
            return solution;
        });

        await testState.RunAsync();
    }

    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/58372")]
    public async Task InvalidProjectName_ChangeNamespace()
    {
        var defaultNamespace = "Invalid-Namespace";
        var editorConfig = $"""
            is_global=true
            build_property.ProjectDir = {Directory}
            build_property.RootNamespace = {defaultNamespace}
            """;

        var folder = CreateFolderPath(["B", "C"]);

        // The project name is invalid so the default namespace is not prepended
        var fixedCode =
            $$"""
            namespace B.C
            {
                class Class1
                {
                }
            }
            """;

        await RunTestAsync(
            "Class1.cs",
            fileContents: """
            namespace [|A.B|]
            {
                class Class1
                {
                }
            }
            """,
            fixedCode: fixedCode,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: defaultNamespace);
    }

    [Fact]
    public async Task InvalidProjectName_DocumentAtRoot_ChangeNamespace()
    {
        var defaultNamespace = "Invalid-Namespace";
        var editorConfig = $"""
            is_global=true
            build_property.ProjectDir = {Directory}
            build_property.RootNamespace = {defaultNamespace}
            """;

        var folder = CreateFolderPath();
        await RunTestAsync(
            "Class1.cs",
            fileContents: $$"""
            namespace Test.Code
            {
                class C { }
            }
            """,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: defaultNamespace);
    }

    [Fact]
    public async Task InvalidRootNamespace_DocumentAtRoot_ChangeNamespace()
    {
        var editorConfig = $"""
            is_global=true
            build_property.ProjectDir = {Directory}
            build_property.RootNamespace = Test.Code # not an editorconfig comment even though it looks like one
            """;

        var folder = CreateFolderPath();
        await RunTestAsync(
            "Class1.cs",
            fileContents: $$"""
            namespace Test.Code
            {
                class C { }
            }
            """,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: "Invalid-Namespace");
    }
}
