﻿// 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.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.LanguageServices.DocumentOutline;
using Microsoft.VisualStudio.Text;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;

namespace Roslyn.VisualStudio.CSharp.UnitTests.DocumentOutline
{
    [Trait(Traits.Feature, Traits.Features.DocumentOutline)]
    public class DocumentOutlineTests : DocumentOutlineTestsBase
    {
        private const string TestCode = """
            private class MyClass
            {
                int _x;

                static void Method1(string[] args) {}

                private class MyClass2 {}

                static void Method2(string[] args) {}
            }

            class App
            {
                void Method() {}

                void Z() {}
            }

            interface foo
            {
                void z() {}

                private static int apple = 9, b = 4, c = 6;

                void r() {}
            }
            """;

        public DocumentOutlineTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
        {
        }

        private async Task<(DocumentOutlineTestMocks mocks, (ImmutableArray<DocumentSymbolData> DocumentSymbolData, ITextSnapshot OriginalSnapshot), ImmutableArray<DocumentSymbolDataViewModel> uiItems)> InitializeMocksAndDataModelAndUIItems(string testCode)
        {
            await using var mocks = await CreateMocksAsync(testCode);
            var response = await DocumentOutlineViewModel.DocumentSymbolsRequestAsync(mocks.TextBuffer, mocks.LanguageServiceBroker, mocks.FilePath, CancellationToken.None);
            AssertEx.NotNull(response.Value);

            var model = DocumentOutlineViewModel.CreateDocumentSymbolData(response.Value.response, response.Value.snapshot);
            var uiItems = DocumentOutlineViewModel.GetDocumentSymbolItemViewModels(SortOption.Location, model);
            return (mocks, (model, response.Value.snapshot), uiItems);
        }

        [WpfTheory]
        [CombinatorialData]
        internal async Task TestSortDocumentSymbolData(SortOption sortOption)
        {
            var (_, _, items) = await InitializeMocksAndDataModelAndUIItems(TestCode);
            var sortedSymbols = SortDocumentSymbols(items, sortOption);
            CheckSortedSymbols(sortedSymbols, sortOption);

            static ImmutableArray<DocumentSymbolDataViewModel> SortDocumentSymbols(
                ImmutableArray<DocumentSymbolDataViewModel> documentSymbolData,
                SortOption sortOption)
            {
                using var _ = ArrayBuilder<DocumentSymbolDataViewModel>.GetInstance(out var sortedDocumentSymbols);
                documentSymbolData = Sort(documentSymbolData, sortOption);
                foreach (var documentSymbol in documentSymbolData)
                {
                    var sortedChildren = SortDocumentSymbols(documentSymbol.Children, sortOption);
                    sortedDocumentSymbols.Add(ReplaceChildren(documentSymbol, sortedChildren));
                }

                return sortedDocumentSymbols.ToImmutable();
            }

            static ImmutableArray<DocumentSymbolDataViewModel> Sort(ImmutableArray<DocumentSymbolDataViewModel> items, SortOption sortOption)
                => (ImmutableArray<DocumentSymbolDataViewModel>)DocumentSymbolDataViewModelSorter.Instance.Convert(new object[] { items, sortOption }, typeof(ImmutableArray<DocumentSymbolDataViewModel>), parameter: null, CultureInfo.CurrentCulture);

            static DocumentSymbolDataViewModel ReplaceChildren(DocumentSymbolDataViewModel symbolToUpdate, ImmutableArray<DocumentSymbolDataViewModel> newChildren)
            {
                var data = symbolToUpdate.Data;
                var symbolData = data with { Children = ImmutableArray<DocumentSymbolData>.Empty };
                return new DocumentSymbolDataViewModel(symbolData, newChildren);
            }

            static void CheckSortedSymbols(ImmutableArray<DocumentSymbolDataViewModel> sortedSymbols, SortOption sortOption)
            {
                var actual = sortedSymbols;
                var expected = sortOption switch
                {
                    SortOption.Name => sortedSymbols.OrderBy(static x => x.Data.Name, StringComparer.OrdinalIgnoreCase),
                    SortOption.Location => sortedSymbols.OrderBy(static x => x.Data.RangeSpan.Start),
                    SortOption.Type => sortedSymbols.OrderBy(static x => x.Data.SymbolKind).ThenBy(static x => x.Data.Name),
                    _ => throw new InvalidOperationException($"The value for {nameof(sortOption)} is invalid: {sortOption}")
                };

                Assert.True(expected.SequenceEqual(actual));

                foreach (var symbol in sortedSymbols)
                    CheckSortedSymbols(symbol.Children, sortOption);
            }
        }

        [WpfFact]
        public async Task TestSearchDocumentSymbolData()
        {
            var (_, model, _) = await InitializeMocksAndDataModelAndUIItems(TestCode);

            // Empty search.  Should filter nothing.
            var searchedSymbols = DocumentOutlineViewModel.SearchDocumentSymbolData(model.DocumentSymbolData, string.Empty, CancellationToken.None);
            Assert.Equal(3, searchedSymbols.Length);

            // Search for 1 parent only (no children should match)
            searchedSymbols = DocumentOutlineViewModel.SearchDocumentSymbolData(model.DocumentSymbolData, "foo", CancellationToken.None);
            Assert.Equal(1, searchedSymbols.Length);
            Assert.Equal(0, searchedSymbols.Single(symbol => symbol.Name.Equals("foo")).Children.Length);

            // Search for children only (across 2 parents)
            searchedSymbols = DocumentOutlineViewModel.SearchDocumentSymbolData(model.DocumentSymbolData, "Method", CancellationToken.None);
            Assert.Equal(2, searchedSymbols.Length);
            Assert.Equal(2, searchedSymbols.Single(symbol => symbol.Name.Equals("MyClass")).Children.Length);
            Assert.Equal(1, searchedSymbols.Single(symbol => symbol.Name.Equals("App")).Children.Length);

            // Search for a parent and a child (of another parent)
            searchedSymbols = DocumentOutlineViewModel.SearchDocumentSymbolData(model.DocumentSymbolData, "app", CancellationToken.None);
            Assert.Equal(2, searchedSymbols.Length);
            Assert.Equal(0, searchedSymbols.Single(symbol => symbol.Name.Equals("App")).Children.Length);
            Assert.Equal(1, searchedSymbols.Single(symbol => symbol.Name.Equals("foo")).Children.Length);

            // No search results found
            searchedSymbols = DocumentOutlineViewModel.SearchDocumentSymbolData(model.DocumentSymbolData, "xyz", CancellationToken.None);
            Assert.Equal(0, searchedSymbols.Length);
        }

        [WpfFact]
        [WorkItem("https://github.com/dotnet/roslyn/issues/66012")]
        public async Task TestEnumOnSingleLine()
        {
            var (_, _, items) = await InitializeMocksAndDataModelAndUIItems(
                """
                enum Test
                  { a, b }
                """);

            Assert.Collection(
                items,
                item =>
                {
                    Assert.Equal(Glyph.EnumInternal, item.Data.Glyph);
                    Assert.Equal("Test", item.Data.Name);
                    Assert.Collection(
                        item.Children,
                        item =>
                        {
                            Assert.Equal(Glyph.EnumMemberPublic, item.Data.Glyph);
                            Assert.Equal("a", item.Data.Name);
                        },
                        item =>
                        {
                            Assert.Equal(Glyph.EnumMemberPublic, item.Data.Glyph);
                            Assert.Equal("b", item.Data.Name);
                        });
                });
        }

        [WpfFact]
        [WorkItem("https://github.com/dotnet/roslyn/issues/66473")]
        public async Task TestClassOnSingleLine()
        {
            var (_, _, items) = await InitializeMocksAndDataModelAndUIItems(
                """
                abstract class TypeName { public string PropertyName { get; } = "Value"; }
                """);

            Assert.Collection(
                items,
                item =>
                {
                    Assert.Equal(Glyph.ClassInternal, item.Data.Glyph);
                    Assert.Equal("TypeName", item.Data.Name);
                    Assert.Collection(
                        item.Children,
                        item =>
                        {
                            Assert.Equal(Glyph.PropertyPublic, item.Data.Glyph);
                            Assert.Equal("PropertyName", item.Data.Name);
                        });
                });
        }
    }
}
