using Moq;
using NUnit.Framework;
using Robust.Shared.Analyzers;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;

namespace Robust.Shared.Tests.IoC
{
    /// <remarks>
    /// This fixture CAN NOT be parallelized, because <see cref="IoCManager"/> is a static singleton.
    /// </remarks>
    [TestFixture, TestOf(typeof(IoCManager))]
    internal sealed partial class IoCManager_Test
    {
        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            IoCManager.InitThread();
        }

        [SetUp]
        public void TestSetup()
        {
            // Because IoCManager is a singleton, it must be cleared every
            // test so that each test is isolated.
            IoCManager.Clear();
        }

        [Test]
        public void IoCTestFieldInjection()
        {
            IoCManager.Register<TestFieldInjection, TestFieldInjection>();
            IoCManager.BuildGraph();
            var tester = IoCManager.Resolve<TestFieldInjection>();

            tester.Test();
        }

        [Test]
        public void IoCTestConstructorInjection()
        {
            IoCManager.Register<TestFieldInjection, TestFieldInjection>();
            IoCManager.Register<TestConstructorInjection, TestConstructorInjection>();
            IoCManager.BuildGraph();

            var tester = IoCManager.Resolve<TestConstructorInjection>();

            Assert.That(tester.FieldInjection, Is.Not.Null);
        }

        [Test]
        public void IoCTestBasic()
        {
            IoCManager.Register<TestFieldInjection, TestFieldInjection>();
            IoCManager.BuildGraph();

            Assert.That(IoCManager.Resolve<TestFieldInjection>(), Is.Not.Null);

            Assert.That(IoCManager.ResolveType(typeof(TestFieldInjection)), Is.Not.Null);
        }

        [Test]
        public void IoCRegisterFactory()
        {
            var newInstance = new TestFieldInjection();
            IoCManager.Register<TestFieldInjection, TestFieldInjection>(() => newInstance);

            IoCManager.BuildGraph(); // Actually calls the factory

            var result = IoCManager.Resolve<TestFieldInjection>();
            Assert.That(result, Is.EqualTo(newInstance));
        }

        [Test]
        public void IoCTestOverwrite()
        {
            IoCManager.Register<IIoCTestPriorities, IoCTestPriorities2>();
            IoCManager.Register<IIoCTestPriorities, IoCTestPriorities1>(true);
            IoCManager.BuildGraph();

            Assert.That(IoCManager.Resolve<IIoCTestPriorities>(), Is.TypeOf<IoCTestPriorities1>());
        }

        [Test]
        public void IoCTestConstructorException()
        {
            IoCManager.Register<IConstructorException, ConstructorException>();
            Assert.That(() => IoCManager.BuildGraph(), Throws.InstanceOf<ImplementationConstructorException>().And.InnerException.InstanceOf<TestConstructorExceptionException>());
        }

        [Test]
        public void IoCTestUnregisteredDirectInject()
        {
            Assert.That(()=>IoCManager.InjectDependencies(new TestUnregisteredInjection()), Throws.TypeOf<UnregisteredDependencyException>());
        }

        [Test]
        public void IoCTestResolveTypeThrowsWhenGraphUnbuilt()
        {
            IoCManager.Register<IIoCFailInterface, TestFailImplementation>();
            // we *forgot* to call BuildGraph
            Assert.That(()=>IoCManager.Resolve<IIoCFailInterface>(), Throws.TypeOf<InvalidOperationException>());
        }

        [Test]
        public void IoCTestResolveThrowsUnregisteredType()
        {
            Assert.That(()=>IoCManager.Resolve<IIoCFailInterface>(), Throws.TypeOf<UnregisteredTypeException>());
        }

        [Test]
        public void IoCTestDuplicateResgisterWithoutBuildingGraph()
        {
            IoCManager.Register<IIoCFailInterface, TestFailImplementation>();
            IoCManager.BuildGraph();

            // cannot overwrite an already built implementation.
            Assert.That(()=> IoCManager.Register<IIoCFailInterface, TestFailImplementation>(true), Throws.TypeOf<InvalidOperationException>());
        }

        [Test]
        public void IoCTestDumplicateRegisterThrows()
        {
            IoCManager.Register<IIoCFailInterface, TestFailImplementation>();

            // duplicate registerations will throw an exception.
            Assert.That(() => IoCManager.Register<IIoCFailInterface, TestFailImplementation>(), Throws.TypeOf<InvalidOperationException>());
        }

        [Test]
        public void IoCBasicRegisterInstance()
        {
            var obj = new Mock<IIoCTestPriorities>().Object;

            IoCManager.RegisterInstance<IIoCTestPriorities>(obj);

            IoCManager.BuildGraph();

            Assert.That(IoCManager.Resolve<IIoCTestPriorities>(), Is.EqualTo(obj));
        }

        private sealed partial class DependencyA
        {
            [Dependency] public DependencyB _depB = default!;
        }
        private sealed partial class DependencyB
        {
            [Dependency] public DependencyA _depA = default!;
        }

        [Test]
        public void IoCRegInstancesBeforeBuildGraph()
        {
            var instanceA = new DependencyA();
            IoCManager.RegisterInstance<DependencyA>(instanceA);

            var instanceB = new DependencyB();
            IoCManager.RegisterInstance<DependencyB>(instanceB);

            IoCManager.BuildGraph();

            var resolveA = IoCManager.Resolve<DependencyA>();
            var resolveB = IoCManager.Resolve<DependencyB>();

            Assert.That(instanceA, Is.EqualTo(resolveA));
            Assert.That(instanceB, Is.EqualTo(resolveB));
            Assert.That(resolveA._depB, Is.EqualTo(resolveB));
            Assert.That(resolveB._depA, Is.EqualTo(resolveA));
        }

        [Test]
        public void IoCRegInstanceBeforeBuildGraph()
        {
            IoCManager.Register<DependencyA, DependencyA>();

            var instanceB = new DependencyB();
            IoCManager.RegisterInstance<DependencyB>(instanceB);

            IoCManager.BuildGraph();

            var resolveA = IoCManager.Resolve<DependencyA>();
            var resolveB = IoCManager.Resolve<DependencyB>();

            Assert.That(instanceB, Is.EqualTo(resolveB));
            Assert.That(resolveA._depB, Is.EqualTo(resolveB));
            Assert.That(resolveB._depA, Is.EqualTo(resolveA));
        }

        [Test]
        public void IoCRegInstanceDepBeforeBuildGraph()
        {
            var instanceB = new DependencyB();
            IoCManager.RegisterInstance<DependencyB>(instanceB);

            IoCManager.Register<DependencyA, DependencyA>();

            IoCManager.BuildGraph();

            var resolveA = IoCManager.Resolve<DependencyA>();
            var resolveB = IoCManager.Resolve<DependencyB>();

            Assert.That(instanceB, Is.EqualTo(resolveB));
            Assert.That(resolveA._depB, Is.EqualTo(resolveB));
            Assert.That(resolveB._depA, Is.EqualTo(resolveA));
        }

        [Test]
        public void TestExplicitInjection()
        {
            // This will explicitly cause the DynamicMethod code path to be taken.
            // To ensure that works.
            var test = new ExplicitInjectionTest();
            IoCManager.InjectDependencies(test);
            Assert.That(test.DependencyCollection, Is.Not.Null);
        }
    }

    public interface IIoCFailInterface { }

    public interface IIoCTestPriorities { }

    public sealed class IoCTestPriorities1 : IIoCTestPriorities { }
    public sealed class IoCTestPriorities2 : IIoCTestPriorities { }

    public interface IConstructorException { }

    public sealed class ConstructorException : IConstructorException
    {
        public ConstructorException()
        {
            throw new TestConstructorExceptionException();
        }
    }

    [Virtual]
    public class TestConstructorExceptionException : Exception
    {
    }

    [Virtual]
    public partial class TestFieldInjectionParent
    {
        [Dependency]
#pragma warning disable 649
#pragma warning disable RA0032
        private TestFieldInjection myself = default!;

        [Dependency]
        public TestFieldInjection myotherself = default!;
#pragma warning restore RA0032
#pragma warning restore 649

        public virtual void Test()
        {
            Assert.That(myself, Is.EqualTo(this));
            Assert.That(myotherself, Is.EqualTo(this));
        }
    }

    public sealed partial class TestFieldInjection : TestFieldInjectionParent
    {
#pragma warning disable RA0032 // Duplicate [Dependency] field. I wrote this test 7 years idk if this makes sense.
        [Dependency]
#pragma warning disable 649
        private TestFieldInjection myuniqueself = default!;

        [Dependency]
        public TestFieldInjection mydifferentself = default!;
#pragma warning restore 649
#pragma warning restore RA0032

        public override void Test()
        {
            base.Test();
            Assert.That(myuniqueself, Is.EqualTo(this));
            Assert.That(mydifferentself, Is.EqualTo(this));
        }
    }

    public sealed class TestConstructorInjection
    {
        public TestFieldInjection FieldInjection { get; }

        public TestConstructorInjection(TestFieldInjection fieldInjection)
        {
            FieldInjection = fieldInjection;
        }
    }

    public sealed partial class TestUnregisteredInjection
    {
        [Dependency]
#pragma warning disable 414
        private IIoCFailInterface FailInterface = default!;
#pragma warning restore 414
    }

    public sealed class TestFailImplementation : IIoCFailInterface { }

    public interface ITestDisposeExceptionCaught { }

    public sealed class TestDisposeExceptionCaught : ITestDisposeExceptionCaught, IDisposable
    {
        public void Dispose()
        {
            throw new Exception("UNIT TEST EXCEPTION");
        }
    }

    public sealed partial class ExplicitInjectionTest
    {
        [Dependency] public IDependencyCollection DependencyCollection = default!;
    }
}
