﻿// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Formatting.Indentation
{
    [Trait(Traits.Feature, Traits.Features.SmartTokenFormatting)]
    public class SmartTokenFormatterFormatTokenTests : CSharpFormatterTestsBase
    {
        public SmartTokenFormatterFormatTokenTests(ITestOutputHelper output) : base(output)
        {
        }

        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/44423")]
        public async Task EmptyFile1()
        {
            var code = @"{";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 0,
                expectedSpace: 0);
        }

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

            await ExpectException_SmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 0);
        }

        [Fact]
        public async Task Namespace1()
        {
            var code = @"namespace NS
{";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 1,
                expectedSpace: 0);
        }

        [Fact]
        public async Task Namespace2()
        {
            var code = @"namespace NS
}";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 1,
                expectedSpace: 0);
        }

        [Fact]
        public async Task Namespace3()
        {
            var code = @"namespace NS
{
    }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 2,
                expectedSpace: 0);
        }

        [Fact]
        public async Task Class1()
        {
            var code = @"namespace NS
{
    class Class
    {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 3,
                expectedSpace: 4);
        }

        [Fact]
        public async Task Class2()
        {
            var code = @"namespace NS
{
    class Class
    }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 3,
                expectedSpace: 4);
        }

        [Fact]
        public async Task Class3()
        {
            var code = @"namespace NS
{
    class Class
    {
        }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 4,
                expectedSpace: 4);
        }

        [Fact]
        public async Task Method1()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Method2()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Method3()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
            }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Property1()
        {
            var code = @"namespace NS
{
    class Class
    {
        int Goo
            {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Property2()
        {
            var code = @"namespace NS
{
    class Class
    {
        int Goo
        {
            }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Event1()
        {
            var code = @"namespace NS
{
    class Class
    {
        event EventHandler Goo
            {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Event2()
        {
            var code = @"namespace NS
{
    class Class
    {
        event EventHandler Goo
        {
            }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Indexer1()
        {
            var code = @"namespace NS
{
    class Class
    {
        int this[int index]
            {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Indexer2()
        {
            var code = @"namespace NS
{
    class Class
    {
        int this[int index]
        {
            }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 8);
        }

        [Fact]
        public async Task Block1()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
        {";

            await AssertSmartTokenFormatterOpenBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 12);
        }

        [Fact]
        public async Task Block2()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        }
        }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 6,
                expectedSpace: 0);
        }

        [Fact]
        public async Task Block3()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
            {
                }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 7,
                expectedSpace: 12);
        }

        [Fact]
        public async Task Block4()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
                {
        }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 7,
                expectedSpace: 12);
        }

        [Fact]
        public async Task ArrayInitializer1()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
            var a = new []          {
        }";

            var expected = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
            var a = new [] {
        }";

            await AssertSmartTokenFormatterOpenBraceAsync(
                expected,
                code,
                indentationLine: 6);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/537827")]
        public async Task ArrayInitializer3()
        {
            var code = @"namespace NS
{
    class Class
    {
        void Method(int i)
        {
            int[,] arr =
            {
                {1,1}, {2,2}
}
        }";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 9,
                expectedSpace: 12);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/543142")]
        public async Task EnterWithTrailingWhitespace()
        {
            var code = @"class Class
{
    void Method(int i)
    {
        var a = new {
 };
";

            await AssertSmartTokenFormatterCloseBraceAsync(
                code,
                indentationLine: 5,
                expectedSpace: 8);
        }

        [Fact, WorkItem(9216, "DevDiv_Projects/Roslyn")]
        public async Task OpenBraceWithBaseIndentation()
        {
            var markup = @"
class C
{
    void M()
    {
[|#line ""Default.aspx"", 273
        if (true)
$${
        }
#line default
#line hidden|]
    }
}";
            await AssertSmartTokenFormatterOpenBraceWithBaseIndentationAsync(markup, baseIndentation: 7, expectedIndentation: 11);
        }

        [Fact, WorkItem(9216, "DevDiv_Projects/Roslyn")]
        public async Task CloseBraceWithBaseIndentation()
        {
            var markup = @"
class C
{
    void M()
    {
[|#line ""Default.aspx"", 273
        if (true)
        {
$$}
#line default
#line hidden|]
    }
}";
            await AssertSmartTokenFormatterCloseBraceWithBaseIndentation(markup, baseIndentation: 7, expectedIndentation: 11);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/766159")]
        public async Task TestPreprocessor()
        {
            var code = @"
class C
{
    void M()
    {
        #
    }
}";

            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine: 5, ch: '#', useTabs: false);
            Assert.Equal(0, actualIndentation);

            actualIndentation = await GetSmartTokenFormatterIndentationAsync(code.Replace("    ", "\t"), indentationLine: 5, ch: '#', useTabs: true);
            Assert.Equal(0, actualIndentation);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/766159")]
        public async Task TestRegion()
        {
            var code = @"
class C
{
    void M()
    {
#region
    }
}";

            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine: 5, ch: 'n', useTabs: false);
            Assert.Equal(8, actualIndentation);

            actualIndentation = await GetSmartTokenFormatterIndentationAsync(code.Replace("    ", "\t"), indentationLine: 5, ch: 'n', useTabs: true);
            Assert.Equal(8, actualIndentation);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/766159")]
        public async Task TestEndRegion()
        {
            var code = @"
class C
{
    void M()
    {
        #region
#endregion
    }
}";

            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine: 5, ch: 'n', useTabs: false);
            Assert.Equal(8, actualIndentation);

            actualIndentation = await GetSmartTokenFormatterIndentationAsync(code.Replace("    ", "\t"), indentationLine: 5, ch: 'n', useTabs: true);
            Assert.Equal(8, actualIndentation);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/777467")]
        public async Task TestSelect()
        {
            var code = @"
using System;
using System.Linq;

class Program
{
    static IEnumerable<int> Goo()
    {
        return from a in new[] { 1, 2, 3 }
                    select
    }
}
";

            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine: 9, ch: 't', useTabs: false);
            Assert.Equal(15, actualIndentation);

            actualIndentation = await GetSmartTokenFormatterIndentationAsync(code.Replace("    ", "\t"), indentationLine: 9, ch: 't', useTabs: true);
            Assert.Equal(15, actualIndentation);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/777467")]
        public async Task TestWhere()
        {
            var code = @"
using System;
using System.Linq;

class Program
{
    static IEnumerable<int> Goo()
    {
        return from a in new[] { 1, 2, 3 }
                    where
    }
}
";

            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine: 9, ch: 'e', useTabs: false);
            Assert.Equal(15, actualIndentation);

            actualIndentation = await GetSmartTokenFormatterIndentationAsync(code.Replace("    ", "\t"), indentationLine: 9, ch: 'e', useTabs: true);
            Assert.Equal(15, actualIndentation);
        }

        private static async Task AssertSmartTokenFormatterOpenBraceWithBaseIndentationAsync(string markup, int baseIndentation, int expectedIndentation)
        {
            await AssertSmartTokenFormatterOpenBraceWithBaseIndentationAsync(markup, baseIndentation, expectedIndentation, useTabs: false).ConfigureAwait(false);
            await AssertSmartTokenFormatterOpenBraceWithBaseIndentationAsync(markup.Replace("    ", "\t"), baseIndentation, expectedIndentation, useTabs: true).ConfigureAwait(false);
        }

        private static Task AssertSmartTokenFormatterOpenBraceWithBaseIndentationAsync(string markup, int baseIndentation, int expectedIndentation, bool useTabs)
        {
            MarkupTestFile.GetPositionAndSpan(markup,
                out var code, out var position, out TextSpan span);

            return AssertSmartTokenFormatterOpenBraceAsync(
                code,
                SourceText.From(code).Lines.IndexOf(position),
                expectedIndentation,
                useTabs,
                baseIndentation,
                span);
        }

        private static async Task AssertSmartTokenFormatterOpenBraceAsync(
            string code,
            int indentationLine,
            int expectedSpace,
            int? baseIndentation = null,
            TextSpan span = default)
        {
            await AssertSmartTokenFormatterOpenBraceAsync(code, indentationLine, expectedSpace, useTabs: false, baseIndentation, span).ConfigureAwait(false);
            await AssertSmartTokenFormatterOpenBraceAsync(code.Replace("    ", "\t"), indentationLine, expectedSpace, useTabs: true, baseIndentation, span).ConfigureAwait(false);
        }

        private static async Task AssertSmartTokenFormatterOpenBraceAsync(
            string code,
            int indentationLine,
            int expectedSpace,
            bool useTabs,
            int? baseIndentation,
            TextSpan span)
        {
            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine, '{', useTabs, baseIndentation, span);
            Assert.Equal(expectedSpace, actualIndentation);
        }

        private static async Task AssertSmartTokenFormatterOpenBraceAsync(
            string expected,
            string code,
            int indentationLine)
        {
            await AssertSmartTokenFormatterOpenBraceAsync(expected, code, indentationLine, useTabs: false).ConfigureAwait(false);
            await AssertSmartTokenFormatterOpenBraceAsync(expected.Replace("    ", "\t"), code.Replace("    ", "\t"), indentationLine, useTabs: true).ConfigureAwait(false);
        }

        private static async Task AssertSmartTokenFormatterOpenBraceAsync(
            string expected,
            string code,
            int indentationLine,
            bool useTabs)
        {
            // create tree service
            using var workspace = TestWorkspace.CreateCSharp(code);

            var buffer = workspace.Documents.First().GetTextBuffer();

            var actual = await TokenFormatAsync(workspace, buffer, indentationLine, '{', useTabs);
            Assert.Equal(expected, actual);
        }

        private static async Task AssertSmartTokenFormatterCloseBraceWithBaseIndentation(string markup, int baseIndentation, int expectedIndentation)
        {
            await AssertSmartTokenFormatterCloseBraceWithBaseIndentation(markup, baseIndentation, expectedIndentation, useTabs: false).ConfigureAwait(false);
            await AssertSmartTokenFormatterCloseBraceWithBaseIndentation(markup.Replace("    ", "\t"), baseIndentation, expectedIndentation, useTabs: true).ConfigureAwait(false);
        }

        private static Task AssertSmartTokenFormatterCloseBraceWithBaseIndentation(string markup, int baseIndentation, int expectedIndentation, bool useTabs)
        {
            MarkupTestFile.GetPositionAndSpan(markup,
                out var code, out var position, out TextSpan span);

            return AssertSmartTokenFormatterCloseBraceAsync(
                code,
                SourceText.From(code).Lines.IndexOf(position),
                expectedIndentation,
                useTabs,
                baseIndentation,
                span);
        }

        private static async Task AssertSmartTokenFormatterCloseBraceAsync(
            string code,
            int indentationLine,
            int expectedSpace,
            int? baseIndentation = null,
            TextSpan span = default)
        {
            await AssertSmartTokenFormatterCloseBraceAsync(code, indentationLine, expectedSpace, useTabs: false, baseIndentation, span).ConfigureAwait(false);
            await AssertSmartTokenFormatterCloseBraceAsync(code.Replace("    ", "\t"), indentationLine, expectedSpace, useTabs: true, baseIndentation, span).ConfigureAwait(false);
        }

        private static async Task AssertSmartTokenFormatterCloseBraceAsync(
            string code,
            int indentationLine,
            int expectedSpace,
            bool useTabs,
            int? baseIndentation,
            TextSpan span)
        {
            var actualIndentation = await GetSmartTokenFormatterIndentationAsync(code, indentationLine, '}', useTabs, baseIndentation, span);
            Assert.Equal(expectedSpace, actualIndentation);
        }

        private static async Task ExpectException_SmartTokenFormatterCloseBraceAsync(
            string code,
            int indentationLine)
        {
            await ExpectException_SmartTokenFormatterCloseBraceAsync(code, indentationLine, useTabs: false).ConfigureAwait(false);
            await ExpectException_SmartTokenFormatterCloseBraceAsync(code.Replace("    ", "\t"), indentationLine, useTabs: true).ConfigureAwait(false);
        }

        private static async Task ExpectException_SmartTokenFormatterCloseBraceAsync(
            string code,
            int indentationLine,
            bool useTabs)
        {
            Assert.NotNull(await Record.ExceptionAsync(() => GetSmartTokenFormatterIndentationAsync(code, indentationLine, '}', useTabs)));
        }
    }
}
