// 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.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Debugger;
using Microsoft.VisualStudio.Debugger.Clr;
using Microsoft.VisualStudio.Debugger.Evaluation;
using Microsoft.VisualStudio.Debugger.Evaluation.ClrCompilation;
using Xunit;

namespace Microsoft.CodeAnalysis.ExpressionEvaluator
{
    public abstract class ResultProviderTestBase
    {
        internal static string GetDynamicDebugViewEmptyMessage()
        {
            // Value should not be cached since it depends on the current CultureInfo.
            var exceptionType = typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException).Assembly.GetType(
                "Microsoft.CSharp.RuntimeBinder.DynamicMetaObjectProviderDebugView+DynamicDebugViewEmptyException");
            var emptyProperty = exceptionType.GetProperty("Empty");
            return (string)emptyProperty.GetValue(exceptionType.Instantiate());
        }

        internal static DkmClrCustomTypeInfo MakeCustomTypeInfo(params bool[] dynamicFlags)
        {
            if (dynamicFlags == null || dynamicFlags.Length == 0)
            {
                return null;
            }

            var builder = ArrayBuilder<bool>.GetInstance(dynamicFlags.Length);
            builder.AddRange(dynamicFlags);
            var result = CustomTypeInfo.Create(DynamicFlagsCustomTypeInfo.ToBytes(builder), tupleElementNames: null);
            builder.Free();
            return result;
        }

        private readonly DkmInspectionSession _inspectionSession;
        internal readonly DkmInspectionContext DefaultInspectionContext;

        internal ResultProviderTestBase(DkmInspectionSession inspectionSession, DkmInspectionContext defaultInspectionContext)
        {
            _inspectionSession = inspectionSession;
            DefaultInspectionContext = defaultInspectionContext;

            // We never want to swallow Exceptions (generate a non-fatal Watson) when running tests.
            ExpressionEvaluatorFatalError.IsFailFastEnabled = true;
        }

        internal DkmClrValue CreateDkmClrValue(
            object value,
            Type type = null,
            string alias = null,
            DkmEvaluationResultFlags evalFlags = DkmEvaluationResultFlags.None,
            DkmClrValueFlags valueFlags = DkmClrValueFlags.None)
        {
            if (type == null)
            {
                type = value.GetType();
            }
            return new DkmClrValue(
                value,
                DkmClrValue.GetHostObjectValue((TypeImpl)type, value),
                new DkmClrType((TypeImpl)type),
                alias,
                evalFlags,
                valueFlags);
        }

        internal DkmClrValue CreateDkmClrValue(
            object value,
            DkmClrType type,
            string alias = null,
            DkmEvaluationResultFlags evalFlags = DkmEvaluationResultFlags.None,
            DkmClrValueFlags valueFlags = DkmClrValueFlags.None,
            ulong nativeComPointer = 0)
        {
            return new DkmClrValue(
                value,
                DkmClrValue.GetHostObjectValue(type.GetLmrType(), value),
                type,
                alias,
                evalFlags,
                valueFlags,
                nativeComPointer: nativeComPointer);
        }

        internal DkmClrValue CreateErrorValue(
            DkmClrType type,
            string message)
        {
            return new DkmClrValue(
                value: null,
                hostObjectValue: message,
                type: type,
                alias: null,
                evalFlags: DkmEvaluationResultFlags.None,
                valueFlags: DkmClrValueFlags.Error);
        }

        #region Formatter Tests

        internal string FormatNull<T>(bool useHexadecimal = false)
        {
            return FormatValue(null, typeof(T), useHexadecimal);
        }

        internal string FormatValue(object value, bool useHexadecimal = false)
        {
            return FormatValue(value, value.GetType(), useHexadecimal);
        }

        internal string FormatValue(object value, Type type, bool useHexadecimal = false)
        {
            var clrValue = CreateDkmClrValue(value, type);
            return FormatValue(clrValue, useHexadecimal);
        }

        internal string FormatValue(DkmClrValue clrValue, bool useHexadecimal = false)
        {
            var inspectionContext = CreateDkmInspectionContext(_inspectionSession, DkmEvaluationFlags.None, radix: useHexadecimal ? 16u : 10u);
            return clrValue.GetValueString(inspectionContext, Formatter.NoFormatSpecifiers);
        }

        internal bool HasUnderlyingString(object value)
        {
            return HasUnderlyingString(value, value.GetType());
        }

        internal bool HasUnderlyingString(object value, Type type)
        {
            var clrValue = GetValueForUnderlyingString(value, type);
            return clrValue.HasUnderlyingString(DefaultInspectionContext);
        }

        internal string GetUnderlyingString(object value)
        {
            var clrValue = GetValueForUnderlyingString(value, value.GetType());
            return clrValue.GetUnderlyingString(DefaultInspectionContext);
        }

        internal DkmClrValue GetValueForUnderlyingString(object value, Type type)
        {
            return CreateDkmClrValue(
                value,
                type,
                evalFlags: DkmEvaluationResultFlags.RawString);
        }

        #endregion

        #region ResultProvider Tests

        internal DkmInspectionContext CreateDkmInspectionContext(
            DkmEvaluationFlags flags = DkmEvaluationFlags.None,
            uint radix = 10,
            DkmRuntimeInstance runtimeInstance = null)
        {
            return CreateDkmInspectionContext(_inspectionSession, flags, radix, runtimeInstance);
        }

        internal static DkmInspectionContext CreateDkmInspectionContext(
            DkmInspectionSession inspectionSession,
            DkmEvaluationFlags flags,
            uint radix,
            DkmRuntimeInstance runtimeInstance = null)
        {
            return new DkmInspectionContext(inspectionSession, flags, radix, runtimeInstance);
        }

        internal DkmEvaluationResult FormatResult(string name, DkmClrValue value, DkmClrType declaredType = null, DkmInspectionContext inspectionContext = null)
        {
            return FormatResult(name, name, value, declaredType, inspectionContext: inspectionContext);
        }

        internal DkmEvaluationResult FormatResult(string name, string fullName, DkmClrValue value, DkmClrType declaredType = null, DkmClrCustomTypeInfo declaredTypeInfo = null, DkmInspectionContext inspectionContext = null)
        {
            var asyncResult = FormatAsyncResult(name, fullName, value, declaredType, declaredTypeInfo, inspectionContext);
            var exception = asyncResult.Exception;
            if (exception != null)
            {
                ExceptionDispatchInfo.Capture(exception).Throw();
            }
            return asyncResult.Result;
        }

        internal DkmEvaluationAsyncResult FormatAsyncResult(string name, string fullName, DkmClrValue value, DkmClrType declaredType = null, DkmClrCustomTypeInfo declaredTypeInfo = null, DkmInspectionContext inspectionContext = null)
        {
            DkmEvaluationAsyncResult asyncResult = default(DkmEvaluationAsyncResult);
            var workList = new DkmWorkList();
            value.GetResult(
                workList,
                DeclaredType: declaredType ?? value.Type,
                CustomTypeInfo: declaredTypeInfo,
                InspectionContext: inspectionContext ?? DefaultInspectionContext,
                FormatSpecifiers: Formatter.NoFormatSpecifiers,
                ResultName: name,
                ResultFullName: fullName,
                CompletionRoutine: r => asyncResult = r);
            workList.Execute();
            return asyncResult;
        }

        internal DkmEvaluationResult[] GetChildren(DkmEvaluationResult evalResult, DkmInspectionContext inspectionContext = null)
        {
            DkmEvaluationResultEnumContext enumContext;
            var builder = ArrayBuilder<DkmEvaluationResult>.GetInstance();

            // Request 0-3 children.
            int size;
            DkmEvaluationResult[] items;
            for (size = 0; size < 3; size++)
            {
                items = GetChildren(evalResult, size, inspectionContext, out enumContext);
                var totalChildCount = enumContext.Count;
                Assert.InRange(totalChildCount, 0, int.MaxValue);
                var expectedSize = (size < totalChildCount) ? size : totalChildCount;
                Assert.Equal(expectedSize, items.Length);
            }

            // Request items (increasing the size of the request with each iteration).
            size = 1;
            items = GetChildren(evalResult, size, inspectionContext, out enumContext);
            while (items.Length > 0)
            {
                builder.AddRange(items);
                Assert.True(builder.Count <= enumContext.Count);

                int offset = builder.Count;
                // Request 0 items.
                items = GetItems(enumContext, offset, 0);
                Assert.Equal(items.Length, 0);
                // Request >0 items.
                size++;
                items = GetItems(enumContext, offset, size);
            }

            Assert.Equal(builder.Count, enumContext.Count);
            return builder.ToArrayAndFree();
        }

        internal DkmEvaluationResult[] GetChildren(DkmEvaluationResult evalResult, int initialRequestSize, DkmInspectionContext inspectionContext, out DkmEvaluationResultEnumContext enumContext)
        {
            DkmGetChildrenAsyncResult getChildrenResult = default(DkmGetChildrenAsyncResult);
            var workList = new DkmWorkList();
            evalResult.GetChildren(workList, initialRequestSize, inspectionContext ?? DefaultInspectionContext, r => { getChildrenResult = r; });
            workList.Execute();
            var exception = getChildrenResult.Exception;
            if (exception != null)
            {
                ExceptionDispatchInfo.Capture(exception).Throw();
            }
            enumContext = getChildrenResult.EnumContext;
            return getChildrenResult.InitialChildren;
        }

        internal DkmEvaluationResult[] GetItems(DkmEvaluationResultEnumContext enumContext, int startIndex, int count)
        {
            DkmEvaluationEnumAsyncResult getItemsResult = default(DkmEvaluationEnumAsyncResult);
            var workList = new DkmWorkList();
            enumContext.GetItems(workList, startIndex, count, r => { getItemsResult = r; });
            workList.Execute();
            var exception = getItemsResult.Exception;
            if (exception != null)
            {
                ExceptionDispatchInfo.Capture(exception).Throw();
            }
            return getItemsResult.Items;
        }

        private const DkmEvaluationResultCategory UnspecifiedCategory = (DkmEvaluationResultCategory)(-1);
        private const DkmEvaluationResultAccessType UnspecifiedAccessType = (DkmEvaluationResultAccessType)(-1);
        public const string UnspecifiedValue = "<<unspecified value>>";

        internal static DkmEvaluationResult EvalResult(
            string name,
            string value,
            string type,
            string fullName,
            DkmEvaluationResultFlags flags = DkmEvaluationResultFlags.None,
            DkmEvaluationResultCategory category = UnspecifiedCategory,
            DkmEvaluationResultAccessType access = UnspecifiedAccessType,
            string editableValue = null,
            DkmCustomUIVisualizerInfo[] customUIVisualizerInfo = null)
        {
            return DkmSuccessEvaluationResult.Create(
                null,
                null,
                name,
                fullName,
                flags,
                value,
                editableValue,
                type,
                category,
                access,
                default(DkmEvaluationResultStorageType),
                default(DkmEvaluationResultTypeModifierFlags),
                null,
                (customUIVisualizerInfo != null) ? new ReadOnlyCollection<DkmCustomUIVisualizerInfo>(customUIVisualizerInfo) : null,
                null,
                null);
        }

        internal static DkmIntermediateEvaluationResult EvalIntermediateResult(
            string name,
            string fullName,
            string expression,
            DkmLanguage language)
        {
            return DkmIntermediateEvaluationResult.Create(
                InspectionContext: null,
                StackFrame: null,
                Name: name,
                FullName: fullName,
                Expression: expression,
                IntermediateLanguage: language,
                TargetRuntime: null,
                DataItem: null);
        }

        internal static DkmEvaluationResult EvalFailedResult(
            string name,
            string message,
            string type = null,
            string fullName = null,
            DkmEvaluationResultFlags flags = DkmEvaluationResultFlags.None)
        {
            return DkmFailedEvaluationResult.Create(
                null,
                null,
                name,
                fullName,
                message,
                flags,
                type,
                null);
        }

        internal static void Verify(IReadOnlyList<DkmEvaluationResult> actual, params DkmEvaluationResult[] expected)
        {
            try
            {
                int n = actual.Count;
                Assert.Equal(expected.Length, n);
                for (int i = 0; i < n; i++)
                {
                    Verify(actual[i], expected[i]);
                }
            }
            catch
            {
                foreach (var result in actual)
                {
                    Console.WriteLine("{0}, ", ToString(result));
                }
                throw;
            }
        }

        private static string ToString(DkmEvaluationResult result)
        {
            if (result is DkmSuccessEvaluationResult success)
                return ToString(success);

            if (result is DkmIntermediateEvaluationResult intermediate)
                return ToString(intermediate);

            return ToString((DkmFailedEvaluationResult)result);
        }

        private static string ToString(DkmSuccessEvaluationResult result)
        {
            var pooledBuilder = PooledStringBuilder.GetInstance();
            var builder = pooledBuilder.Builder;
            builder.Append("EvalResult(");
            builder.Append(Quote(result.Name));
            builder.Append(", ");
            builder.Append((result.Value == null) ? "null" : Quote(Escape(result.Value)));
            builder.Append(", ");
            builder.Append(Quote(result.Type));
            builder.Append(", ");
            builder.Append((result.FullName != null) ? Quote(Escape(result.FullName)) : "null");
            if (result.Flags != DkmEvaluationResultFlags.None)
            {
                builder.Append(", ");
                builder.Append(FormatEnumValue(result.Flags));
            }
            if (result.Category != DkmEvaluationResultCategory.Other)
            {
                builder.Append(", ");
                builder.Append(FormatEnumValue(result.Category));
            }
            if (result.Access != DkmEvaluationResultAccessType.None)
            {
                builder.Append(", ");
                builder.Append(FormatEnumValue(result.Access));
            }
            if (result.EditableValue != null)
            {
                builder.Append(", ");
                builder.Append(Quote(result.EditableValue));
            }
            builder.Append(")");
            return pooledBuilder.ToStringAndFree();
        }

        private static string ToString(DkmIntermediateEvaluationResult result)
        {
            var pooledBuilder = PooledStringBuilder.GetInstance();
            var builder = pooledBuilder.Builder;
            builder.Append("IntermediateEvalResult(");
            builder.Append(Quote(result.Name));
            builder.Append(", ");
            builder.Append(Quote(result.Expression));
            if (result.Type != null)
            {
                builder.Append(", ");
                builder.Append(Quote(result.Type));
            }
            if (result.FullName != null)
            {
                builder.Append(", ");
                builder.Append(Quote(Escape(result.FullName)));
            }
            if (result.Flags != DkmEvaluationResultFlags.None)
            {
                builder.Append(", ");
                builder.Append(FormatEnumValue(result.Flags));
            }
            builder.Append(")");
            return pooledBuilder.ToStringAndFree();
        }

        private static string ToString(DkmFailedEvaluationResult result)
        {
            var pooledBuilder = PooledStringBuilder.GetInstance();
            var builder = pooledBuilder.Builder;
            builder.Append("EvalFailedResult(");
            builder.Append(Quote(result.Name));
            builder.Append(", ");
            builder.Append(Quote(result.ErrorMessage));
            if (result.Type != null)
            {
                builder.Append(", ");
                builder.Append(Quote(result.Type));
            }
            if (result.FullName != null)
            {
                builder.Append(", ");
                builder.Append(Quote(Escape(result.FullName)));
            }
            if (result.Flags != DkmEvaluationResultFlags.None)
            {
                builder.Append(", ");
                builder.Append(FormatEnumValue(result.Flags));
            }
            builder.Append(")");
            return pooledBuilder.ToStringAndFree();
        }

        private static string Escape(string str)
        {
            return str.Replace("\"", "\\\"");
        }

        private static string Quote(string str)
        {
            return '"' + str + '"';
        }

        private static string FormatEnumValue(Enum e)
        {
            var parts = e.ToString().Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
            var enumTypeName = e.GetType().Name;
            return string.Join(" | ", parts.Select(p => enumTypeName + "." + p));
        }

        internal static void Verify(DkmEvaluationResult actual, DkmEvaluationResult expected)
        {
            Assert.Equal(expected.Name, actual.Name);
            Assert.Equal(expected.FullName, actual.FullName);
            var expectedSuccess = expected as DkmSuccessEvaluationResult;
            var expectedIntermediate = expected as DkmIntermediateEvaluationResult;
            if (expectedSuccess != null)
            {
                var actualSuccess = (DkmSuccessEvaluationResult)actual;

                Assert.NotEqual(UnspecifiedValue, actualSuccess.Value);
                if (expectedSuccess.Value != UnspecifiedValue)
                {
                    Assert.Equal(expectedSuccess.Value, actualSuccess.Value);
                }

                Assert.Equal(expectedSuccess.Type, actualSuccess.Type);
                Assert.Equal(expectedSuccess.Flags, actualSuccess.Flags);
                if (expectedSuccess.Category != UnspecifiedCategory)
                {
                    Assert.Equal(expectedSuccess.Category, actualSuccess.Category);
                }
                if (expectedSuccess.Access != UnspecifiedAccessType)
                {
                    Assert.Equal(expectedSuccess.Access, actualSuccess.Access);
                }

                Assert.Equal(expectedSuccess.EditableValue, actualSuccess.EditableValue);
                Assert.True(
                    (expectedSuccess.CustomUIVisualizers == actualSuccess.CustomUIVisualizers) ||
                    (expectedSuccess.CustomUIVisualizers != null && actualSuccess.CustomUIVisualizers != null &&
                    expectedSuccess.CustomUIVisualizers.SequenceEqual(actualSuccess.CustomUIVisualizers, CustomUIVisualizerInfoComparer.Instance)));
            }
            else if (expectedIntermediate != null)
            {
                var actualIntermediate = (DkmIntermediateEvaluationResult)actual;
                Assert.Equal(expectedIntermediate.Expression, actualIntermediate.Expression);
                Assert.Equal(expectedIntermediate.IntermediateLanguage.Id.LanguageId, actualIntermediate.IntermediateLanguage.Id.LanguageId);
                Assert.Equal(expectedIntermediate.IntermediateLanguage.Id.VendorId, actualIntermediate.IntermediateLanguage.Id.VendorId);
            }
            else
            {
                var actualFailed = (DkmFailedEvaluationResult)actual;
                var expectedFailed = (DkmFailedEvaluationResult)expected;
                Assert.Equal(expectedFailed.ErrorMessage, actualFailed.ErrorMessage);
                Assert.Equal(expectedFailed.Type, actualFailed.Type);
                Assert.Equal(expectedFailed.Flags, actualFailed.Flags);
            }
        }

        #endregion

        private sealed class CustomUIVisualizerInfoComparer : IEqualityComparer<DkmCustomUIVisualizerInfo>
        {
            internal static readonly CustomUIVisualizerInfoComparer Instance = new CustomUIVisualizerInfoComparer();

            bool IEqualityComparer<DkmCustomUIVisualizerInfo>.Equals(DkmCustomUIVisualizerInfo x, DkmCustomUIVisualizerInfo y)
            {
                return x == y ||
                    (x != null && y != null &&
                    x.Id == y.Id &&
                    x.MenuName == y.MenuName &&
                    x.Description == y.Description &&
                    x.Metric == y.Metric &&
                    x.UISideVisualizerTypeName == y.UISideVisualizerTypeName &&
                    x.UISideVisualizerAssemblyName == y.UISideVisualizerAssemblyName &&
                    x.UISideVisualizerAssemblyLocation == y.UISideVisualizerAssemblyLocation &&
                    x.DebuggeeSideVisualizerTypeName == y.DebuggeeSideVisualizerTypeName &&
                    x.DebuggeeSideVisualizerAssemblyName == y.DebuggeeSideVisualizerAssemblyName &&
                    x.ExtensionPartId == y.ExtensionPartId);
            }

            int IEqualityComparer<DkmCustomUIVisualizerInfo>.GetHashCode(DkmCustomUIVisualizerInfo obj)
            {
                throw new NotImplementedException();
            }
        }
    }
}
