// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using EntityFrameworkCore.Jet.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EntityFrameworkCore.Jet.FunctionalTests.Query;
using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Collections.Specialized;

// ReSharper disable InconsistentNaming
// ReSharper disable UnusedAutoPropertyAccessor.Local
// ReSharper disable UnusedMember.Local
// ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable VirtualMemberCallInConstructor
namespace EntityFrameworkCore.Jet.FunctionalTests
{
    public class DbContextPoolingTest : IClassFixture<NorthwindQueryJetFixture<NoopModelCustomizer>>
    {
        private static DbContextOptionsBuilder<TContext> ConfigureOptions<TContext>(DbContextOptionsBuilder<TContext> optionsBuilder)
        where TContext : DbContext
        => optionsBuilder
            .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString)
            .EnableServiceProviderCaching(false);

        private static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString)
                .EnableServiceProviderCaching(false);

        private static IServiceProvider BuildServiceProvider<TContextService, TContext>()
            where TContextService : class
            where TContext : DbContext, TContextService
            => new ServiceCollection()
                .AddDbContextPool<TContextService, TContext>(ob => ConfigureOptions(ob))
                .AddDbContextPool<ISecondContext, SecondContext>(ob => ConfigureOptions(ob))
                .BuildServiceProvider(validateScopes: true);

        private static IServiceProvider BuildServiceProvider<TContext>()
            where TContext : DbContext
            => new ServiceCollection()
                .AddDbContextPool<TContext>(ob => ConfigureOptions(ob))
                .AddDbContextPool<SecondContext>(ob => ConfigureOptions(ob))
                .BuildServiceProvider(validateScopes: true);

        private static IServiceProvider BuildServiceProviderWithFactory<TContext>()
            where TContext : DbContext
            => new ServiceCollection()
                .AddPooledDbContextFactory<TContext>(ob => ConfigureOptions(ob))
                .AddDbContextPool<SecondContext>(ob => ConfigureOptions(ob))
                .BuildServiceProvider(validateScopes: true);

        private static IServiceProvider BuildServiceProvider<TContextService, TContext>(int poolSize)
            where TContextService : class
            where TContext : DbContext, TContextService
            => new ServiceCollection()
                .AddDbContextPool<TContextService, TContext>(ob => ConfigureOptions(ob), poolSize)
                .AddDbContextPool<ISecondContext, SecondContext>(ob => ConfigureOptions(ob), poolSize)
                .BuildServiceProvider(validateScopes: true);

        private static IServiceProvider BuildServiceProvider<TContext>(int poolSize)
            where TContext : DbContext
            => new ServiceCollection()
                .AddDbContextPool<TContext>(ob => ConfigureOptions(ob), poolSize)
                .AddDbContextPool<SecondContext>(ob => ConfigureOptions(ob), poolSize)
                .BuildServiceProvider(validateScopes: true);

        private static IServiceProvider BuildServiceProviderWithFactory<TContext>(int poolSize)
            where TContext : DbContext
            => new ServiceCollection()
                .AddPooledDbContextFactory<TContext>(ob => ConfigureOptions(ob), poolSize)
                .AddDbContextPool<SecondContext>(ob => ConfigureOptions(ob), poolSize)
                .BuildServiceProvider(validateScopes: true);

        private static IDbContextFactory<TContext> BuildFactory<TContext>(bool withDependencyInjection)
            where TContext : DbContext
            => withDependencyInjection
                ? BuildServiceProviderWithFactory<TContext>().GetService<IDbContextFactory<TContext>>()
                : new PooledDbContextFactory<TContext>(ConfigureOptions(new DbContextOptionsBuilder<TContext>()).Options);

        private static IDbContextFactory<TContext> BuildFactory<TContext>(bool withDependencyInjection, int poolSize)
            where TContext : DbContext
            => withDependencyInjection
                ? BuildServiceProviderWithFactory<TContext>(poolSize).GetService<IDbContextFactory<TContext>>()
                : new PooledDbContextFactory<TContext>(ConfigureOptions(new DbContextOptionsBuilder<TContext>()).Options, poolSize);

        private interface IPooledContext
        {
        }

        private class DefaultOptionsPooledContext : DbContext
        {
            public DefaultOptionsPooledContext(DbContextOptions options)
                : base(options)
            {
            }
        }

        private class PooledContext : DbContext, IPooledContext
        {
            public static int DisposedCount;
            public static int InstanceCount;

            public static bool ModifyOptions;

            public PooledContext(DbContextOptions options)
                : base(options)
            {
                Interlocked.Increment(ref InstanceCount);

                ChangeTracker.AutoDetectChangesEnabled = false;
                ChangeTracker.LazyLoadingEnabled = false;
                Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
                Database.AutoSavepointsEnabled = false;
                ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never;
                ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;
                SavingChanges += (sender, args) => { };
                SavedChanges += (sender, args) => { };
                SaveChangesFailed += (sender, args) => { };
                ChangeTracker.Tracking += (sender, args) => { };
                ChangeTracker.Tracked += (sender, args) => { };
                ChangeTracker.StateChanging += (sender, args) => { };
                ChangeTracker.StateChanged += (sender, args) => { };
                ChangeTracker.DetectingAllChanges += (sender, args) => { };
                ChangeTracker.DetectedAllChanges += (sender, args) => { };
                ChangeTracker.DetectingEntityChanges += (sender, args) => { };
                ChangeTracker.DetectedEntityChanges += (sender, args) => { };
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                if (ModifyOptions)
                {
                    optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
                }
            }

            public DbSet<Customer> Customers { get; set; }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
                => modelBuilder.Entity<Customer>().ToTable("Customers");

            public override void Dispose()
            {
                base.Dispose();

                Interlocked.Increment(ref DisposedCount);
            }
        }

        private class PooledContextWithOverrides : DbContext, IPooledContext
        {
            public PooledContextWithOverrides(DbContextOptions options)
                : base(options)
            {
            }

            public DbSet<Customer> Customers { get; set; }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
                => modelBuilder.Entity<Customer>().ToTable("Customers");
        }

        public class Customer
        {
            public string CustomerId { get; set; }
            public string CompanyName { get; set; }
            public ObservableCollection<Order> Orders { get; } = new();
        }

        public class Order
        {
            public string OrderId { get; set; }
        }

        private interface ISecondContext
        {
        }

        private class SecondContext : DbContext, ISecondContext
        {
            public DbSet<Blog> Blogs { get; set; }

            public SecondContext(DbContextOptions options)
                : base(options)
            {
            }

            public class Blog
            {
                public int Id { get; set; }
            }
        }

        [ConditionalFact]
        public void Invalid_pool_size()
        {
            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildServiceProvider<PooledContext>(poolSize: 0));

            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildServiceProvider<PooledContext>(poolSize: -1));

            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildServiceProvider<IPooledContext, PooledContext>(poolSize: 0));

            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildServiceProvider<IPooledContext, PooledContext>(poolSize: -1));
        }

        [ConditionalTheory]
        [InlineData(false)]
        [InlineData(true)]
        public void Invalid_pool_size_with_factory(bool withDependencyInjection)
        {
            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildFactory<PooledContext>(withDependencyInjection, poolSize: 0));

            Assert.Throws<ArgumentOutOfRangeException>(
                () => BuildFactory<PooledContext>(withDependencyInjection, poolSize: -1));
        }

        [ConditionalFact]
        public void Validate_pool_size()
        {
            var serviceProvider = BuildServiceProvider<PooledContext>(poolSize: 64);

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                64,
                scope.ServiceProvider
                    .GetRequiredService<PooledContext>()
                    .GetService<IDbContextOptions>()
                    .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalFact]
        public void Validate_pool_size_with_service_interface()
        {
            var serviceProvider = BuildServiceProvider<IPooledContext, PooledContext>(poolSize: 64);

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                64,
                ((DbContext)scope.ServiceProvider
                    .GetRequiredService<IPooledContext>())
                .GetService<IDbContextOptions>()
                .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalFact]
        public void Validate_pool_size_with_factory()
        {
            var serviceProvider = BuildServiceProviderWithFactory<PooledContext>(poolSize: 64);

            using var context = serviceProvider.GetRequiredService<IDbContextFactory<PooledContext>>().CreateDbContext();

            Assert.Equal(
                64,
                context.GetService<IDbContextOptions>()
                    .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalTheory]
        [InlineData(false)]
        [InlineData(true)]
        public void Validate_pool_size_behavior_with_factory(bool withDependencyInjection)
        {
            var factory = BuildFactory<PooledContext>(withDependencyInjection, poolSize: 1);

            var (ctx1, ctx2) = (factory.CreateDbContext(), factory.CreateDbContext());
            ctx1.Dispose();
            ctx2.Dispose();

            using var ctx3 = factory.CreateDbContext();
            using var ctx4 = factory.CreateDbContext();
            Assert.Same(ctx1, ctx3);
            Assert.NotSame(ctx2, ctx4);
        }

        [ConditionalFact]
        public void Validate_pool_size_default()
        {
            var serviceProvider = BuildServiceProvider<PooledContext>();

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                1024,
                scope.ServiceProvider
                    .GetRequiredService<PooledContext>()
                    .GetService<IDbContextOptions>()
                    .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalFact]
        public void Validate_pool_size_with_service_interface_default()
        {
            var serviceProvider = BuildServiceProvider<IPooledContext, PooledContext>();

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                1024,
                ((DbContext)scope.ServiceProvider
                    .GetRequiredService<IPooledContext>())
                .GetService<IDbContextOptions>()
                .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalFact]
        public void Pool_can_get_context_by_concrete_type_even_when_service_interface_is_used()
        {
            var serviceProvider = BuildServiceProvider<IPooledContext, PooledContext>();

            using var scope = serviceProvider.CreateScope();

            Assert.Same(
                scope.ServiceProvider.GetRequiredService<IPooledContext>(),
                scope.ServiceProvider.GetRequiredService<PooledContext>());
        }

        [ConditionalFact]
        public void Validate_pool_size_with_factory_default()
        {
            var serviceProvider = BuildServiceProviderWithFactory<PooledContext>();

            using var context = serviceProvider.GetRequiredService<IDbContextFactory<PooledContext>>().CreateDbContext();

            Assert.Equal(
                1024,
                context.GetService<IDbContextOptions>()
                    .FindExtension<CoreOptionsExtension>()!.MaxPoolSize);
        }

        [ConditionalTheory]
        [InlineData(true)]
        [InlineData(false)]
        public void Options_modified_in_on_configuring(bool useInterface)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var scopedProvider = serviceProvider.CreateScope().ServiceProvider;

            PooledContext.ModifyOptions = true;

            try
            {
                Assert.Throws<InvalidOperationException>(
                    () => useInterface
                        ? scopedProvider.GetService<IPooledContext>()
                        : scopedProvider.GetService<PooledContext>());
            }
            finally
            {
                PooledContext.ModifyOptions = false;
            }
        }

        [ConditionalFact]
        public void Options_modified_in_on_configuring_with_factory()
        {
            var serviceProvider = BuildServiceProviderWithFactory<PooledContext>();
            var scopedProvider = serviceProvider.CreateScope().ServiceProvider;

            PooledContext.ModifyOptions = true;

            try
            {
                var factory = scopedProvider.GetService<IDbContextFactory<PooledContext>>();
                Assert.Throws<InvalidOperationException>(() => factory!.CreateDbContext());
            }
            finally
            {
                PooledContext.ModifyOptions = false;
            }
        }

        private class BadCtorContext : DbContext
        {
        }

        [ConditionalFact]
        public void Throws_when_used_with_parameterless_constructor_context()
        {
            var serviceCollection = new ServiceCollection();

            Assert.Equal(
                CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)),
                Assert.Throws<ArgumentException>(
                    () => serviceCollection.AddDbContextPool<BadCtorContext>(
                        _ => { })).Message);

            Assert.Equal(
                CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)),
                Assert.Throws<ArgumentException>(
                    () => serviceCollection.AddDbContextPool<BadCtorContext>(
                        (_, __) => { })).Message);

            Assert.Equal(
                CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)),
                Assert.Throws<ArgumentException>(
                    () => serviceCollection.AddPooledDbContextFactory<BadCtorContext>(
                        (_, __) => { })).Message);
        }

        [ConditionalFact]
        public void Throws_when_pooled_context_constructor_has_more_than_one_parameter()
        {
            var serviceProvider
                = new ServiceCollection().AddDbContextPool<TwoParameterConstructorContext>(_ => { })
                    .BuildServiceProvider(validateScopes: true);

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                CoreStrings.PoolingContextCtorError(nameof(TwoParameterConstructorContext)),
                Assert.Throws<InvalidOperationException>(() => scope.ServiceProvider.GetService<TwoParameterConstructorContext>()).Message);
        }

        private class TwoParameterConstructorContext : DbContext
        {
            public TwoParameterConstructorContext(DbContextOptions options, string x)
                : base(options)
            {
            }
        }

        [ConditionalFact]
        public void Throws_when_pooled_context_constructor_wrong_parameter()
        {
            var serviceProvider
                = new ServiceCollection().AddDbContextPool<WrongParameterConstructorContext>(_ => { })
                    .BuildServiceProvider(validateScopes: true);

            using var scope = serviceProvider.CreateScope();

            Assert.Equal(
                CoreStrings.PoolingContextCtorError(nameof(WrongParameterConstructorContext)),
                Assert.Throws<InvalidOperationException>(() => scope.ServiceProvider.GetService<WrongParameterConstructorContext>())
                    .Message);
        }

        private class WrongParameterConstructorContext : DbContext
        {
            public WrongParameterConstructorContext(string x)
                : base(new DbContextOptions<WrongParameterConstructorContext>())
            {
            }
        }

        [ConditionalFact]
        public void Does_not_throw_when_parameterless_and_correct_constructor()
        {
            var serviceProvider
                = new ServiceCollection().AddDbContextPool<WithParameterlessConstructorContext>(_ => { })
                    .BuildServiceProvider(validateScopes: true);

            using var scope = serviceProvider.CreateScope();

            var context = scope.ServiceProvider.GetRequiredService<WithParameterlessConstructorContext>();

            Assert.Equal("Options", context.ConstructorUsed);
        }

        [ConditionalFact]
        public void Does_not_throw_when_parameterless_and_correct_constructor_using_factory_pool()
        {
            var serviceProvider
                = new ServiceCollection().AddPooledDbContextFactory<WithParameterlessConstructorContext>(_ => { })
                    .BuildServiceProvider(validateScopes: true);

            using var scope = serviceProvider.CreateScope();

            var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<WithParameterlessConstructorContext>>();
            using var context = factory.CreateDbContext();

            Assert.Equal("Options", context.ConstructorUsed);
        }

        private class WithParameterlessConstructorContext : DbContext
        {
            public string ConstructorUsed { get; }

            public WithParameterlessConstructorContext()
            {
                ConstructorUsed = "Parameterless";
            }

            public WithParameterlessConstructorContext(DbContextOptions<WithParameterlessConstructorContext> options)
                : base(options)
            {
                ConstructorUsed = "Options";
            }
        }

        [ConditionalFact]
        public void Can_pool_non_derived_context()
        {
            var serviceProvider = BuildServiceProvider<DbContext>();

            var serviceScope1 = serviceProvider.CreateScope();
            var context1 = serviceScope1.ServiceProvider.GetService<DbContext>();

            var serviceScope2 = serviceProvider.CreateScope();
            var context2 = serviceScope2.ServiceProvider.GetService<DbContext>();

            Assert.NotSame(context1, context2);

            var id1 = context1.ContextId;
            var id2 = context2.ContextId;

            Assert.NotEqual(default, id1.InstanceId);
            Assert.NotEqual(id1, id2);
            Assert.NotEqual(id1.InstanceId, id2.InstanceId);
            Assert.Equal(1, id1.Lease);
            Assert.Equal(1, id2.Lease);

            serviceScope1.Dispose();
            serviceScope2.Dispose();

            var id1d = context1.ContextId;
            var id2d = context2.ContextId;

            Assert.Equal(id1, id1d);
            Assert.Equal(id1.InstanceId, id1d.InstanceId);
            Assert.Equal(1, id1d.Lease);
            Assert.Equal(1, id2d.Lease);

            var serviceScope3 = serviceProvider.CreateScope();
            var context3 = serviceScope3.ServiceProvider.GetService<DbContext>();

            var id1r = context3.ContextId;

            Assert.Same(context1, context3);
            Assert.Equal(id1.InstanceId, id1r.InstanceId);
            Assert.NotEqual(default, id1r.InstanceId);
            Assert.NotEqual(id1, id1r);
            Assert.Equal(2, id1r.Lease);

            var serviceScope4 = serviceProvider.CreateScope();
            var context4 = serviceScope4.ServiceProvider.GetService<DbContext>();

            var id2r = context4.ContextId;

            Assert.Same(context2, context4);
            Assert.Equal(id2.InstanceId, id2r.InstanceId);
            Assert.NotEqual(default, id2r.InstanceId);
            Assert.NotEqual(id2, id2r);
            Assert.Equal(2, id2r.Lease);
        }

        [ConditionalFact]
        public void ContextIds_make_sense_when_not_pooling()
        {
            var serviceProvider = new ServiceCollection()
                .AddDbContext<DbContext>(
                    ob
                        => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory)
                            .EnableServiceProviderCaching(false))
                .BuildServiceProvider();

            var serviceScope1 = serviceProvider.CreateScope();
            var context1 = serviceScope1.ServiceProvider.GetService<DbContext>();

            var serviceScope2 = serviceProvider.CreateScope();
            var context2 = serviceScope2.ServiceProvider.GetService<DbContext>();

            Assert.NotSame(context1, context2);

            var id1 = context1.ContextId;
            var id2 = context2.ContextId;

            Assert.NotEqual(default, id1.InstanceId);
            Assert.NotEqual(default, id2.InstanceId);

            Assert.NotEqual(id1, id2);
            Assert.Equal(0, id1.Lease);
            Assert.Equal(0, id2.Lease);

            serviceScope1.Dispose();
            serviceScope2.Dispose();

            var id1d = context1.ContextId;
            var id2d = context2.ContextId;

            Assert.Equal(id1.InstanceId, id1d.InstanceId);
            Assert.Equal(id2.InstanceId, id2d.InstanceId);
            Assert.Equal(0, id1d.Lease);
            Assert.Equal(0, id2d.Lease);

            var serviceScope3 = serviceProvider.CreateScope();
            var context3 = serviceScope3.ServiceProvider.GetService<DbContext>();

            var id1r = context3.ContextId;

            Assert.NotSame(context1, context3);
            Assert.NotEqual(default, id1r.InstanceId);
            Assert.NotEqual(id1.InstanceId, id1r.InstanceId);
            Assert.NotEqual(id1, id1r);
            Assert.Equal(0, id1r.Lease);

            var serviceScope4 = serviceProvider.CreateScope();
            var context4 = serviceScope4.ServiceProvider.GetService<DbContext>();

            var id2r = context4.ContextId;

            Assert.NotSame(context2, context4);
            Assert.NotEqual(default, id2r.InstanceId);
            Assert.NotEqual(id2.InstanceId, id2r.InstanceId);
            Assert.NotEqual(id2, id2r);
            Assert.Equal(0, id2r.Lease);
        }

        [ConditionalTheory]
        [InlineData(true)]
        [InlineData(false)]
        public void Contexts_are_pooled(bool useInterface)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope1 = serviceProvider.CreateScope();
            var scopedProvider1 = serviceScope1.ServiceProvider;

            var context1 = useInterface
                ? scopedProvider1.GetService<IPooledContext>()
                : scopedProvider1.GetService<PooledContext>();

            var secondContext1 = useInterface
                ? scopedProvider1.GetService<ISecondContext>()
                : scopedProvider1.GetService<SecondContext>();

            var serviceScope2 = serviceProvider.CreateScope();
            var scopedProvider2 = serviceScope2.ServiceProvider;

            var context2 = useInterface
                ? scopedProvider2.GetService<IPooledContext>()
                : scopedProvider2.GetService<PooledContext>();

            var secondContext2 = useInterface
                ? scopedProvider2.GetService<ISecondContext>()
                : scopedProvider2.GetService<SecondContext>();

            Assert.NotSame(context1, context2);
            Assert.NotSame(secondContext1, secondContext2);

            serviceScope1.Dispose();
            serviceScope2.Dispose();

            var serviceScope3 = serviceProvider.CreateScope();
            var scopedProvider3 = serviceScope3.ServiceProvider;

            var context3 = useInterface
                ? scopedProvider3.GetService<IPooledContext>()
                : scopedProvider3.GetService<PooledContext>();

            var secondContext3 = useInterface
                ? scopedProvider3.GetService<ISecondContext>()
                : scopedProvider3.GetService<SecondContext>();

            Assert.Same(context1, context3);
            Assert.Same(secondContext1, secondContext3);

            var serviceScope4 = serviceProvider.CreateScope();
            var scopedProvider4 = serviceScope4.ServiceProvider;

            var context4 = useInterface
                ? scopedProvider4.GetService<IPooledContext>()
                : scopedProvider4.GetService<PooledContext>();

            var secondContext4 = useInterface
                ? scopedProvider4.GetService<ISecondContext>()
                : scopedProvider4.GetService<SecondContext>();

            Assert.Same(context2, context4);
            Assert.Same(secondContext2, secondContext4);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Context_configuration_is_reset(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context1 = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            Assert.Null(context1!.Database.GetCommandTimeout());

            var set = context1.Customers;
            var localView = set.Local;
            localView.PropertyChanged += LocalView_OnPropertyChanged;
            localView.PropertyChanging += LocalView_OnPropertyChanging;
            localView.CollectionChanged += LocalView_OnCollectionChanged;
            var customer1 = new Customer { CustomerId = "C" };
            context1.Customers.Attach(customer1);
            Assert.Equal(1, localView.Count);
            Assert.Same(customer1, localView.ToBindingList().Single());
            Assert.Same(customer1, localView.ToObservableCollection().Single());
            Assert.True(_localView_OnPropertyChanging);
            Assert.True(_localView_OnPropertyChanged);
            Assert.True(_localView_OnCollectionChanged);

            context1.ChangeTracker.AutoDetectChangesEnabled = true;
            context1.ChangeTracker.LazyLoadingEnabled = true;
            context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate;
            context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate;
            context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
            context1.Database.AutoSavepointsEnabled = true;
            context1.Database.SetCommandTimeout(1);
            context1.ChangeTracker.Tracking += ChangeTracker_OnTracking;
            context1.ChangeTracker.Tracked += ChangeTracker_OnTracked;
            context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging;
            context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged;
            context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges;
            context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges;
            context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges;
            context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges;
            context1.SavingChanges += Context_OnSavingChanges;
            context1.SavedChanges += Context_OnSavedChanges;
            context1.SaveChangesFailed += Context_OnSaveChangesFailed;

            _localView_OnPropertyChanging = false;
            _localView_OnPropertyChanged = false;
            _localView_OnCollectionChanged = false;

            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context2 = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            Assert.Same(context1, context2);

            Assert.False(context2!.ChangeTracker.AutoDetectChangesEnabled);
            Assert.False(context2.ChangeTracker.LazyLoadingEnabled);
            Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior);
            Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.CascadeDeleteTiming);
            Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.DeleteOrphansTiming);
            Assert.Equal(AutoTransactionBehavior.Never, context2.Database.AutoTransactionBehavior);
            Assert.False(context2.Database.AutoSavepointsEnabled);
            Assert.Null(context1.Database.GetCommandTimeout());

            Assert.Empty(localView);
            Assert.Empty(localView.ToBindingList());
            Assert.Empty(localView.ToObservableCollection());

            var customer2 = new Customer { CustomerId = "C" };
            context2.Customers.Attach(customer2).State = EntityState.Modified;
            context2.Customers.Attach(customer2).State = EntityState.Unchanged;

            Assert.False(_changeTracker_OnTracking);
            Assert.False(_changeTracker_OnTracked);
            Assert.False(_changeTracker_OnStateChanging);
            Assert.False(_changeTracker_OnStateChanged);

            context2.SaveChanges();

            Assert.False(_changeTracker_OnDetectingAllChanges);
            Assert.False(_changeTracker_OnDetectedAllChanges);
            Assert.False(_changeTracker_OnDetectingEntityChanges);
            Assert.False(_changeTracker_OnDetectedEntityChanges);
            Assert.False(_context_OnSavedChanges);
            Assert.False(_context_OnSavingChanges);
            Assert.False(_context_OnSaveChangesFailed);

            Assert.Same(set, context2!.Customers);
            Assert.Same(localView, context2!.Customers.Local);
            Assert.Equal(1, localView.Count);
            Assert.Same(customer2, localView.ToBindingList().Single());
            Assert.Same(customer2, localView.ToObservableCollection().Single());
            Assert.False(_localView_OnPropertyChanging);
            Assert.False(_localView_OnPropertyChanged);
            Assert.False(_localView_OnCollectionChanged);
        }

        [ConditionalTheory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task Uninitialized_context_configuration_is_reset_properly(bool async)
        {
            var serviceProvider = BuildServiceProvider<SecondContext>();

            var serviceScope = serviceProvider.CreateScope();
            var ctx = serviceScope.ServiceProvider.GetRequiredService<SecondContext>();
            await Dispose(ctx, async);
            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            var ctx2 = serviceScope.ServiceProvider.GetRequiredService<SecondContext>();
            Assert.Same(ctx, ctx2);
            ctx2.Blogs.Add(new SecondContext.Blog());
            await Dispose(ctx2, async);
            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            var ctx3 = serviceScope.ServiceProvider.GetRequiredService<SecondContext>();
            Assert.Same(ctx, ctx3);
            Assert.Empty(ctx3.ChangeTracker.Entries());
            await Dispose(ctx2, async);
            await Dispose(serviceScope, async);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Context_configuration_is_reset_with_factory(bool async, bool withDependencyInjection)
        {
            var factory = BuildFactory<PooledContext>(withDependencyInjection);

            var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
            var set = context1.Customers;

            var localView = set.Local;
            localView.PropertyChanged += LocalView_OnPropertyChanged;
            localView.PropertyChanging += LocalView_OnPropertyChanging;
            localView.CollectionChanged += LocalView_OnCollectionChanged;
            var customer1 = new Customer { CustomerId = "C" };
            context1.Customers.Attach(customer1);
            Assert.Equal(1, localView.Count);
            Assert.Same(customer1, localView.ToBindingList().Single());
            Assert.Same(customer1, localView.ToObservableCollection().Single());
            Assert.True(_localView_OnPropertyChanging);
            Assert.True(_localView_OnPropertyChanged);
            Assert.True(_localView_OnCollectionChanged);

            context1.ChangeTracker.AutoDetectChangesEnabled = true;
            context1.ChangeTracker.LazyLoadingEnabled = true;
            context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate;
            context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate;
            context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
            context1.Database.AutoSavepointsEnabled = true;
            context1.ChangeTracker.Tracking += ChangeTracker_OnTracking;
            context1.ChangeTracker.Tracked += ChangeTracker_OnTracked;
            context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging;
            context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged;
            context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges;
            context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges;
            context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges;
            context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges;
            context1.SavingChanges += Context_OnSavingChanges;
            context1.SavedChanges += Context_OnSavedChanges;
            context1.SaveChangesFailed += Context_OnSaveChangesFailed;

            _localView_OnPropertyChanging = false;
            _localView_OnPropertyChanged = false;
            _localView_OnCollectionChanged = false;

            await Dispose(context1, async);

            var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            Assert.Same(context1, context2);

            Assert.Empty(localView);
            Assert.Empty(localView.ToBindingList());
            Assert.Empty(localView.ToObservableCollection());

            var customer2 = new Customer { CustomerId = "C" };
            context2.Customers.Attach(customer2).State = EntityState.Modified;
            context2.Customers.Attach(customer2).State = EntityState.Unchanged;

            Assert.False(_changeTracker_OnTracking);
            Assert.False(_changeTracker_OnTracked);
            Assert.False(_changeTracker_OnStateChanging);
            Assert.False(_changeTracker_OnStateChanged);

            context2.SaveChanges();

            Assert.False(_changeTracker_OnDetectingAllChanges);
            Assert.False(_changeTracker_OnDetectedAllChanges);
            Assert.False(_changeTracker_OnDetectingEntityChanges);
            Assert.False(_changeTracker_OnDetectedEntityChanges);
            Assert.False(_context_OnSavedChanges);
            Assert.False(_context_OnSavingChanges);
            Assert.False(_context_OnSaveChangesFailed);

            Assert.Same(set, context2!.Customers);
            Assert.Same(localView, context2!.Customers.Local);
            Assert.Equal(1, localView.Count);
            Assert.Same(customer2, localView.ToBindingList().Single());
            Assert.Same(customer2, localView.ToObservableCollection().Single());
            Assert.False(_localView_OnPropertyChanging);
            Assert.False(_localView_OnPropertyChanged);
            Assert.False(_localView_OnCollectionChanged);
        }

        [ConditionalFact]
        public void Change_tracker_can_be_cleared_without_resetting_context_config()
        {
            var context = new PooledContext(
                new DbContextOptionsBuilder().UseJet(
                    JetNorthwindTestStoreFactory.NorthwindConnectionString).Options);

            context.ChangeTracker.AutoDetectChangesEnabled = true;
            context.ChangeTracker.LazyLoadingEnabled = true;
            context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate;
            context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate;
            context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
            context.Database.AutoSavepointsEnabled = true;
            context.ChangeTracker.Tracking += ChangeTracker_OnTracking;
            context.ChangeTracker.Tracked += ChangeTracker_OnTracked;
            context.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging;
            context.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged;
            context.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges;
            context.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges;
            context.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges;
            context.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges;
            context.SavingChanges += Context_OnSavingChanges;
            context.SavedChanges += Context_OnSavedChanges;
            context.SaveChangesFailed += Context_OnSaveChangesFailed;

            context.ChangeTracker.Clear();

            Assert.True(context.ChangeTracker.AutoDetectChangesEnabled);
            Assert.True(context.ChangeTracker.LazyLoadingEnabled);
            Assert.Equal(QueryTrackingBehavior.NoTracking, context.ChangeTracker.QueryTrackingBehavior);
            Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.CascadeDeleteTiming);
            Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.DeleteOrphansTiming);
            Assert.Equal(AutoTransactionBehavior.WhenNeeded, context.Database.AutoTransactionBehavior);
            Assert.True(context.Database.AutoSavepointsEnabled);

            Assert.False(_changeTracker_OnTracking);
            Assert.False(_changeTracker_OnTracked);
            Assert.False(_changeTracker_OnStateChanging);
            Assert.False(_changeTracker_OnStateChanged);
            Assert.False(_changeTracker_OnDetectingAllChanges);
            Assert.False(_changeTracker_OnDetectedAllChanges);
            Assert.False(_changeTracker_OnDetectingEntityChanges);
            Assert.False(_changeTracker_OnDetectedEntityChanges);

            var customer = new Customer { CustomerId = "C" };
            context.Customers.Attach(customer).State = EntityState.Modified;
            context.Customers.Attach(customer).State = EntityState.Unchanged;

            Assert.True(_changeTracker_OnTracking);
            Assert.True(_changeTracker_OnTracked);
            Assert.True(_changeTracker_OnStateChanging);
            Assert.True(_changeTracker_OnStateChanged);
            Assert.False(_changeTracker_OnDetectingAllChanges);
            Assert.False(_changeTracker_OnDetectedAllChanges);
            Assert.False(_changeTracker_OnDetectingEntityChanges);
            Assert.False(_changeTracker_OnDetectedEntityChanges);

            context.SaveChanges();

            Assert.True(_changeTracker_OnDetectingAllChanges);
            Assert.True(_changeTracker_OnDetectedAllChanges);
            Assert.True(_changeTracker_OnDetectingEntityChanges);
            Assert.True(_changeTracker_OnDetectedEntityChanges);
            Assert.True(_context_OnSavedChanges);
            Assert.True(_context_OnSavingChanges);
            Assert.False(_context_OnSaveChangesFailed);
        }

        private void Context_OnSavingChanges(object sender, SavingChangesEventArgs e)
            => _context_OnSavingChanges = true;

        private bool _context_OnSavingChanges;

        private void Context_OnSavedChanges(object sender, SavedChangesEventArgs e)
            => _context_OnSavedChanges = true;

        private bool _context_OnSavedChanges;

        private void Context_OnSaveChangesFailed(object sender, SaveChangesFailedEventArgs e)
            => _context_OnSaveChangesFailed = true;

        private bool _context_OnSaveChangesFailed;

        private bool _localView_OnPropertyChanged;

        private void LocalView_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
            => _localView_OnPropertyChanged = true;

        private bool _localView_OnPropertyChanging;

        private void LocalView_OnPropertyChanging(object sender, PropertyChangingEventArgs e)
            => _localView_OnPropertyChanging = true;

        private bool _localView_OnCollectionChanged;

        private void LocalView_OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            => _localView_OnCollectionChanged = true;

        private bool _changeTracker_OnTracking;

        private void ChangeTracker_OnTracking(object sender, EntityTrackingEventArgs e)
            => _changeTracker_OnTracking = true;

        private bool _changeTracker_OnTracked;

        private void ChangeTracker_OnTracked(object sender, EntityTrackedEventArgs e)
            => _changeTracker_OnTracked = true;

        private bool _changeTracker_OnStateChanging;

        private void ChangeTracker_OnStateChanging(object sender, EntityStateChangingEventArgs e)
            => _changeTracker_OnStateChanging = true;

        private bool _changeTracker_OnStateChanged;

        private void ChangeTracker_OnStateChanged(object sender, EntityStateChangedEventArgs e)
            => _changeTracker_OnStateChanged = true;

        private bool _changeTracker_OnDetectingAllChanges;

        private void ChangeTracker_OnDetectingAllChanges(object sender, DetectChangesEventArgs e)
            => _changeTracker_OnDetectingAllChanges = true;

        private bool _changeTracker_OnDetectedAllChanges;

        private void ChangeTracker_OnDetectedAllChanges(object sender, DetectedChangesEventArgs e)
            => _changeTracker_OnDetectedAllChanges = true;

        private bool _changeTracker_OnDetectingEntityChanges;

        private void ChangeTracker_OnDetectingEntityChanges(object sender, DetectEntityChangesEventArgs e)
            => _changeTracker_OnDetectingEntityChanges = true;

        private bool _changeTracker_OnDetectedEntityChanges;

        private void ChangeTracker_OnDetectedEntityChanges(object sender, DetectedEntityChangesEventArgs e)
            => _changeTracker_OnDetectedEntityChanges = true;

        [ConditionalTheory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task Default_Context_configuration_is_reset(bool async)
        {
            var serviceProvider = BuildServiceProvider<DefaultOptionsPooledContext>();

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context1 = scopedProvider.GetService<DefaultOptionsPooledContext>();

            context1!.ChangeTracker.AutoDetectChangesEnabled = false;
            context1.ChangeTracker.LazyLoadingEnabled = false;
            context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
            context1.Database.AutoSavepointsEnabled = false;
            context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate;
            context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate;

            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context2 = scopedProvider.GetService<DefaultOptionsPooledContext>();

            Assert.Same(context1, context2);

            Assert.True(context2!.ChangeTracker.AutoDetectChangesEnabled);
            Assert.True(context2.ChangeTracker.LazyLoadingEnabled);
            Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior);
            Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming);
            Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming);
            Assert.Equal(AutoTransactionBehavior.WhenNeeded, context2.Database.AutoTransactionBehavior);
            Assert.True(context2.Database.AutoSavepointsEnabled);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Default_Context_configuration_is_reset_with_factory(bool async, bool withDependencyInjection)
        {
            var factory = BuildFactory<DefaultOptionsPooledContext>(withDependencyInjection);

            var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            context1.ChangeTracker.AutoDetectChangesEnabled = false;
            context1.ChangeTracker.LazyLoadingEnabled = false;
            context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
            context1.Database.AutoSavepointsEnabled = false;
            context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate;
            context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate;

            await Dispose(context1, async);

            var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            Assert.Same(context1, context2);

            Assert.True(context2.ChangeTracker.AutoDetectChangesEnabled);
            Assert.True(context2.ChangeTracker.LazyLoadingEnabled);
            Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior);
            Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming);
            Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming);
            Assert.Equal(AutoTransactionBehavior.WhenNeeded, context2.Database.AutoTransactionBehavior);
            Assert.True(context2.Database.AutoSavepointsEnabled);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task State_manager_is_reset(bool useInterface, bool async)
        {
            var weakRef = await Scoper(
                async () =>
                {
                    var serviceProvider = useInterface
                        ? BuildServiceProvider<IPooledContext, PooledContext>()
                        : BuildServiceProvider<PooledContext>();

                    var serviceScope = serviceProvider.CreateScope();
                    var scopedProvider = serviceScope.ServiceProvider;

                    var context1 = useInterface
                        ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                        : scopedProvider.GetService<PooledContext>();

                    var entity = context1.Customers.First(c => c.CustomerId == "ALFKI");

                    Assert.Single(context1.ChangeTracker.Entries());

                    await Dispose(serviceScope, async);

                    serviceScope = serviceProvider.CreateScope();
                    scopedProvider = serviceScope.ServiceProvider;

                    var context2 = useInterface
                        ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                        : scopedProvider.GetService<PooledContext>();

                    Assert.Same(context1, context2);
                    Assert.Empty(context2.ChangeTracker.Entries());

                    return new WeakReference(entity);
                });

            GC.Collect();

            Assert.False(weakRef.IsAlive);
        }

        private static async Task<T> Scoper<T>(Func<Task<T>> getter)
            => await getter();

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task State_manager_is_reset_with_factory(bool async, bool withDependencyInjection)
        {
            var weakRef = await Scoper(
                async () =>
                {
                    var factory = BuildFactory<PooledContext>(withDependencyInjection);

                    var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

                    var entity = context1.Customers.First(c => c.CustomerId == "ALFKI");

                    Assert.Single(context1.ChangeTracker.Entries());

                    await Dispose(context1, async);

                    var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

                    Assert.Same(context1, context2);
                    Assert.Empty(context2.ChangeTracker.Entries());

                    return new WeakReference(entity);
                });

            GC.Collect();

            Assert.False(weakRef.IsAlive);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Pool_disposes_context_when_context_not_pooled(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope1 = serviceProvider.CreateScope();
            var scopedProvider1 = serviceScope1.ServiceProvider;

            var context1 = useInterface
                ? (PooledContext)scopedProvider1.GetService<IPooledContext>()
                : scopedProvider1.GetService<PooledContext>();

            var serviceScope2 = serviceProvider.CreateScope();
            var scopedProvider2 = serviceScope2.ServiceProvider;

            var context2 = useInterface
                ? (PooledContext)scopedProvider2.GetService<IPooledContext>()
                : scopedProvider2.GetService<PooledContext>();

            await Dispose(serviceScope1, async);
            await Dispose(serviceScope2, async);

            Assert.Throws<ObjectDisposedException>(() => context1.Customers.ToList());
            Assert.Throws<ObjectDisposedException>(() => context2.Customers.ToList());
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Pool_disposes_contexts_when_disposed(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            await Dispose(serviceScope, async);

            await Dispose((IDisposable)serviceProvider, async);

            Assert.Throws<ObjectDisposedException>(() => context.Customers.ToList());
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Object_in_pool_is_disposed(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            await Dispose(serviceScope, async);

            Assert.Throws<ObjectDisposedException>(() => context!.Customers.ToList());
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Double_dispose_does_not_enter_pool_twice(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var scope = serviceProvider.CreateScope();
            var lease = scope.ServiceProvider.GetRequiredService<IScopedDbContextLease<PooledContext>>();
            var context = lease.Context;

            await Dispose(scope, async);

            await Dispose(scope, async);

            using var scope1 = serviceProvider.CreateScope();
            var lease1 = scope1.ServiceProvider.GetRequiredService<IScopedDbContextLease<PooledContext>>();

            using var scope2 = serviceProvider.CreateScope();
            var lease2 = scope2.ServiceProvider.GetRequiredService<IScopedDbContextLease<PooledContext>>();

            Assert.Same(context, lease1.Context);
            Assert.NotSame(lease1.Context, lease2.Context);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Double_dispose_with_standalone_lease_does_not_enter_pool_twice(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var pool = serviceProvider.GetRequiredService<IDbContextPool<PooledContext>>();
            var lease = new DbContextLease(pool, standalone: true);
            var context = (PooledContext)lease.Context;
            ((IDbContextPoolable)context).SetLease(lease);

            await Dispose(context, async);

            await Dispose(context, async);

            using var context1 = new DbContextLease(pool, standalone: true).Context;
            using var context2 = new DbContextLease(pool, standalone: true).Context;

            Assert.Same(context, context1);
            Assert.NotSame(context1, context2);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Can_double_dispose_with_factory(bool async, bool withDependencyInjection)
        {
            var factory = BuildFactory<PooledContext>(withDependencyInjection);

            var context = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            context.Customers.Load();

            await Dispose(context, async);

            Assert.Throws<ObjectDisposedException>(() => context.Customers.ToList());

            await Dispose(context, async);

            Assert.Throws<ObjectDisposedException>(() => context.Customers.ToList());
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Provider_services_are_reset(bool useInterface, bool async)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context1 = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            context1!.Database.BeginTransaction();

            Assert.NotNull(context1.Database.CurrentTransaction);

            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context2 = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            Assert.Same(context1, context2);
            Assert.Null(context2!.Database.CurrentTransaction);

            context2.Database.BeginTransaction();

            Assert.NotNull(context2.Database.CurrentTransaction);

            await Dispose(serviceScope, async);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context3 = useInterface
                ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                : scopedProvider.GetService<PooledContext>();

            Assert.Same(context2, context3);
            Assert.Null(context3!.Database.CurrentTransaction);
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Provider_services_are_reset_with_factory(bool async, bool withDependencyInjection)
        {
            var factory = BuildFactory<PooledContext>(withDependencyInjection);

            var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            context1.Database.BeginTransaction();

            Assert.NotNull(context1.Database.CurrentTransaction);

            await Dispose(context1, async);

            var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            Assert.Same(context1, context2);
            Assert.Null(context2.Database.CurrentTransaction);

            context2.Database.BeginTransaction();

            Assert.NotNull(context2.Database.CurrentTransaction);

            await Dispose(context2, async);

            var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

            Assert.Same(context2, context3);
            Assert.Null(context3.Database.CurrentTransaction);
        }

        [ConditionalTheory] // Issue #27308.
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Handle_open_connection_when_returning_to_pool_for_owned_connection(bool async, bool openWithEf)
        {
            var serviceProvider = new ServiceCollection()
                .AddDbContextPool<PooledContext>(
                    ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString)
                        .EnableServiceProviderCaching(false))
                .BuildServiceProvider(validateScopes: true);

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context1 = scopedProvider.GetRequiredService<PooledContext>();
            var connection1 = context1.Database.GetDbConnection();

            if (async)
            {
                if (openWithEf)
                {
                    await context1.Database.OpenConnectionAsync();
                }
                else
                {
                    await connection1.OpenAsync();
                }
            }
            else
            {
                if (openWithEf)
                {
                    context1.Database.OpenConnection();
                }
                else
                {
                    connection1.Open();
                }
            }

            Assert.Equal(System.Data.ConnectionState.Open, connection1.State);

            await Dispose(serviceScope, async);

            Assert.Equal(System.Data.ConnectionState.Closed, connection1.State);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context2 = scopedProvider.GetRequiredService<PooledContext>();
            Assert.Same(context1, context2);

            var connection2 = context2.Database.GetDbConnection();
            Assert.Same(connection1, connection2);

            Assert.Equal(System.Data.ConnectionState.Closed, connection1.State);

            await Dispose(serviceScope, async);
        }

        [ConditionalTheory] // Issue #27308.
        [InlineData(false, false, false)]
        [InlineData(true, false, false)]
        [InlineData(false, true, false)]
        [InlineData(true, true, false)]
        [InlineData(false, false, true)]
        [InlineData(true, false, true)]
        public async Task Handle_open_connection_when_returning_to_pool_for_external_connection(bool async, bool startsOpen, bool openWithEf)
        {
            using var connection = new JetConnection(JetNorthwindTestStoreFactory.NorthwindConnectionString);

            if (startsOpen)
            {
                if (async)
                {
                    await connection.OpenAsync();
                }
                else
                {
                    connection.Open();
                }
            }

            var serviceProvider = new ServiceCollection()
                .AddDbContextPool<PooledContext>(
                    ob => ob.UseJet(connection)
                        .EnableServiceProviderCaching(false))
                .BuildServiceProvider(validateScopes: true);

            var serviceScope = serviceProvider.CreateScope();
            var scopedProvider = serviceScope.ServiceProvider;

            var context1 = scopedProvider.GetRequiredService<PooledContext>();
            Assert.Same(connection, context1.Database.GetDbConnection());

            if (!startsOpen)
            {
                if (async)
                {
                    if (openWithEf)
                    {
                        await context1.Database.OpenConnectionAsync();
                    }
                    else
                    {
                        await connection.OpenAsync();
                    }
                }
                else
                {
                    if (openWithEf)
                    {
                        context1.Database.OpenConnection();
                    }
                    else
                    {
                        connection.Open();
                    }
                }
            }

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            await Dispose(serviceScope, async);

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            serviceScope = serviceProvider.CreateScope();
            scopedProvider = serviceScope.ServiceProvider;

            var context2 = scopedProvider.GetRequiredService<PooledContext>();
            Assert.Same(context1, context2);

            Assert.Same(connection, context2.Database.GetDbConnection());

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            await Dispose(serviceScope, async);
        }

        [ConditionalTheory] // Issue #27308.
        [InlineData(false, false, false)]
        [InlineData(true, false, false)]
        [InlineData(false, true, false)]
        [InlineData(true, true, false)]
        [InlineData(false, false, true)]
        [InlineData(true, false, true)]
        [InlineData(false, true, true)]
        [InlineData(true, true, true)]
        public async Task Handle_open_connection_when_returning_to_pool_for_owned_connection_with_factory(
            bool async,
            bool openWithEf,
            bool withDependencyInjection)
        {
            var options = new DbContextOptionsBuilder<PooledContext>()
                .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString)
                .EnableServiceProviderCaching(false)
                .Options;

            var factory =
                withDependencyInjection
                    ? new ServiceCollection()
                        .AddPooledDbContextFactory<PooledContext>(
                            ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString)
                                .EnableServiceProviderCaching(false))
                        .BuildServiceProvider(validateScopes: true)
                        .GetRequiredService<IDbContextFactory<PooledContext>>()
                    : new PooledDbContextFactory<PooledContext>(options);

            var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
            var connection1 = context1.Database.GetDbConnection();

            if (async)
            {
                if (openWithEf)
                {
                    await context1.Database.OpenConnectionAsync();
                }
                else
                {
                    await connection1.OpenAsync();
                }
            }
            else
            {
                if (openWithEf)
                {
                    context1.Database.OpenConnection();
                }
                else
                {
                    connection1.Open();
                }
            }

            Assert.Equal(System.Data.ConnectionState.Open, connection1.State);

            await Dispose(context1, async);

            Assert.Equal(System.Data.ConnectionState.Closed, connection1.State);

            var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
            Assert.Same(context1, context2);

            var connection2 = context2.Database.GetDbConnection();
            Assert.Same(connection1, connection2);

            Assert.Equal(System.Data.ConnectionState.Closed, connection1.State);

            await Dispose(context2, async);
        }

        [ConditionalTheory] // Issue #27308.
        [InlineData(false, false, false, false)]
        [InlineData(true, false, false, false)]
        [InlineData(false, true, false, false)]
        [InlineData(true, true, false, false)]
        [InlineData(false, false, true, false)]
        [InlineData(true, false, true, false)]
        [InlineData(false, false, false, true)]
        [InlineData(true, false, false, true)]
        [InlineData(false, true, false, true)]
        [InlineData(true, true, false, true)]
        [InlineData(false, false, true, true)]
        [InlineData(true, false, true, true)]
        public async Task Handle_open_connection_when_returning_to_pool_for_external_connection_with_factory(
            bool async,
            bool startsOpen,
            bool openWithEf,
            bool withDependencyInjection)
        {
            using var connection = new JetConnection(JetNorthwindTestStoreFactory.NorthwindConnectionString);

            if (startsOpen)
            {
                if (async)
                {
                    await connection.OpenAsync();
                }
                else
                {
                    connection.Open();
                }
            }

            var options = new DbContextOptionsBuilder<PooledContext>()
                .UseJet(connection)
                .EnableServiceProviderCaching(false)
                .Options;

            var factory =
                withDependencyInjection
                    ? new ServiceCollection()
                        .AddPooledDbContextFactory<PooledContext>(
                            ob => ob.UseJet(connection)
                                .EnableServiceProviderCaching(false))
                        .BuildServiceProvider(validateScopes: true)
                        .GetRequiredService<IDbContextFactory<PooledContext>>()
                    : new PooledDbContextFactory<PooledContext>(options);

            var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
            Assert.Same(connection, context1.Database.GetDbConnection());

            if (!startsOpen)
            {
                if (async)
                {
                    if (openWithEf)
                    {
                        await context1.Database.OpenConnectionAsync();
                    }
                    else
                    {
                        await connection.OpenAsync();
                    }
                }
                else
                {
                    if (openWithEf)
                    {
                        context1.Database.OpenConnection();
                    }
                    else
                    {
                        connection.Open();
                    }
                }
            }

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            await Dispose(context1, async);

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
            Assert.Same(context1, context2);

            Assert.Same(connection, context2.Database.GetDbConnection());

            Assert.Equal(System.Data.ConnectionState.Open, connection.State);

            await Dispose(context2, async);
        }

        [ConditionalTheory]
        [InlineData(true)]
        [InlineData(false)]
        public void Double_dispose_concurrency_test(bool useInterface)
        {
            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            Parallel.For(
                fromInclusive: 0, toExclusive: 32, body: s =>
                {
                    using var scope = serviceProvider.CreateScope();
                    var scopedProvider = scope.ServiceProvider;

                    var context = useInterface
                        ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                        : scopedProvider.GetService<PooledContext>();

                    var _ = context.Customers.ToList();

                    context.Dispose();
                });
        }

        [ConditionalTheory]
        [InlineData(false, false)]
        [InlineData(true, false)]
        [InlineData(false, true)]
        [InlineData(true, true)]
        public async Task Concurrency_test(bool useInterface, bool async)
        {
            PooledContext.InstanceCount = 0;
            PooledContext.DisposedCount = 0;

            var results = WriteResults();

            var serviceProvider = useInterface
                ? BuildServiceProvider<IPooledContext, PooledContext>()
                : BuildServiceProvider<PooledContext>();

            async Task ProcessRequest()
            {
                while (_stopwatch.IsRunning)
                {
                    using var serviceScope = serviceProvider.CreateScope();
                    var scopedProvider = serviceScope.ServiceProvider;

                    var context = useInterface
                        ? (PooledContext)scopedProvider.GetService<IPooledContext>()
                        : scopedProvider.GetService<PooledContext>();

                    await context!.Customers.AsNoTracking().FirstAsync(c => c.CustomerId == "ALFKI");

                    Interlocked.Increment(ref _requests);
                }
            }

            var tasks = new Task[32];

            for (var i = 0; i < 32; i++)
            {
                tasks[i] = ProcessRequest();
            }

            await Task.WhenAll(tasks);
            await results;

            Assert.Equal(_requests, PooledContext.DisposedCount);
            Assert.InRange(PooledContext.InstanceCount, low: 32, high: 64);
        }

        private readonly TimeSpan _duration = TimeSpan.FromSeconds(value: 10);

        private int _stopwatchStarted;

        private readonly Stopwatch _stopwatch = new();

        private long _requests;

        private async Task WriteResults()
        {
            if (Interlocked.Exchange(ref _stopwatchStarted, value: 1) == 0)
            {
                _stopwatch.Start();
            }

            var lastRequests = (long)0;
            var lastElapsed = TimeSpan.Zero;

            while (_stopwatch.IsRunning)
            {
                await Task.Delay(TimeSpan.FromSeconds(value: 1));

                var currentRequests = _requests - lastRequests;
                lastRequests = _requests;

                var elapsed = _stopwatch.Elapsed;
                var currentElapsed = elapsed - lastElapsed;
                lastElapsed = elapsed;

                _testOutputHelper?
                    .WriteLine(
                        $"[{DateTime.Now:HH:mm:ss.fff}] Requests: {_requests}, "
                        + $"RPS: {Math.Round(currentRequests / currentElapsed.TotalSeconds)}");

                if (elapsed > _duration)
                {
                    _testOutputHelper?.WriteLine(message: "");
                    _testOutputHelper?.WriteLine($"Average RPS: {Math.Round(_requests / elapsed.TotalSeconds)}");

                    _stopwatch.Stop();
                }
            }
        }

        [ConditionalTheory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task Concurrency_test2(bool async)
        {
            var factory = BuildFactory<PooledContext>(withDependencyInjection: false);

            await Task.WhenAll(
                Enumerable.Range(0, 10).Select(
                    _ => Task.Run(
                        async () =>
                        {
                            for (var j = 0; j < 1_000_000; j++)
                            {
                                var ctx = factory.CreateDbContext();

                                if (async)
                                {
                                    await ctx.DisposeAsync();
                                }
                                else
                                {
                                    ctx.Dispose();
                                }
                            }
                        })));
        }

        private async Task Dispose(IDisposable disposable, bool async)
        {
            if (async)
            {
                await ((IAsyncDisposable)disposable).DisposeAsync();
            }
            else
            {
                disposable.Dispose();
            }
        }

        private readonly ITestOutputHelper _testOutputHelper = null;

        // ReSharper disable once UnusedParameter.Local
        public DbContextPoolingTest(NorthwindQueryJetFixture<NoopModelCustomizer> fixture, ITestOutputHelper testOutputHelper)
        {
            //_testOutputHelper = testOutputHelper;
        }
    }
}
