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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities;
using EntityFrameworkCore.Jet.Internal;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.EntityFrameworkCore.ValueGeneration.Internal;
using Xunit;

namespace EntityFrameworkCore.Jet.FunctionalTests;

#nullable disable

public abstract class JetValueGenerationScenariosTestBase
{
    //protected static readonly GeometryFactory GeometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326);

    protected abstract string DatabaseName { get; }

    protected abstract Guid GuidSentinel { get; }
    protected abstract int IntSentinel { get; }
    protected abstract uint UIntSentinel { get; }
    protected abstract IntKey IntKeySentinel { get; }
    //protected abstract ULongKey ULongKeySentinel { get; }
    protected abstract int? NullableIntSentinel { get; }
    protected abstract string StringSentinel { get; }
    protected abstract DateTime DateTimeSentinel { get; }
    protected abstract NeedsConverter NeedsConverterSentinel { get; }
    //protected abstract GeometryCollection GeometryCollectionSentinel { get; }
    protected abstract byte[] TimestampSentinel { get; }

    // Positive cases

    [ConditionalFact]
    public async Task Insert_with_Identity_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns"));

            context.SaveChanges();
        }

        await using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(1, blogs[0].Id);
            Assert.Equal(2, blogs[1].Id);
        }
    }

    public class BlogContextIdentity(string databaseName, Action<ModelBuilder> modelBuilder) : ContextBase(databaseName, modelBuilder);

    [ConditionalFact]
    public async Task Insert_with_default_value_from_sequence()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns"));

            context.SaveChanges();
        }

        await using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(0, blogs[0].Id);
            Assert.Equal(1, blogs[1].Id);
        }

        await using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating))
        {
            context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns"));

            context.SaveChanges();
        }

        await using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(0, blogs[0].Id);
            Assert.Equal(1, blogs[1].Id);
            Assert.Equal(2, blogs[2].Id);
            Assert.Equal(3, blogs[3].Id);
        }
    }

    public class BlogContextDefaultValue(string databaseName, Action<ModelBuilder> modelBuilder) : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .HasSequence("MySequence")
                .StartsAt(0);

            modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .HasDefaultValueSql("next value for MySequence");
        }
    }

    public class BlogContextDefaultValueNoMigrations(string databaseName, Action<ModelBuilder> modelBuilder)
        : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .HasDefaultValue();
        }
    }

    [ConditionalFact]
    public async Task Insert_with_default_string_value_from_sequence()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" },
                new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel))
        {
            var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal("i77", blogs[0].Id);
            Assert.Equal("i78", blogs[1].Id);
        }
    }

    public class BlogContextStringDefaultValue(string databaseName, Action<ModelBuilder> modelBuilder, string stringSentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly string _stringSentinel = stringSentinel;

        public DbSet<BlogWithStringKey> StringyBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .HasSequence("MyStringSequence")
                .StartsAt(77);

            modelBuilder
                .Entity<BlogWithStringKey>()
                .Property(e => e.Id)
                .HasDefaultValueSql("'i' + CAST((NEXT VALUE FOR MyStringSequence) AS VARCHAR(20))")
                .HasSentinel(_stringSentinel);
        }
    }

    public class BlogWithStringKey
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    [ConditionalFact]
    public async Task Insert_with_key_default_value_from_sequence()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns"));

            context.SaveChanges();
        }

        await using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(77, blogs[0].Id);
            Assert.Equal(78, blogs[1].Id);
        }
    }

    public class BlogContextKeyColumnWithDefaultValue(string databaseName, Action<ModelBuilder> modelBuilder)
        : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .HasSequence("MySequence")
                .StartsAt(77);

            modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .HasDefaultValueSql("next value for MySequence")
                .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw);
        }
    }

    [ConditionalFact]
    public async Task Insert_uint_to_Identity_column_using_value_converter()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new BlogWithUIntKey { Id = UIntSentinel, Name = "One Unicorn" },
                new BlogWithUIntKey { Id = UIntSentinel, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel))
        {
            var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal((uint)1, blogs[0].Id);
            Assert.Equal((uint)2, blogs[1].Id);
        }
    }

    public class BlogContextUIntToIdentityUsingValueConverter(string databaseName, Action<ModelBuilder> modelBuilder, uint uintSentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly uint _uintSentinel = uintSentinel;

        public DbSet<BlogWithUIntKey> UnsignedBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<BlogWithUIntKey>()
                .Property(e => e.Id)
                .HasConversion<int>()
                .HasSentinel(_uintSentinel);
        }
    }

    public class BlogWithUIntKey
    {
        public uint Id { get; set; }
        public string Name { get; set; }
    }

    [ConditionalFact]
    public async Task Insert_int_enum_to_Identity_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "One Unicorn" },
                new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel))
        {
            var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(1, (int)blogs[0].Id);
            Assert.Equal(2, (int)blogs[1].Id);
        }
    }

    public class BlogContextIntEnumToIdentity(string databaseName, Action<ModelBuilder> modelBuilder, IntKey sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly IntKey _sentinel = sentinel;

        public DbSet<BlogWithIntEnumKey> EnumBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<BlogWithIntEnumKey>()
                .Property(e => e.Id)
                .ValueGeneratedOnAdd()
                .HasSentinel(_sentinel);
        }
    }

    public class BlogWithIntEnumKey
    {
        public IntKey Id { get; set; }
        public string Name { get; set; }
    }

    public enum IntKey
    {
        Zero,
        One,
        SixSixSeven,
    }

    //Does not support ulong as identity
    /*[ConditionalFact]
    public async Task Insert_ulong_enum_to_Identity_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "One Unicorn" },
                new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel))
        {
            var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(1, (int)blogs[0].Id);
            Assert.Equal(2, (int)blogs[1].Id);
        }
    }

    public class BlogContextULongEnumToIdentity(string databaseName, Action<ModelBuilder> modelBuilder, ULongKey sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly ULongKey _sentinel = sentinel;

        public DbSet<BlogWithULongEnumKey> EnumBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<BlogWithULongEnumKey>()
                .Property(e => e.Id)
                .ValueGeneratedOnAdd()
                .HasSentinel(_sentinel);
        }
    }

    public class BlogWithULongEnumKey
    {
        public ULongKey Id { get; set; }
        public string Name { get; set; }
    }

    public enum ULongKey : ulong
    {
        Zero,
        Sentinel
    }*/

    [ConditionalFact]
    public async Task Insert_string_to_Identity_column_using_value_converter()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" },
                new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel))
        {
            var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal("1", blogs[0].Id);
            Assert.Equal("2", blogs[1].Id);
        }
    }

    public class BlogContextStringToIdentityUsingValueConverter(string databaseName, Action<ModelBuilder> modelBuilder, string sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly string _sentinel = sentinel;

        public DbSet<BlogWithStringKey> StringyBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            Guid guid;
            modelBuilder
                .Entity<BlogWithStringKey>()
                .Property(e => e.Id)
                .HasValueGenerator<TemporaryStringValueGenerator>()
                .HasConversion(
                    v => Guid.TryParse(v, out guid)
                        ? default
                        : int.Parse(v),
                    v => v.ToString())
                .ValueGeneratedOnAdd()
                .HasSentinel(_sentinel);
        }
    }

    [ConditionalFact]
    public async Task Insert_with_explicit_non_default_keys()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new Blog { Id = 66, Name = "One Unicorn" }, new Blog { Id = 67, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(66, blogs[0].Id);
            Assert.Equal(67, blogs[1].Id);
        }
    }

    public class BlogContextNoKeyGeneration(string databaseName, Action<ModelBuilder> modelBuilder)
        : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .ValueGeneratedNever();
        }
    }

    [ConditionalFact]
    public async Task Insert_with_explicit_with_default_keys()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new NullableKeyBlog { Id = 0, Name = "One Unicorn" },
                new NullableKeyBlog { Id = 1, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel))
        {
            var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(0, blogs[0].Id);
            Assert.Equal(1, blogs[1].Id);
        }
    }

    public class BlogContextNoKeyGenerationNullableKey(string databaseName, Action<ModelBuilder> modelBuilder, int? sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly int? _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<NullableKeyBlog>()
                .Property(e => e.Id)
                .ValueGeneratedNever()
                .HasSentinel(_sentinel);
        }
    }

    [ConditionalFact]
    public async Task Insert_with_non_key_default_value()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);

        await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            var blogs = new List<Blog>
            {
                new()
                {
                    Id = IntSentinel,
                    Name = "One Unicorn",
                    CreatedOn = DateTimeSentinel,
                    NeedsConverter = NeedsConverterSentinel
                },
                new()
                {
                    Id = IntSentinel,
                    Name = "Two Unicorns",
                    CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0),
                    NeedsConverter = new NeedsConverter(111)
                }
            };

            context.AddRange(blogs);

            context.SaveChanges();

            Assert.NotEqual(new DateTime(), blogs[0].CreatedOn);
            Assert.NotEqual(new DateTime(), blogs[1].CreatedOn);
            Assert.Equal(111, blogs[1].NeedsConverter.Value);
        }

        await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Name).ToList();
            Assert.Equal(3, blogs.Count);

            Assert.NotEqual(new DateTime(), blogs[0].CreatedOn);
            Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn);
            Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn);

            blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0);

            blogs[1].Name = "X Unicorns";
            blogs[1].NeedsConverter = new NeedsConverter(222);

            blogs[2].Name = "Y Unicorns";
            blogs[2].NeedsConverter = new NeedsConverter(333);

            context.SaveChanges();
        }

        await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.Blogs.OrderBy(e => e.Name).ToList();
            Assert.Equal(3, blogs.Count);

            Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn);
            Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn);
            Assert.Equal(222, blogs[1].NeedsConverter.Value);
            Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn);
            Assert.Equal(333, blogs[2].NeedsConverter.Value);
        }
    }

    /*[ConditionalFact]
    public async Task Insert_with_non_key_default_spatial_value()
    {
        using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);

        using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating))
        {
            context.Database.EnsureCreatedResiliently();

            var blogs = new List<BlogWithSpatial>
            {
                new()
                {
                    Id = IntSentinel,
                    Name = "One Unicorn",
                    GeometryCollection = GeometryCollectionSentinel
                },
                new()
                {
                    Id = IntSentinel,
                    Name = "Two Unicorns",
                    GeometryCollection = GeometryFactory.CreateGeometryCollection(
                        [GeometryFactory.CreatePoint(new Coordinate(1, 3))])
                }
            };

            context.AddRange(blogs);

            context.SaveChanges();

            var point = ((Point)blogs[1].GeometryCollection.Geometries[0]);
            Assert.Equal(1, point.X);
            Assert.Equal(3, point.Y);
        }

        using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.SpatialBlogs.OrderBy(e => e.Name).ToList();
            Assert.Equal(3, blogs.Count);

            var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]);
            Assert.Equal(1, point1.X);
            Assert.Equal(3, point1.Y);

            var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]);
            Assert.Equal(1, point2.X);
            Assert.Equal(2, point2.Y);

            blogs[1].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 11));
            blogs[2].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 22));

            context.SaveChanges();
        }

        using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating))
        {
            var blogs = context.SpatialBlogs.OrderBy(e => e.Name).ToList();
            Assert.Equal(3, blogs.Count);

            var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]);
            Assert.Equal(1, point1.X);
            Assert.Equal(11, point1.Y);

            var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]);
            Assert.Equal(1, point2.X);
            Assert.Equal(22, point2.Y);
        }
    }*/

    public class BlogContextNonKeyDefaultValue(string databaseName, Action<ModelBuilder> modelBuilder)
        : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Blog>(
                b =>
                {
                    b.Property(e => e.CreatedOn).HasDefaultValueSql("NOW()");

                    b.HasData(
                        new Blog
                        {
                            Id = 9979,
                            Name = "W Unicorns",
                            CreatedOn = new DateTime(1974, 8, 3, 0, 10, 0),
                            NeedsConverter = new NeedsConverter(111),
                        });
                });
        }
    }

    /*public class BlogContextNonKeyDefaultSpatialValue(string databaseName, Action<ModelBuilder> modelBuilder)
        : ContextBase(databaseName, modelBuilder)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<BlogWithSpatial>(
                b =>
                {
                    b.Property(e => e.GeometryCollection).HasDefaultValue(GeometryFactory.CreateGeometryCollection());

                    b.HasData(
                        new BlogWithSpatial
                        {
                            Id = 9979,
                            Name = "W Unicorns",
                            GeometryCollection = GeometryFactory.CreateGeometryCollection(
                                [GeometryFactory.CreatePoint(new Coordinate(1, 2))])
                        });
                });
        }
    }*/

    [ConditionalFact]
    public async Task Insert_with_non_key_default_value_readonly()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new Blog
                {
                    Id = IntSentinel,
                    Name = "One Unicorn",
                    CreatedOn = DateTimeSentinel
                },
                new Blog
                {
                    Id = IntSentinel,
                    Name = "Two Unicorns",
                    CreatedOn = DateTimeSentinel
                });

            context.SaveChanges();

            Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn);
        }

        DateTime dateTime0;

        using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            dateTime0 = blogs[0].CreatedOn;

            Assert.NotEqual(new DateTime(), dateTime0);
            Assert.NotEqual(new DateTime(), blogs[1].CreatedOn);

            blogs[0].Name = "One Pegasus";
            blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0);

            context.SaveChanges();
        }

        using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(dateTime0, blogs[0].CreatedOn);
            Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn);
        }
    }

    public class BlogContextNonKeyReadOnlyDefaultValue(
        string databaseName,
        Action<ModelBuilder> modelBuilder,
        int intSentinel,
        DateTime dateTimeSentinel) : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _intSentinel = intSentinel;
        private readonly DateTime _dateTimeSentinel = dateTimeSentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Blog>(
                b =>
                {
                    b.Property(e => e.Id).HasSentinel(_intSentinel);

                    var property = b.Property(e => e.CreatedOn).HasDefaultValueSql("NOW()").HasSentinel(_dateTimeSentinel);
                    property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw);
                });
        }
    }

    [ConditionalFact]
    public async Task Insert_and_update_with_computed_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            var blog = context.Add(
                new FullNameBlog
                {
                    Id = IntSentinel,
                    FirstName = "One",
                    LastName = "Unicorn",
                    FullName = StringSentinel
                }).Entity;

            context.SaveChanges();

            Assert.Equal("One Unicorn", blog.FullName);
        }

        using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            var blog = context.FullNameBlogs.Single();

            Assert.Equal("One Unicorn", blog.FullName);

            blog.LastName = "Pegasus";

            context.SaveChanges();

            Assert.Equal("One Pegasus", blog.FullName);
        }
    }

    public class BlogContextComputedColumn(string databaseName, Action<ModelBuilder> modelBuilder, int intSentinel, string stringSentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _intSentinel = intSentinel;
        private readonly string _stringSentinel = stringSentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<FullNameBlog>(
                b =>
                {
                    b.Property(e => e.Id).HasSentinel(_intSentinel);

                    var property = b.Property(e => e.FullName)
                        .HasComputedColumnSql("FirstName + ' ' + LastName")
                        .HasSentinel(_stringSentinel)
                        .Metadata;

                    property.SetBeforeSaveBehavior(PropertySaveBehavior.Throw);
                    property.SetAfterSaveBehavior(PropertySaveBehavior.Throw);
                });
        }
    }

    public class BlogContextComputedColumnWithTriggerMetadata(
        string databaseName,
        Action<ModelBuilder> modelBuilder,
        int intSentinel,
        string stringSentinel) : BlogContextComputedColumn(databaseName, modelBuilder, intSentinel, stringSentinel)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<FullNameBlog>().ToTable(tb => tb.HasTrigger("SomeTrigger"));
        }
    }

    /*// #6044
    [ConditionalFact]
    public async Task Insert_and_update_with_computed_column_with_function()
    {
        using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            context.Database.ExecuteSqlRaw
            (
                @"CREATE FUNCTION
[dbo].[GetFullName](@First NVARCHAR(MAX), @Second NVARCHAR(MAX))
RETURNS NVARCHAR(MAX) WITH SCHEMABINDING AS BEGIN RETURN @First + @Second END");

            context.GetService<IRelationalDatabaseCreator>().CreateTables();
        }

        using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            var blog = context.Add(
                new FullNameBlog
                {
                    Id = IntSentinel,
                    FirstName = "One",
                    LastName = "Unicorn",
                    FullName = StringSentinel
                }).Entity;

            context.SaveChanges();

            Assert.Equal("OneUnicorn", blog.FullName);
        }

        using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            var blog = context.FullNameBlogs.Single();

            Assert.Equal("OneUnicorn", blog.FullName);

            blog.LastName = "Pegasus";

            context.SaveChanges();

            Assert.Equal("OnePegasus", blog.FullName);
        }
    }

    public class BlogContextComputedColumnWithFunction(
        string databaseName,
        Action<ModelBuilder> modelBuilder,
        int intSentinel,
        string stringSentinel) : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _intSentinel = intSentinel;
        private readonly string _stringSentinel = stringSentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<FullNameBlog>(
                b =>
                {
                    b.Property(e => e.Id).HasSentinel(_intSentinel);

                    var property = modelBuilder.Entity<FullNameBlog>()
                        .Property(e => e.FullName)
                        .HasComputedColumnSql("[dbo].[GetFullName]([FirstName], [LastName])")
                        .HasSentinel(_stringSentinel)
                        .Metadata;

                    property.SetAfterSaveBehavior(PropertySaveBehavior.Throw);
                });
        }
    }

    // #6044
    [ConditionalFact]
    public async Task Insert_and_update_with_computed_column_with_querying_function()
    {
        JetTestStore testStore = null;
        try
        {
            testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
            using (var context = new BlogContextComputedColumnWithTriggerMetadata(
                       testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
            {
                context.GetService<IRelationalDatabaseCreator>().CreateTables();

                context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;");

                context.Database.ExecuteSqlRaw(
                    @"CREATE FUNCTION [dbo].[GetFullName](@Id int)
RETURNS nvarchar(max) WITH SCHEMABINDING AS
BEGIN
    DECLARE @FullName nvarchar(max);
    SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id;
    RETURN @FullName
END");

                context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); ");
            }

            using (var context = new BlogContextComputedColumnWithTriggerMetadata(
                       testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
            {
                var blog = context.Add(
                    new FullNameBlog
                    {
                        Id = IntSentinel,
                        FirstName = "One",
                        LastName = "Unicorn",
                        FullName = StringSentinel
                    }).Entity;

                context.SaveChanges();

                Assert.Equal("OneUnicorn", blog.FullName);
            }

            using (var context = new BlogContextComputedColumnWithTriggerMetadata(
                       testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
            {
                var blog = context.FullNameBlogs.Single();

                Assert.Equal("OneUnicorn", blog.FullName);

                blog.LastName = "Pegasus";

                context.SaveChanges();

                Assert.Equal("OnePegasus", blog.FullName);
            }

            using (var context = new BlogContextComputedColumnWithTriggerMetadata(
                       testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
            {
                var blog1 = context.Add(
                    new FullNameBlog
                    {
                        Id = IntSentinel,
                        FirstName = "Hank",
                        LastName = "Unicorn",
                        FullName = StringSentinel
                    }).Entity;

                var blog2 = context.Add(
                    new FullNameBlog
                    {
                        Id = IntSentinel,
                        FirstName = "Jeff",
                        LastName = "Unicorn",
                        FullName = StringSentinel
                    }).Entity;

                context.SaveChanges();

                Assert.Equal("HankUnicorn", blog1.FullName);
                Assert.Equal("JeffUnicorn", blog2.FullName);
            }
        }
        finally
        {
            using var context = new BlogContextComputedColumnWithTriggerMetadata(
                DatabaseName, OnModelCreating, IntSentinel, StringSentinel);
            context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;");
            context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];");
            testStore?.Dispose();
        }
    }*/

    [ConditionalFact]
    public async Task Insert_with_client_generated_GUID_key()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        Guid afterSave;
        await using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            var blog = context.Add(
                new GuidBlog
                {
                    Id = GuidSentinel,
                    Name = "One Unicorn",
                    NotId = GuidSentinel
                }).Entity;

            var beforeSave = blog.Id;
            var beforeSaveNotId = blog.NotId;

            Assert.NotEqual(default, beforeSave);
            Assert.NotEqual(default, beforeSaveNotId);

            context.SaveChanges();

            afterSave = blog.Id;
            var afterSaveNotId = blog.NotId;

            Assert.Equal(beforeSave, afterSave);
            Assert.Equal(beforeSaveNotId, afterSaveNotId);
        }

        await using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel))
        {
            Assert.Equal(afterSave, context.GuidBlogs.Single().Id);
        }
    }

    public class BlogContextClientGuidKey(string databaseName, Action<ModelBuilder> modelBuilder, Guid sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly Guid _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<GuidBlog>(
                eb =>
                {
                    eb.HasAlternateKey(e => e.NotId);
                    eb.Property(e => e.NotId).ValueGeneratedOnAdd().HasSentinel(_sentinel);
                    eb.Property(e => e.Id).HasSentinel(_sentinel);
                });
        }
    }

    [ConditionalFact]
    public async Task Insert_with_ValueGeneratedOnAdd_GUID_nonkey_property_throws()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContextClientGuidNonKey(testStore.Name, OnModelCreating, GuidSentinel);
        context.Database.EnsureCreatedResiliently();

        var blog = context.Add(
            new GuidBlog
            {
                Id = GuidSentinel,
                Name = "One Unicorn",
                NotId = GuidSentinel
            }).Entity;

        Assert.Equal(GuidSentinel, blog.NotId);

        // No value set on a required column
        var updateException = Assert.Throws<DbUpdateException>(() => context.SaveChanges());
        Assert.Single(updateException.Entries);
    }

    public class BlogContextClientGuidNonKey(string databaseName, Action<ModelBuilder> modelBuilder, Guid sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly Guid _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<GuidBlog>(
                b =>
                {
                    b.Property(e => e.Id).HasSentinel(_sentinel);
                    b.Property(e => e.NotId).ValueGeneratedOnAdd().HasSentinel(_sentinel);
                });
        }
    }

    [ConditionalFact(Skip = "Jet can't return server generated guid's when it is the key. Currently using client geerated guid's")]
    public async Task Insert_with_server_generated_GUID_key()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        Guid afterSave;
        await using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            var blog = context.Add(
                new GuidBlog
                {
                    Id = GuidSentinel,
                    Name = "One Unicorn",
                    NotId = GuidSentinel
                }).Entity;

            var beforeSave = blog.Id;
            var beforeSaveNotId = blog.NotId;

            Assert.Equal(GuidSentinel, beforeSave);
            Assert.Equal(GuidSentinel, beforeSaveNotId);

            context.SaveChanges();

            afterSave = blog.Id;
            var afterSaveNotId = blog.NotId;

            Assert.NotEqual(GuidSentinel, afterSave);
            Assert.NotEqual(GuidSentinel, afterSaveNotId);
            Assert.NotEqual(beforeSave, afterSave);
            Assert.NotEqual(beforeSaveNotId, afterSaveNotId);
        }

        using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel))
        {
            Assert.Equal(afterSave, context.GuidBlogs.Single().Id);
        }
    }

    public class BlogContextServerGuidKey(string databaseName, Action<ModelBuilder> modelBuilder, Guid sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly Guid _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<GuidBlog>(
                    eb =>
                    {
                        eb.Property(e => e.Id).HasDefaultValueSql("GenGUID()").HasSentinel(_sentinel);
                        eb.Property(e => e.NotId).HasDefaultValueSql("GenGUID()").HasSentinel(_sentinel);
                    });
        }
    }

    // Negative cases
    [ConditionalFact]
    public async Task Insert_with_explicit_non_default_keys_by_default()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContext(testStore.Name, OnModelCreating);
        context.Database.EnsureCreatedResiliently();

        context.AddRange(
            new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = 2, Name = "Two Unicorns" });

        // DbUpdateException : An error occurred while updating the entries. See the
        // inner exception for details.
        // SqlException : Cannot insert explicit value for identity column in table
        // 'Blog' when IDENTITY_INSERT is set to OFF.
        context.Database.CreateExecutionStrategy().Execute(context, c => Assert.Throws<DbUpdateException>(() => c.SaveChanges()));
    }

    [ConditionalFact]
    public async Task Insert_with_explicit_default_keys()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContext(testStore.Name, OnModelCreating);
        context.Database.EnsureCreatedResiliently();

        context.AddRange(
            new Blog { Id = IntSentinel, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" });

        // DbUpdateException : An error occurred while updating the entries. See the
        // inner exception for details.
        // SqlException : Cannot insert explicit value for identity column in table
        // 'Blog' when IDENTITY_INSERT is set to OFF.
        var updateException = Assert.Throws<DbUpdateException>(() => context.SaveChanges());
        Assert.Single(updateException.Entries);
    }

    public class BlogContext(string databaseName, Action<ModelBuilder> modelBuilder) : ContextBase(databaseName, modelBuilder);

    [ConditionalFact]
    public async Task Insert_with_implicit_default_keys()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.AddRange(
                new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 667, Name = "Two Unicorns" });

            context.SaveChanges();
        }

        await using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel))
        {
            var blogs = context.Blogs.OrderBy(e => e.Id).ToList();

            Assert.Equal(0, blogs[0].Id);
            Assert.Equal(667, blogs[1].Id);
        }
    }

    public class BlogContextSpecifyKeysUsingDefault(string databaseName, Action<ModelBuilder> modelBuilder, int sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .ValueGeneratedNever()
                .HasSentinel(_sentinel);
        }
    }

    [ConditionalFact]
    public async Task Insert_explicit_value_throws_when_readonly_sequence_before_save()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContextReadOnlySequenceKeyColumnWithDefaultValue(testStore.Name, OnModelCreating, IntSentinel);
        context.Database.EnsureCreatedResiliently();

        context.AddRange(
            new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = IntSentinel, Name = "Two Unicorns" });

        // The property 'Id' on entity type 'Blog' is defined to be read-only before it is
        // saved, but its value has been set to something other than a temporary or default value.
        Assert.Equal(
            CoreStrings.PropertyReadOnlyBeforeSave("Id", "Blog"),
            Assert.Throws<InvalidOperationException>(() => context.SaveChanges()).Message);
    }

    public class BlogContextReadOnlySequenceKeyColumnWithDefaultValue(string databaseName, Action<ModelBuilder> modelBuilder, int sentinel)
        : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _sentinel = sentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.HasSequence("MySequence");

            var property = modelBuilder
                .Entity<Blog>()
                .Property(e => e.Id)
                .HasDefaultValueSql("next value for MySequence")
                .HasSentinel(_sentinel);

            property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw);
        }
    }

    [ConditionalFact]
    public async Task Insert_explicit_value_throws_when_readonly_before_save()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel);
        context.Database.EnsureCreatedResiliently();

        context.AddRange(
            new Blog
            {
                Id = IntSentinel,
                Name = "One Unicorn",
                CreatedOn = DateTimeSentinel
            },
            new Blog
            {
                Id = IntSentinel,
                Name = "Two Unicorns",
                CreatedOn = new DateTime(1973, 8, 3, 0, 10, 0)
            });

        // The property 'CreatedOn' on entity type 'Blog' is defined to be read-only before it is
        // saved, but its value has been set to something other than a temporary or default value.
        Assert.Equal(
            CoreStrings.PropertyReadOnlyBeforeSave("CreatedOn", "Blog"),
            Assert.Throws<InvalidOperationException>(() => context.SaveChanges()).Message);
    }

    [ConditionalFact]
    public async Task Insert_explicit_value_into_computed_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel);
        context.Database.EnsureCreatedResiliently();

        context.Add(
            new FullNameBlog
            {
                Id = IntSentinel,
                FirstName = "One",
                LastName = "Unicorn",
                FullName = "Gerald"
            });

        // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only before it is
        // saved, but its value has been set to something other than a temporary or default value.
        Assert.Equal(
            CoreStrings.PropertyReadOnlyBeforeSave("FullName", "FullNameBlog"),
            Assert.Throws<InvalidOperationException>(() => context.SaveChanges()).Message);
    }

    [ConditionalFact]
    public async Task Update_explicit_value_in_computed_column()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            context.Database.EnsureCreatedResiliently();

            context.Add(
                new FullNameBlog
                {
                    Id = IntSentinel,
                    FirstName = "One",
                    LastName = "Unicorn",
                    FullName = StringSentinel
                });

            context.SaveChanges();
        }

        await using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel))
        {
            var blog = context.FullNameBlogs.Single();

            blog.FullName = "The Gorilla";

            // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only after it has been saved,
            // but its value has been modified or marked as modified.
            Assert.Equal(
                CoreStrings.PropertyReadOnlyAfterSave("FullName", "FullNameBlog"),
                Assert.Throws<InvalidOperationException>(() => context.SaveChanges()).Message);
        }
    }

    // Concurrency
    [ConditionalFact]
    public async Task Resolve_concurrency()
    {
        await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName);
        await using var context = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel);
        context.Database.EnsureCreatedResiliently();

        var blog = context.Add(
            new ConcurrentBlog
            {
                Id = IntSentinel,
                Name = "One Unicorn",
                Timestamp = TimestampSentinel
            }).Entity;

        context.SaveChanges();

        await using var innerContext = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel);
        var updatedBlog = innerContext.ConcurrentBlogs.Single();
        updatedBlog.Name = "One Pegasus";
        innerContext.SaveChanges();
        var currentTimestamp = updatedBlog.Timestamp.ToArray();

        try
        {
            blog.Name = "One Earth Pony";
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException)
        {
            // Update original values (and optionally any current values)
            // Would normally do this with just one method call
            context.Entry(blog).Property(e => e.Id).OriginalValue = updatedBlog.Id;
            context.Entry(blog).Property(e => e.Name).OriginalValue = updatedBlog.Name;
            context.Entry(blog).Property(e => e.Timestamp).OriginalValue = updatedBlog.Timestamp;

            context.SaveChanges();

            Assert.NotEqual(blog.Timestamp, currentTimestamp);
        }
    }

    public class BlogContextConcurrencyWithRowversion(
        string databaseName,
        Action<ModelBuilder> modelBuilder,
        int intSentinel,
        byte[] timestampSentinel) : ContextBase(databaseName, modelBuilder)
    {
        private readonly int _intSentinel = intSentinel;
        private readonly byte[] _timestampSentinel = timestampSentinel;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<ConcurrentBlog>(
                b =>
                {
                    b.Property(e => e.Id).HasSentinel(_intSentinel);
                    b.Property(e => e.Timestamp)
                        .ValueGeneratedOnAddOrUpdate()
                        .IsConcurrencyToken()
                        .HasSentinel(_timestampSentinel);
                });
        }
    }

    protected Blog CreateBlog(string name)
        => new()
        {
            Id = IntSentinel,
            Name = name,
            CreatedOn = DateTimeSentinel,
            NeedsConverter = NeedsConverterSentinel,
            OtherId = NullableIntSentinel
        };

    public class Blog
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime CreatedOn { get; set; }
        public NeedsConverter NeedsConverter { get; set; }
        public int? OtherId { get; set; }
    }

    /*protected BlogWithSpatial CreateBlogWithSpatial(string name)
        => new()
        {
            Id = IntSentinel,
            Name = name,
            GeometryCollection = GeometryCollectionSentinel,
        };

    public class BlogWithSpatial
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public GeometryCollection GeometryCollection { get; set; }
    }*/

    public class NeedsConverter(int value)
    {
        public int Value { get; } = value;

        public override bool Equals(object obj)
            => throw new InvalidOperationException();

        public override int GetHashCode()
            => throw new InvalidOperationException();
    }

    public class NullableKeyBlog
    {
        public int? Id { get; set; }
        public string Name { get; set; }
        public DateTime CreatedOn { get; set; }
    }

    public class FullNameBlog
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName { get; set; }
    }

    public class GuidBlog
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Guid NotId { get; set; }
    }

    public class ConcurrentBlog
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public byte[] Timestamp { get; set; }
    }

    protected virtual void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.Entity<Blog>()
            .Property(e => e.NeedsConverter)
            .HasConversion(
                v => v.Value,
                v => new NeedsConverter(v),
                new ValueComparer<NeedsConverter>(
                    (l, r) => (l == null && r == null) || (l != null && r != null && l.Value == r.Value),
                    v => v.Value.GetHashCode(),
                    v => new NeedsConverter(v.Value)))
            .HasDefaultValue(new NeedsConverter(999));

    public abstract class ContextBase(string databaseName, Action<ModelBuilder> builder) : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        //public DbSet<BlogWithSpatial> SpatialBlogs { get; set; }
        public DbSet<NullableKeyBlog> NullableKeyBlogs { get; set; }
        public DbSet<FullNameBlog> FullNameBlogs { get; set; }
        public DbSet<GuidBlog> GuidBlogs { get; set; }
        public DbSet<ConcurrentBlog> ConcurrentBlogs { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
            => builder(modelBuilder);

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .EnableServiceProviderCaching(false)
                .UseJet(
                    JetTestStore.CreateConnectionString(databaseName),
                    b => b.ApplyConfiguration());
    }

    public static IEnumerable<object[]> IsAsyncData = [[false], [true]];
}
