using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class EmailOwnershipIntegrationTests
{
    [Fact]
    public async Task Real_confirmation_token_is_single_use()
    {
        await using var fixture = await Fixture.CreateAsync();
        var user = await fixture.CreateUserAsync("person@example.test", "person@example.test", confirmed: false);
        var token = await fixture.Users.GenerateEmailConfirmationTokenAsync(user);
        var controller = fixture.Controller();

        Assert.IsType<NoContentResult>(await controller.VerifyEmail(new AuthController.VerifyEmailRequest(user.Id, token)));
        Assert.IsType<BadRequestObjectResult>(await controller.VerifyEmail(new AuthController.VerifyEmailRequest(user.Id, token)));
        Assert.True((await fixture.Users.FindByIdAsync(user.Id))!.EmailConfirmed);
    }

    [Fact]
    public async Task Real_expired_confirmation_token_is_rejected()
    {
        await using var fixture = await Fixture.CreateAsync(TimeSpan.Zero);
        var user = await fixture.CreateUserAsync("expired@example.test", "expired@example.test", confirmed: false);
        var token = await fixture.Users.GenerateEmailConfirmationTokenAsync(user);
        await Task.Delay(20);

        var result = await fixture.Controller().VerifyEmail(new AuthController.VerifyEmailRequest(user.Id, token));

        Assert.IsType<BadRequestObjectResult>(result);
        Assert.False((await fixture.Users.FindByIdAsync(user.Id))!.EmailConfirmed);
    }

    [Fact]
    public async Task Real_change_email_token_preserves_custom_username_and_cannot_be_replayed()
    {
        await using var fixture = await Fixture.CreateAsync();
        var user = await fixture.CreateUserAsync("old@example.test", "ada", confirmed: true);
        user.PendingEmail = "new@example.test";
        user.PendingEmailRequestedAtUtc = DateTimeOffset.UtcNow;
        Assert.True((await fixture.Users.UpdateAsync(user)).Succeeded);
        var token = await fixture.Users.GenerateChangeEmailTokenAsync(user, user.PendingEmail);
        var request = new AuthController.ConfirmEmailChangeRequest(user.Id, user.PendingEmail, token);
        var controller = fixture.Controller();

        Assert.IsType<NoContentResult>(await controller.ConfirmEmailChange(request, default));
        fixture.Db.ChangeTracker.Clear();
        var changed = Assert.IsType<ApplicationUser>(await fixture.Users.FindByIdAsync(user.Id));
        Assert.Equal("new@example.test", changed.Email);
        Assert.Equal("ada", changed.UserName);
        Assert.Null(changed.PendingEmail);
        Assert.IsType<BadRequestObjectResult>(await controller.ConfirmEmailChange(request, default));
    }

    private sealed class Fixture : IAsyncDisposable
    {
        private readonly SqliteConnection _connection;
        private readonly ServiceProvider _provider;
        private readonly Mock<IAppEmailSender> _email = new();
        public JobTrackerContext Db { get; }
        public UserManager<ApplicationUser> Users { get; }

        private Fixture(SqliteConnection connection, ServiceProvider provider, JobTrackerContext db, UserManager<ApplicationUser> users)
        {
            _connection = connection;
            _provider = provider;
            Db = db;
            Users = users;
        }

        public static async Task<Fixture> CreateAsync(TimeSpan? tokenLifespan = null)
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var services = new ServiceCollection();
            services.AddLogging();
            services.AddDataProtection();
            services.AddHttpContextAccessor();
            services.AddScoped<CurrentUserService>();
            services.AddScoped<ICurrentUserService>(provider => provider.GetRequiredService<CurrentUserService>());
            services.AddDbContext<JobTrackerContext>((_, options) => options.UseSqlite(connection));
            services.AddIdentityCore<ApplicationUser>(options => options.User.RequireUniqueEmail = true)
                .AddRoles<IdentityRole>()
                .AddEntityFrameworkStores<JobTrackerContext>()
                .AddDefaultTokenProviders();
            if (tokenLifespan is not null)
                services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = tokenLifespan.Value);
            var provider = services.BuildServiceProvider();
            var scope = provider.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
            await db.Database.EnsureCreatedAsync();
            return new Fixture(connection, provider, db, scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>())
            {
                _scope = scope,
            };
        }

        private IServiceScope? _scope;

        public async Task<ApplicationUser> CreateUserAsync(string email, string userName, bool confirmed)
        {
            var user = new ApplicationUser
            {
                Id = Guid.NewGuid().ToString("N"), UserName = userName, Email = email, EmailConfirmed = confirmed,
            };
            var created = await Users.CreateAsync(user, "Password123!");
            Assert.True(created.Succeeded, string.Join("; ", created.Errors.Select(error => error.Description)));
            return user;
        }

        public AuthController Controller()
        {
            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["App:PublicBaseUrl"] = "https://jobs.example.test",
            }).Build();
            return new AuthController(
                configuration,
                Users,
                Mock.Of<ITokenService>(),
                _email.Object,
                Mock.Of<IGoogleTokenValidator>(),
                Mock.Of<IMicrosoftTokenValidator>(),
                NullLogger<AuthController>.Instance,
                Mock.Of<ITwoFactorPendingTokenService>(),
                Db)
            {
                ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
            };
        }

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