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

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Debugging
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.DebuggingDataTips)]
    public class DataTipInfoGetterTests
    {
        private static async Task TestAsync(string markup, string expectedText = null)
        {
            await TestSpanGetterAsync(markup, async (document, position, expectedSpan) =>
            {
                var result = await DataTipInfoGetter.GetInfoAsync(document, position, CancellationToken.None);

                Assert.Equal(expectedSpan, result.Span);
                Assert.Equal(expectedText, result.Text);
            });
        }

        private static async Task TestNoDataTipAsync(string markup)
        {
            await TestSpanGetterAsync(markup, async (document, position, expectedSpan) =>
            {
                var result = await DataTipInfoGetter.GetInfoAsync(document, position, CancellationToken.None);
                Assert.True(result.IsDefault);
            });
        }

        private static async Task TestSpanGetterAsync(string markup, Func<Document, int, TextSpan?, Task> continuation)
        {
            using var workspace = TestWorkspace.CreateCSharp(markup);
            var testHostDocument = workspace.Documents.Single();
            var position = testHostDocument.CursorPosition.Value;
            var expectedSpan = testHostDocument.SelectedSpans.Any()
                ? testHostDocument.SelectedSpans.Single()
                : (TextSpan?)null;

            await continuation(
                workspace.CurrentSolution.Projects.First().Documents.First(),
                position,
                expectedSpan);
        }

        [Fact]
        public async Task TestCSharpLanguageDebugInfoGetDataTipSpanAndText()
        {
            await TestAsync("class [|C$$|] { }");
            await TestAsync("struct [|C$$|] { }");
            await TestAsync("interface [|C$$|] { }");
            await TestAsync("enum [|C$$|] { }");
            await TestAsync("delegate void [|C$$|] ();"); // Without the space, that position is actually on the open paren.
        }

        [Fact]
        public async Task Test1()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|Sys$$tem|].Console.WriteLine(args);
  }
}");
        }

        [Fact]
        public async Task Test2()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|System$$.Console|].WriteLine(args);
  }
}");
        }

        [Fact]
        public async Task Test3()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|System.$$Console|].WriteLine(args);
  }
}");
        }

        [Fact]
        public async Task Test4()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|System.Con$$sole|].WriteLine(args);
  }
}");
        }

        [Fact]
        public async Task Test5()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|System.Console.Wri$$teLine|](args);
  }
}");
        }

        [Fact]
        public async Task Test6()
        {
            await TestNoDataTipAsync(
@"class C
{
  void Goo()
  {
    [|System.Console.WriteLine|]$$(args);
  }
}");
        }

        [Fact]
        public async Task Test7()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    System.Console.WriteLine($$[|args|]);
  }
}");
        }

        [Fact]
        public async Task Test8()
        {
            await TestNoDataTipAsync(
@"class C
{
  void Goo()
  {
    [|System.Console.WriteLine|](args$$);
  }
}");
        }

        [Fact]
        public async Task TestVar()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|va$$r|] v = 0;
  }
}", "int");
        }

        [Fact]
        public async Task TestVariableType()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    [|in$$t|] i = 0;
  }
}");
        }

        [Fact]
        public async Task TestVariableIdentifier()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    int [|$$i|] = 0;
  }
}");
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/539910")]
        public async Task TestLiterals()
        {
            await TestAsync(
@"class C
{
  void Goo()
  {
    int i = [|4$$2|];
  }
}", "int");
        }

        [Fact]
        public async Task TestNonExpressions()
        {
            await TestNoDataTipAsync(
@"class C
{
  void Goo()
  {
    int i = 42;
  }$$
}");
        }

        [Fact]
        public async Task TestParameterIdentifier()
        {
            await TestAsync(
@"class C
{
  void Goo(int [|$$i|])
  {
  }
}");
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/942699")]
        public async Task TestCatchIdentifier()
        {
            await TestAsync(
@"class C
{
    void Goo()
    {
        try
        {
        }
        catch (System.Exception [|$$e|])
        {
        }
    }
}");
        }

        [Fact]
        public async Task TestEvent()
        {
            await TestAsync(
@"class C
{
    event System.Action [|$$E|];
}");

            await TestAsync(
@"class C
{
    event System.Action [|$$E|]
    {
        add { }
        remove { }
    }
}");
        }

        [Fact]
        public async Task TestMethod()
        {
            await TestAsync(
@"class C
{
    int [|$$M|]() { }
}");
        }

        [Fact]
        public async Task TestTypeParameter()
        {
            await TestAsync("class C<T, [|$$U|], V> { }");
            await TestAsync(
@"class C
{
    void M<T, [|$$U|]>() { }
}");
        }

        [Fact]
        public async Task UsingAlias()
        {
            await TestAsync(
@"using [|$$S|] = Static;

static class Static
{
}");
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/540921")]
        public async Task TestForEachIdentifier()
        {
            await TestAsync(
@"class C
{
  void Goo(string[] args)
  {
    foreach (string [|$$s|] in args)
    {
    }
  }
}");
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/546328")]
        public async Task TestProperty()
        {
            await TestAsync(
@"namespace ConsoleApplication16
{
    class C
    {
        public int [|$$goo|] { get; private set; } // hover over me
        public C()
        {
            this.goo = 1;
        }
        public int Goo()
        {
            return 2; // breakpoint here
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            new C().Goo();
        }
    }
}
");
        }

        [Fact]
        public async Task TestQueryIdentifier()
        {
            await TestAsync( // From
@"class C
{
    object Goo(string[] args)
    {
        return from [|$$a|] in args select a;
    }
}");
            await TestAsync( // Let
@"class C
{
    object Goo(string[] args)
    {
        return from a in args let [|$$b|] = ""END"" select a + b;
    }
}");
            await TestAsync( // Join
@"class C
{
    object Goo(string[] args)
    {
        return from a in args join [|$$b|] in args on a equals b;
    }
}");
            await TestAsync( // Join Into
@"class C
{
    object Goo(string[] args)
    {
        return from a in args join b in args on a equals b into [|$$c|];
    }
}");
            await TestAsync( // Continuation
@"class C
{
    object Goo(string[] args)
    {
        return from a in args select a into [|$$b|] from c in b select c;
    }
}");
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1077843")]
        public async Task TestConditionalAccessExpression()
        {
            var sourceTemplate = @"
class A
{{
    B B;

    object M()
    {{
        return {0};
    }}
}}

class B
{{
    C C;
}}

class C
{{
    D D;
}}

class D
{{
}}
";

            // One level.
            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|]"));

            // Two levels.
            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|].C"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B.$$C|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me.$$B|]?.C"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B?.$$C|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|]?.C"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B?.$$C|]"));

            // Three levels.
            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|].C.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B.$$C|].D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B.C.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me.$$B|]?.C.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B?.$$C|].D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B?.C.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me.$$B|].C?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B.$$C|]?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B.C?.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|]?.C.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B?.$$C|].D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B?.C.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|].C?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B.$$C|]?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B.C?.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me.$$B|]?.C?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B?.$$C|]?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me.B?.C?.$$D|]"));

            await TestAsync(string.Format(sourceTemplate, "[|Me?.$$B|]?.C?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B?.$$C|]?.D"));
            await TestAsync(string.Format(sourceTemplate, "[|Me?.B?.C?.$$D|]"));
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1077843")]
        public async Task TestConditionalAccessExpression_Trivia()
        {
            var sourceTemplate = @"
class A
{{
    B B;

    object M()
    {{
        return {0};
    }}
}}

class B
{{
    C C;
}}

class C
{{
}}
";

            await TestAsync(string.Format(sourceTemplate, "/*1*/[|$$Me|]/*2*/?./*3*/B/*4*/?./*5*/C/*6*/"));
            await TestAsync(string.Format(sourceTemplate, "/*1*/[|Me/*2*/?./*3*/$$B|]/*4*/?./*5*/C/*6*/"));
            await TestAsync(string.Format(sourceTemplate, "/*1*/[|Me/*2*/?./*3*/B/*4*/?./*5*/$$C|]/*6*/"));
        }
    }
}
