// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Test.Helpers;

namespace Microsoft.AspNetCore.Components.Test;

public class DependencyInjectionTest
{
    private readonly TestRenderer _renderer;
    private readonly TestServiceProvider _serviceProvider;

    public DependencyInjectionTest()
    {
        _serviceProvider = new TestServiceProvider();
        _renderer = new TestRenderer(_serviceProvider);
    }

    [Fact]
    public void IgnoresPropertiesWithoutInjectAttribute()
    {
        // Arrange/Act
        var component = InstantiateComponent<HasPropertiesWithoutInjectAttribute>();

        // Assert
        Assert.Null(component.SomeProperty);
        Assert.Null(component.PrivatePropertyValue);
    }

    [Fact]
    public void IgnoresStaticProperties()
    {
        // Arrange/Act
        var component = InstantiateComponent<HasStaticProperties>();

        // Assert
        Assert.Null(HasStaticProperties.StaticPropertyWithInject);
        Assert.Null(HasStaticProperties.StaticPropertyWithoutInject);
    }

    [Fact]
    public void ThrowsForInjectablePropertiesWithoutSetter()
    {
        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasGetOnlyPropertyWithInject>();
        });

        Assert.Equal($"Cannot provide a value for property '{nameof(HasInjectableProperty.MyService)}' " +
            $"on type '{typeof(HasGetOnlyPropertyWithInject).FullName}' because the property " +
            $"has no setter.", ex.Message);
    }

    [Fact]
    public void ThrowsIfNoSuchServiceIsRegistered()
    {
        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasInjectableProperty>();
        });

        Assert.Equal($"Cannot provide a value for property '{nameof(HasInjectableProperty.MyService)}' " +
            $"on type '{typeof(HasInjectableProperty).FullName}'. There is no registered service " +
            $"of type '{typeof(IMyService).FullName}'.", ex.Message);
    }

    [Fact]
    public void ThrowsIfNoSuchKeyedServiceIsRegistered()
    {
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, "mismatched-key");

        var ex = Assert.Throws<InvalidOperationException>(() =>
        {
            InstantiateComponent<HasKeyedInjectableProperty>();
        });

        Assert.Equal($"Cannot provide a value for property '{nameof(HasKeyedInjectableProperty.MyService)}' " +
            $"on type '{typeof(HasKeyedInjectableProperty).FullName}'. There is no registered keyed service " +
            $"of type '{typeof(IMyService).FullName}' with key '{HasKeyedInjectableProperty.ServiceKey}'.", ex.Message);
    }

    [Fact]
    public void SetsInjectablePropertyValueIfServiceIsRegistered()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);

        // Act
        var instance = InstantiateComponent<HasInjectableProperty>();

        // Assert
        Assert.Same(serviceInstance, instance.MyService);
    }

    [Fact]
    public void SetsKeyedInjectablePropertyValueIfKeyedServiceIsRegistered()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, HasKeyedInjectableProperty.ServiceKey);

        // Act
        var instance = InstantiateComponent<HasKeyedInjectableProperty>();

        // Assert
        Assert.Same(serviceInstance, instance.MyService);
    }

    [Fact]
    public void HandlesInjectablePropertyScenarios()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        var otherServiceInstance = new MyOtherServiceImplementation();
        var concreteServiceInstance = new MyConcreteService();
        _serviceProvider.AddService<IMyService>(serviceInstance);
        _serviceProvider.AddService<IMyOtherService>(otherServiceInstance);
        _serviceProvider.AddService(concreteServiceInstance);
        _serviceProvider.AddKeyedService<IMyService>(serviceInstance, HasManyInjectableProperties.ServiceKey);

        // Act
        var instance = InstantiateComponent<HasManyInjectableProperties>();

        // Assert
        Assert.Same(serviceInstance, instance.PublicReadWrite);
        Assert.Same(serviceInstance, instance.PublicReadOnly);
        Assert.Same(serviceInstance, instance.PrivateValue);
        Assert.Same(otherServiceInstance, instance.DifferentServiceType);
        Assert.Same(concreteServiceInstance, instance.ConcreteServiceType);
        Assert.Same(serviceInstance, instance.KeyedService);
    }

    [Fact]
    public void SetsInheritedInjectableProperties()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);

        // Act
        var instance = InstantiateComponent<HasInheritedInjectedProperty>();

        // Assert
        Assert.Same(serviceInstance, instance.MyService);
    }

    [Fact]
    public void SetsPrivateInheritedInjectableProperties()
    {
        // Arrange
        var serviceInstance = new MyServiceImplementation();
        _serviceProvider.AddService<IMyService>(serviceInstance);

        // Act
        var instance = InstantiateComponent<HasInheritedPrivateInjectableProperty>();

        // Assert
        Assert.Same(serviceInstance, instance.PrivateMyService);
    }

    private T InstantiateComponent<T>() where T : IComponent
        => (T)_renderer.InstantiateComponent<T>();

    class HasPropertiesWithoutInjectAttribute : TestComponent
    {
        public IMyService SomeProperty { get; set; }
        public IMyService PrivatePropertyValue => PrivateProperty;
        private IMyService PrivateProperty { get; set; }
    }

    class HasStaticProperties : TestComponent
    {
        [Inject] public static IMyService StaticPropertyWithInject { get; set; }
        public static IMyService StaticPropertyWithoutInject { get; set; }
    }

    class HasGetOnlyPropertyWithInject : TestComponent
    {
        [Inject] public IMyService MyService { get; }
    }

    class HasInjectableProperty : TestComponent
    {
        [Inject] public IMyService MyService { get; set; }
    }

    class HasKeyedInjectableProperty : TestComponent
    {
        public const string ServiceKey = "my-service";

        [Inject(Key = ServiceKey)] public IMyService MyService { get; set; }
    }

    class HasPrivateInjectableProperty : TestComponent
    {
        [Inject] private IMyService MyService { get; set; }

        public IMyService PrivateMyService => MyService;
    }

    class HasInheritedPrivateInjectableProperty : HasPrivateInjectableProperty { }

    class HasManyInjectableProperties : TestComponent
    {
        public const string ServiceKey = "my-service";

        [Inject] public IMyService PublicReadWrite { get; set; }
        [Inject] public IMyService PublicReadOnly { get; private set; }
        [Inject] private IMyService Private { get; set; }
        [Inject] public IMyOtherService DifferentServiceType { get; set; }
        [Inject] public MyConcreteService ConcreteServiceType { get; set; }
        [Inject(Key = ServiceKey)] public IMyService KeyedService { get; set; }

        public IMyService PrivateValue => Private;
    }

    class HasInheritedInjectedProperty : HasInjectableProperty { }

    interface IMyService { }
    interface IMyOtherService { }

    class MyServiceImplementation : IMyService { }
    class MyOtherServiceImplementation : IMyOtherService { }
    class MyConcreteService { }

    class TestComponent : IComponent
    {
        // IMPORTANT: The fact that these throw demonstrates that the injection
        // happens before any of the lifecycle methods. If you change these to
        // not throw, then be sure also to add a test to verify that injection
        // occurs before lifecycle methods.

        public void Attach(RenderHandle renderHandle)
            => throw new NotImplementedException();

        public Task SetParametersAsync(ParameterView parameters)
            => throw new NotImplementedException();
    }
}
