// 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;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests
{
    [UseExportProvider]
    public class TryApplyChangesTests
    {
        private class CustomizedCanApplyWorkspace : Workspace
        {
            private readonly ImmutableArray<ApplyChangesKind> _allowedKinds;
            private readonly Func<ParseOptions, ParseOptions, bool>? _canApplyParseOptions;
            private readonly Func<CompilationOptions, CompilationOptions, bool>? _canApplyCompilationOptions;

            public CustomizedCanApplyWorkspace(params ApplyChangesKind[] allowedKinds)
                : this(allowedKinds, canApplyParseOptions: null)
            {
            }

            public CustomizedCanApplyWorkspace(ApplyChangesKind[] allowedKinds,
                Func<ParseOptions, ParseOptions, bool>? canApplyParseOptions = null,
                Func<CompilationOptions, CompilationOptions, bool>? canApplyCompilationOptions = null)
                : base(Host.Mef.MefHostServices.DefaultHost, workspaceKind: nameof(CustomizedCanApplyWorkspace))
            {
                _allowedKinds = allowedKinds.ToImmutableArray();
                _canApplyParseOptions = canApplyParseOptions;
                _canApplyCompilationOptions = canApplyCompilationOptions;

                // Add a C# project automatically so each test has something to try mutating
                OnProjectAdded(ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestProject", LanguageNames.CSharp));
            }

            public override bool CanApplyChange(ApplyChangesKind feature)
            {
                return _allowedKinds.Contains(feature);
            }

            public override bool CanApplyParseOptionChange(ParseOptions oldOptions, ParseOptions newOptions, Project project)
            {
                if (_canApplyParseOptions != null)
                {
                    return _canApplyParseOptions(oldOptions, newOptions);
                }

                return base.CanApplyParseOptionChange(oldOptions, newOptions, project);
            }

            public override bool CanApplyCompilationOptionChange(CompilationOptions oldOptions, CompilationOptions newOptions, Project project)
            {
                if (_canApplyCompilationOptions != null)
                {
                    return _canApplyCompilationOptions(oldOptions, newOptions);
                }

                return base.CanApplyCompilationOptionChange(oldOptions, newOptions, project);
            }
        }

        [Fact]
        public void TryApplyWorksIfAllowingAnyCompilationOption()
        {
            // If we simply support the main change kind, then any type of change should be supported and we should not get called
            // to the other method
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new[] { ApplyChangesKind.ChangeCompilationOptions },
                canApplyCompilationOptions: (_, __) => throw new Exception("This should not have been called."));

            var project = workspace.CurrentSolution.Projects.Single();

            Assert.True(workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("Test")).Solution));
        }

        [Fact]
        public void TryApplyWorksSpecificChangeIsAllowedForCompilationOption()
        {
            // If we don't support the main change kind, then the other method should be called
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new ApplyChangesKind[] { },
                canApplyCompilationOptions: (_, newCompilationOptions) => newCompilationOptions.MainTypeName == "Test");

            var project = workspace.CurrentSolution.Projects.Single();

            Assert.True(workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("Test")).Solution));
        }

        [Fact]
        public void TryApplyWorksThrowsIfChangeIsDisallowedForCompilationOption()
        {
            // If we don't support the main change kind, then the other method should be called
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new ApplyChangesKind[] { },
                canApplyCompilationOptions: (_, newCompilationOptions) => newCompilationOptions.MainTypeName == "Expected");

            var project = workspace.CurrentSolution.Projects.Single();

            var exception = Assert.Throws<NotSupportedException>(
                () => workspace.TryApplyChanges(project.WithCompilationOptions(project.CompilationOptions!.WithMainTypeName("WrongThing")).Solution));

            Assert.Equal(WorkspacesResources.Changing_compilation_options_is_not_supported, exception.Message);
        }

        [Fact]
        public void TryApplyWorksIfAllowingAnyParseOption()
        {
            // If we simply support the main change kind, then any type of change should be supported and we should not get called
            // to the other method
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new[] { ApplyChangesKind.ChangeParseOptions },
                canApplyParseOptions: (_, __) => throw new Exception("This should not have been called."));

            var project = workspace.CurrentSolution.Projects.Single();

            Assert.True(workspace.TryApplyChanges(
                project.WithParseOptions(
                    project.ParseOptions!.WithFeatures(new[] { KeyValuePairUtil.Create("Feature", "") })).Solution));
        }

        [Fact]
        public void TryApplyWorksSpecificChangeIsAllowedForParseOption()
        {
            // If we don't support the main change kind, then the other method should be called
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new ApplyChangesKind[] { },
                canApplyParseOptions: (_, newParseOptions) => newParseOptions.Features["Feature"] == "ExpectedValue");

            var project = workspace.CurrentSolution.Projects.Single();

            Assert.True(
                workspace.TryApplyChanges(
                    project.WithParseOptions(project.ParseOptions!.WithFeatures(new[] { KeyValuePairUtil.Create("Feature", "ExpectedValue") })).Solution));
        }

        [Fact]
        public void TryApplyWorksThrowsIfChangeIsDisallowedForParseOption()
        {
            // If we don't support the main change kind, then the other method should be called
            using var workspace = new CustomizedCanApplyWorkspace(
                allowedKinds: new ApplyChangesKind[] { },
                canApplyParseOptions: (_, newParseOptions) => newParseOptions.Features["Feature"] == "ExpectedValue");

            var project = workspace.CurrentSolution.Projects.Single();

            var exception = Assert.Throws<NotSupportedException>(
                () => workspace.TryApplyChanges(
                    project.WithParseOptions(project.ParseOptions!.WithFeatures(new[] { KeyValuePairUtil.Create("Feature", "WrongThing") })).Solution));

            Assert.Equal(WorkspacesResources.Changing_parse_options_is_not_supported, exception.Message);
        }

        [Fact]
        public void TryApplyWorksWhenAddingEditorConfigWithoutSupportingCompilationOptionsChanging()
        {
            using var workspace = new CustomizedCanApplyWorkspace(allowedKinds: ApplyChangesKind.AddAnalyzerConfigDocument);

            var project = workspace.CurrentSolution.Projects.Single();

            Assert.True(workspace.TryApplyChanges(project.AddAnalyzerConfigDocument(".editorconfig", SourceText.From("")).Project.Solution));
        }
    }
}
