using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Jiaowu.Api.Tests;

public sealed class AuthSessionServiceTests
{
    [Fact]
    public async Task Web_session_rotates_refresh_token_and_rejects_reuse()
    {
        await using var fixture = await SessionFixture.CreateAsync();

        var issued = await fixture.Service.CreateAsync(
            fixture.User,
            [SystemRoles.Student],
            AuthenticationClientType.Web);

        Assert.InRange(
            issued.SessionExpiresAt,
            DateTime.UtcNow.AddMinutes(29),
            DateTime.UtcNow.AddMinutes(31));
        var stored = await fixture.Db.RefreshSessions.SingleAsync();
        Assert.NotEqual(issued.RefreshToken, stored.TokenHash);

        var refreshed = await fixture.Service.RefreshAsync(issued.RefreshToken);

        Assert.NotNull(refreshed);
        Assert.NotEqual(issued.RefreshToken, refreshed.RefreshToken);
        Assert.Null(await fixture.Service.RefreshAsync(issued.RefreshToken));
        Assert.Equal(1, await fixture.Db.RefreshSessions.CountAsync(x => x.RevokedAt == null));
    }

    [Fact]
    public async Task App_session_uses_three_day_sliding_window()
    {
        await using var fixture = await SessionFixture.CreateAsync();

        var issued = await fixture.Service.CreateAsync(
            fixture.User,
            [SystemRoles.Student],
            AuthenticationClientType.App);

        Assert.InRange(
            issued.SessionExpiresAt,
            DateTime.UtcNow.AddDays(3).AddMinutes(-1),
            DateTime.UtcNow.AddDays(3).AddMinutes(1));
    }

    private sealed class SessionFixture : IAsyncDisposable
    {
        private readonly SqliteConnection _connection;
        private readonly ServiceProvider _provider;
        private readonly AsyncServiceScope _scope;

        private SessionFixture(
            SqliteConnection connection,
            ServiceProvider provider,
            AsyncServiceScope scope,
            AppDbContext db,
            ApplicationUser user,
            IAuthSessionService service)
        {
            _connection = connection;
            _provider = provider;
            _scope = scope;
            Db = db;
            User = user;
            Service = service;
        }

        public AppDbContext Db { get; }
        public ApplicationUser User { get; }
        public IAuthSessionService Service { get; }

        public static async Task<SessionFixture> CreateAsync()
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var services = new ServiceCollection();
            services.AddLogging();
            services.AddDbContext<AppDbContext>(options => options.UseSqlite(connection));
            services.Configure<JwtOptions>(options =>
            {
                options.Issuer = "tests";
                options.Audience = "tests-web";
                options.Key = "a-test-signing-key-that-is-at-least-32-bytes-long";
                options.AccessTokenMinutes = 10;
                options.WebIdleMinutes = 30;
                options.AppIdleMinutes = 4320;
            });
            services
                .AddIdentityCore<ApplicationUser>()
                .AddRoles<ApplicationRole>()
                .AddEntityFrameworkStores<AppDbContext>();
            services.AddScoped<ITokenService, TokenService>();
            services.AddScoped<IAuthSessionService, AuthSessionService>();

            var provider = services.BuildServiceProvider();
            var scope = provider.CreateAsyncScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            await db.Database.EnsureCreatedAsync();
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            var user = new ApplicationUser
            {
                UserName = "session-user",
                DisplayName = "会话测试用户",
                IsEnabled = true,
                LockoutEnabled = true
            };
            Assert.True((await userManager.CreateAsync(user, "SessionUser@123")).Succeeded);

            return new SessionFixture(
                connection,
                provider,
                scope,
                db,
                user,
                scope.ServiceProvider.GetRequiredService<IAuthSessionService>());
        }

        public async ValueTask DisposeAsync()
        {
            await _scope.DisposeAsync();
            await _provider.DisposeAsync();
            await _connection.DisposeAsync();
        }
    }
}
