﻿// 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.Linq;
using System.Text;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.VisualStudio.Core.Imaging;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Text.Adornments;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Test.Utilities.QuickInfo
{
    public static class ToolTipAssert
    {
        public static void EqualContent(object expected, object? actual)
        {
            try
            {
                Assert.IsType(expected.GetType(), actual);

                if (expected is ContainerElement containerElement)
                {
                    EqualContainerElement(containerElement, (ContainerElement)actual!);
                    return;
                }

                if (expected is ImageElement imageElement)
                {
                    EqualImageElement(imageElement, (ImageElement)actual!);
                    return;
                }

                if (expected is ClassifiedTextElement classifiedTextElement)
                {
                    EqualClassifiedTextElement(classifiedTextElement, (ClassifiedTextElement)actual!);
                    return;
                }

                if (expected is ClassifiedTextRun classifiedTextRun)
                {
                    EqualClassifiedTextRun(classifiedTextRun, (ClassifiedTextRun)actual!);
                    return;
                }

                throw ExceptionUtilities.Unreachable();
            }
            catch (Exception)
            {
                var renderedExpected = ContainerToString(expected);
                var renderedActual = ContainerToString(actual!);
                AssertEx.EqualOrDiff(renderedExpected, renderedActual);

                // This is not expected to be hit, but it will be hit if the difference cannot be detected within the diff
                throw;
            }
        }

        private static void EqualContainerElement(ContainerElement expected, ContainerElement actual)
        {
            Assert.Equal(expected.Style, actual.Style);
            Assert.Equal(expected.Elements.Count(), actual.Elements.Count());
            foreach (var (expectedElement, actualElement) in expected.Elements.Zip(actual.Elements, (expectedElement, actualElement) => (expectedElement, actualElement)))
            {
                EqualContent(expectedElement, actualElement);
            }
        }

        private static void EqualImageElement(ImageElement expected, ImageElement actual)
        {
            Assert.Equal(expected.ImageId.Guid, actual.ImageId.Guid);
            Assert.Equal(expected.ImageId.Id, actual.ImageId.Id);
            Assert.Equal(expected.AutomationName, actual.AutomationName);
        }

        private static void EqualClassifiedTextElement(ClassifiedTextElement expected, ClassifiedTextElement actual)
        {
            Assert.Equal(expected.Runs.Count(), actual.Runs.Count());
            foreach (var (expectedRun, actualRun) in expected.Runs.Zip(actual.Runs, (expectedRun, actualRun) => (expectedRun, actualRun)))
            {
                EqualClassifiedTextRun(expectedRun, actualRun);
            }
        }

        private static void EqualClassifiedTextRun(ClassifiedTextRun expected, ClassifiedTextRun actual)
        {
            Assert.Equal(expected.ClassificationTypeName, actual.ClassificationTypeName);
            Assert.Equal(expected.Text, actual.Text);
            Assert.Equal(expected.Tooltip, actual.Tooltip);
            Assert.Equal(expected.Style, actual.Style);

            if (expected.NavigationAction is null)
            {
                Assert.Equal(expected.NavigationAction, actual.NavigationAction);
            }
            else if (expected.NavigationAction.Target is QuickInfoHyperLink hyperLink)
            {
                Assert.Same(expected.NavigationAction, hyperLink.NavigationAction);
                var actualTarget = Assert.IsType<QuickInfoHyperLink>(actual.NavigationAction.Target);
                Assert.Same(actual.NavigationAction, actualTarget.NavigationAction);
                Assert.Equal(hyperLink, actualTarget);
            }
            else
            {
                // Cannot validate this navigation action
                Assert.NotNull(actual.NavigationAction);
                Assert.IsNotType<QuickInfoHyperLink>(actual.NavigationAction.Target);
            }
        }

        private static string ContainerToString(object element)
        {
            var result = new StringBuilder();
            ContainerToString(element, "", result);
            return result.ToString();
        }

        private static void ContainerToString(object element, string indent, StringBuilder result)
        {
            result.Append($"{indent}New {element.GetType().Name}(");

            if (element is ContainerElement container)
            {
                result.AppendLine();
                indent += "    ";
                result.AppendLine($"{indent}{ContainerStyleToString(container.Style)},");
                var elements = container.Elements.ToArray();
                for (var i = 0; i < elements.Length; i++)
                {
                    ContainerToString(elements[i], indent, result);

                    if (i < elements.Length - 1)
                        result.AppendLine(",");
                    else
                        result.Append(")");
                }

                return;
            }

            if (element is ImageElement image)
            {
                var guid = GetKnownImageGuid(image.ImageId.Guid);
                var id = GetKnownImageId(image.ImageId.Id);
                result.Append($"New {nameof(ImageId)}({guid}, {id}))");
                return;
            }

            if (element is ClassifiedTextElement classifiedTextElement)
            {
                result.AppendLine();
                indent += "    ";
                var runs = classifiedTextElement.Runs.ToArray();
                for (var i = 0; i < runs.Length; i++)
                {
                    ContainerToString(runs[i], indent, result);

                    if (i < runs.Length - 1)
                        result.AppendLine(",");
                    else
                        result.Append(")");
                }

                return;
            }

            if (element is ClassifiedTextRun classifiedTextRun)
            {
                var classification = GetKnownClassification(classifiedTextRun.ClassificationTypeName);
                result.Append($"{classification}, \"{classifiedTextRun.Text.Replace("\"", "\"\"")}\"");
                if (classifiedTextRun.NavigationAction is object || !string.IsNullOrEmpty(classifiedTextRun.Tooltip))
                {
                    var tooltip = classifiedTextRun.Tooltip is object ? $"\"{classifiedTextRun.Tooltip.Replace("\"", "\"\"")}\"" : "Nothing";
                    if (classifiedTextRun.NavigationAction?.Target is QuickInfoHyperLink hyperLink)
                    {
                        result.Append($", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri(\"{hyperLink.Uri}\", UriKind.Absolute))");
                    }
                    else
                    {
                        result.Append(", navigationAction:=Sub() Return");
                    }

                    result.Append($", {tooltip}");
                }

                if (classifiedTextRun.Style != ClassifiedTextRunStyle.Plain)
                {
                    result.Append($", {TextRunStyleToString(classifiedTextRun.Style)}");
                }

                result.Append(")");
                return;
            }

            throw ExceptionUtilities.Unreachable();
        }

        private static string ContainerStyleToString(ContainerElementStyle style)
        {
            var stringValue = style.ToString();
            return string.Join(" Or ", stringValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(value => $"{nameof(ContainerElementStyle)}.{value}"));
        }

        private static string TextRunStyleToString(ClassifiedTextRunStyle style)
        {
            var stringValue = style.ToString();
            return string.Join(" Or ", stringValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(value => $"{nameof(ClassifiedTextRunStyle)}.{value}"));
        }

        private static string GetKnownClassification(string classification)
        {
            foreach (var field in typeof(ClassificationTypeNames).GetFields())
            {
                if (!field.IsStatic)
                    continue;

                var value = field.GetValue(null) as string;
                if (value == classification)
                    return $"{nameof(ClassificationTypeNames)}.{field.Name}";
            }

            return $"\"{classification}\"";
        }

        private static string GetKnownImageGuid(Guid guid)
        {
            foreach (var field in typeof(KnownImageIds).GetFields())
            {
                if (!field.IsStatic)
                    continue;

                var value = field.GetValue(null) as Guid?;
                if (value == guid)
                    return $"{nameof(KnownImageIds)}.{field.Name}";
            }

            return guid.ToString();
        }

        private static string GetKnownImageId(int id)
        {
            foreach (var field in typeof(KnownImageIds).GetFields())
            {
                if (!field.IsStatic)
                    continue;

                var value = field.GetValue(null) as int?;
                if (value == id)
                    return $"{nameof(KnownImageIds)}.{field.Name}";
            }

            return id.ToString();
        }
    }
}
