using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Deck;
using SVSim.Database.Repositories.Viewer;
using SVSim.EmulatedEntrypoint;
using SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication;
namespace SVSim.UnitTests.Infrastructure;
///
/// Test host for the EmulatedEntrypoint app. Each instance opens a private SQLite in-memory
/// database, swaps the production DbContext + Steam auth handler for SQLite-friendly +
/// header-driven test versions, and exposes a helper for tests
/// to create realistic viewer rows.
///
internal sealed class SVSimTestFactory : WebApplicationFactory
{
private readonly SqliteConnection _connection;
private long _nextSeededShortUdid = 400_000_001;
public SVSimTestFactory()
{
// SQLite :memory: lives only as long as a connection is open — keep ours open for the
// factory's lifetime so the DbContext can reattach to the same DB across scopes.
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Tell Program.cs we're in tests so it skips UpdateDatabase() — the Postgres-targeted
// migrations would fail against SQLite. We call EnsureCreated below instead.
builder.UseEnvironment("Testing");
builder.ConfigureTestServices(services =>
{
ReplaceDbContext(services);
ReplaceAuthHandler(services);
});
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = base.CreateHost(builder);
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
db.Database.EnsureCreated();
return host;
}
private void ReplaceDbContext(IServiceCollection services)
{
// Production registered DbContextOptions with the Npgsql provider; tear
// out every related descriptor so AddDbContext below installs a clean SQLite-backed one.
foreach (var descriptor in services
.Where(d => d.ServiceType == typeof(DbContextOptions)
|| d.ServiceType == typeof(DbContextOptions)
|| d.ServiceType == typeof(SVSimDbContext))
.ToList())
{
services.Remove(descriptor);
}
services.AddDbContext(opt =>
{
opt.UseSqlite(_connection);
opt.ReplaceService();
});
}
private static void ReplaceAuthHandler(IServiceCollection services)
{
// Production Program.cs registered SteamSessionAuthenticationHandler under the
// "SteamAuthentication" scheme. Drop that scheme from BOTH the SchemeMap and the
// parallel Schemes list (AddScheme writes to both — and the provider iterates the
// list, not the map, so leaving the old builder behind throws "Scheme already exists"
// when it re-adds during provider construction).
services.AddTransient();
services.PostConfigure(opt =>
{
opt.SchemeMap.Remove(SteamAuthenticationConstants.SchemeName, out _);
var schemesList = (IList)opt.Schemes;
foreach (var stale in schemesList
.Where(s => s.Name == SteamAuthenticationConstants.SchemeName)
.ToList())
{
schemesList.Remove(stale);
}
opt.AddScheme(SteamAuthenticationConstants.SchemeName, b =>
{
b.HandlerType = typeof(TestAuthHandler);
});
});
}
///
/// Creates a fully-formed viewer via the real
/// path (so the test exercises the same nav-graph wiring real users hit). The viewer's
/// ShortUdid is overwritten to a unique non-zero value because the Postgres sequence
/// is disabled on SQLite — without this every test viewer collides on 0.
///
public async Task SeedViewerAsync(
ulong steamId = 76_561_198_000_000_001UL,
string displayName = "Test Viewer")
{
long viewerId;
long shortUdid;
using (var scope = Services.CreateScope())
{
var repo = scope.ServiceProvider.GetRequiredService();
var v = await repo.RegisterViewer(displayName, SocialAccountType.Steam, steamId);
viewerId = v.Id;
shortUdid = Interlocked.Increment(ref _nextSeededShortUdid);
}
// Second scope: assign a real ShortUdid so claim-based lookups in tests have something
// to find (and so per-viewer ShortUdids don't collide across SeedViewerAsync calls).
using (var scope = Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService();
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
v.ShortUdid = shortUdid;
await db.SaveChangesAsync();
}
return viewerId;
}
/// Convenience: bake the X-Test-Viewer-Id header into a fresh client.
public HttpClient CreateAuthenticatedClient(long viewerId)
{
var client = CreateClient();
client.DefaultRequestHeaders.Add(TestAuthHandler.ViewerIdHeader, viewerId.ToString());
return client;
}
///
/// Inserts a deck for the viewer via the real
/// path. Picks the first seeded class/sleeve/leader-skin from the master tables; tests
/// that need specific ids should hit the DB directly.
///
public async Task SeedDeckAsync(long viewerId, Format format, int number, string name = "Test Deck")
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var repo = scope.ServiceProvider.GetRequiredService();
var cls = await db.Classes.FirstAsync();
var sleeve = await db.Sleeves.FirstAsync();
var skin = await db.LeaderSkins.FirstAsync();
await repo.UpsertDeck(viewerId, format, number, d =>
{
d.Name = name;
d.Class = cls;
d.Sleeve = sleeve;
d.LeaderSkin = skin;
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_connection.Dispose();
}
}
}