/*
 Usage
Add the following attribute to classes for which you want tests run in order:
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
Then decorate your test methods with the Priority attribute.
[Fact, Priority(-10)]
public void FirstTestToRun() { }
[Fact, Priority(0)]
public void SecondTestToRun() { }
[Fact, Priority(10)]
public void ThirdTestToRunA() { }
[Fact, Priority(10)]
public void ThirdTestToRunB() { }
[Fact]
public void TestsWithNoPriorityRunLast() { }
Priorities are evaluated in numeric order (including 0 and negative numbers). If there are multiple tests with the same priority, those tests will be run in alphabetical order.
By default, tests with no explicit Priority attribute are assigned priority int.MaxValue and will be run last. You can change this by setting a DefaultPriority attribute on your test class.
[DefaultPriority(0)]
public class MyTests
{
    [Fact]
    public void SomeTest() { }
    
    [Fact]
    public void SomeOtherTest() { }
    
    [Fact, Priority(10)]
    public void RunMeLast() { }
}
 
 */

using Xunit.Abstractions;
using Xunit.Sdk;

using Tests.CoreTests.Attributes;
using System.Collections.Concurrent;

namespace CoreTests.Core
{
    public class PriorityOrderer : ITestCaseOrderer
    {

        public const string Name = "Tests.CoreTests.PriorityOrderer";
        public const string Assembly = "Tests.CoreTests";

        private static string _priorityAttributeName = typeof(PriorityAttribute).AssemblyQualifiedName;
        private static string _defaultPriorityAttributeName = typeof(DefaultPriorityAttribute).AssemblyQualifiedName;
        private static string _priorityArgumentName = nameof(PriorityAttribute.Priority);

        private static ConcurrentDictionary<string, int> _defaultPriorities = new ConcurrentDictionary<string, int>();

        public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
        {
            var groupedTestCases = new Dictionary<int, List<ITestCase>>();
            var defaultPriorities = new Dictionary<Type, int>();

            foreach (var testCase in testCases)
            {
                var defaultPriority = DefaultPriorityForClass(testCase);
                var priority = PriorityForTest(testCase, defaultPriority);

                if (!groupedTestCases.ContainsKey(priority))
                    groupedTestCases[priority] = new List<ITestCase>();

                groupedTestCases[priority].Add(testCase);
            }

            var orderedKeys = groupedTestCases.Keys.OrderBy(k => k);
            foreach (var list in orderedKeys.Select(priority => groupedTestCases[priority]))
            {
                list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.TestMethod.Method.Name, y.TestMethod.Method.Name));
                foreach (TTestCase testCase in list)
                    yield return testCase;
            }
        }

        private int PriorityForTest(ITestCase testCase, int defaultPriority)
        {
            var priorityAttribute = testCase.TestMethod.Method.GetCustomAttributes(_priorityAttributeName).SingleOrDefault();
            return priorityAttribute?.GetNamedArgument<int>(_priorityArgumentName) ?? defaultPriority;
        }

        private int DefaultPriorityForClass(ITestCase testCase)
        {
            var testClass = testCase.TestMethod.TestClass.Class;
            if (!_defaultPriorities.TryGetValue(testClass.Name, out var result))
            {
                var defaultAttribute = testClass.GetCustomAttributes(_defaultPriorityAttributeName).SingleOrDefault();
                result = defaultAttribute?.GetNamedArgument<int>(_priorityArgumentName) ?? int.MaxValue;
                _defaultPriorities[testClass.Name] = result;
            }

            return result;
        }
    }
}