using ErsatzTV.Infrastructure;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Sqlite.Data;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;

namespace ErsatzTV.Tests.Support;

/// <summary>
///     A multi-connection in-memory SQLite harness (shared cache) so several <see cref="TvContext" />
///     instances on DISTINCT connections hit the same database and real unique-constraint enforcement
///     applies across connections. Unlike <see cref="InMemoryTvContext" /> (a single shared connection),
///     this can reproduce a genuine cross-connection write race — needed to exercise the #308 composite-PK
///     collision through the real handler. A keep-alive connection holds the shared in-memory DB open for
///     the harness lifetime.
/// </summary>
public sealed class SharedCacheTvContext : IAsyncDisposable
{
    private readonly string _connectionString;
    private readonly SqliteConnection _keepAlive;

    private SharedCacheTvContext(string connectionString, SqliteConnection keepAlive)
    {
        _connectionString = connectionString;
        _keepAlive = keepAlive;
    }

    public static async Task<SharedCacheTvContext> CreateAsync(string name)
    {
        TvContext.IsSqlite = true;
        TvContext.IsUniqueConstraintViolation = SqliteErrorClassifier.IsUniqueConstraintViolation;

        // Foreign keys OFF (connection-string keyword, inherited by every connection) so partial object
        // graphs can be seeded without satisfying every FK — matches InMemoryTvContext. The composite-PK /
        // UNIQUE constraint under test is enforced regardless of this pragma.
        var connectionString = $"Data Source={name};Mode=Memory;Cache=Shared;Foreign Keys=False";
        var keepAlive = new SqliteConnection(connectionString);
        await keepAlive.OpenAsync();

        await using (TvContext context = Create(BuildOptions(connectionString, null)))
        {
            await context.Database.EnsureCreatedAsync();
        }

        return new SharedCacheTvContext(connectionString, keepAlive);
    }

    /// <summary>A factory whose contexts each open their own connection to the shared DB.</summary>
    public IDbContextFactory<TvContext> Factory(IInterceptor? interceptor = null) =>
        new PerConnectionFactory(_connectionString, interceptor);

    public TvContext CreateContext() => Create(BuildOptions(_connectionString, null));

    /// <summary>A freshly opened raw connection to the shared DB (used to stage a concurrent writer).</summary>
    public SqliteConnection OpenConnection()
    {
        var connection = new SqliteConnection(_connectionString);
        connection.Open();
        return connection;
    }

    public async ValueTask DisposeAsync() => await _keepAlive.DisposeAsync();

    private static DbContextOptions<TvContext> BuildOptions(string connectionString, IInterceptor? interceptor)
    {
        DbContextOptionsBuilder<TvContext> builder = new DbContextOptionsBuilder<TvContext>()
            .UseSqlite(connectionString);
        if (interceptor is not null)
        {
            builder.AddInterceptors(interceptor);
        }

        return builder.Options;
    }

    private static TvContext Create(DbContextOptions<TvContext> options) =>
        new(
            options,
            NullLoggerFactory.Instance,
            new SlowQueryInterceptor(NullLogger<SlowQueryInterceptor>.Instance));

    private sealed class PerConnectionFactory(string connectionString, IInterceptor? interceptor)
        : IDbContextFactory<TvContext>
    {
        public TvContext CreateDbContext() => Create(BuildOptions(connectionString, interceptor));
    }
}
