using System.Security.Claims;
using System.Linq.Expressions;
using System.Data.Common;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Data.Sqlite;
using JobTrackerApi.Data;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class UsersControllerTests
{
    [Fact]
    public async Task SetRoles_rejects_removing_the_final_administrator()
    {
        var admin = User("admin-1");
        var users = TestHostFactory.CreateUserManager(admin);
        users.Setup(x => x.GetRolesAsync(admin)).ReturnsAsync(["Admin"]);
        users.Setup(x => x.GetUsersInRoleAsync("Admin")).ReturnsAsync([admin]);

        var controller = CreateController(users, admin.Id);
        var result = await controller.SetRoles(admin.Id, new UsersController.SetRolesRequest([]), CancellationToken.None);

        var conflict = Assert.IsType<ConflictObjectResult>(result);
        Assert.Equal("Last administrator protected", Assert.IsType<ProblemDetails>(conflict.Value).Title);
        users.Verify(x => x.RemoveFromRolesAsync(It.IsAny<ApplicationUser>(), It.IsAny<IEnumerable<string>>()), Times.Never);
    }

    [Fact]
    public async Task SetRoles_allows_a_confirmed_self_demotion_when_another_admin_exists()
    {
        var admin = User("admin-1");
        var otherAdmin = User("admin-2");
        var users = TestHostFactory.CreateUserManager(admin);
        users.Setup(x => x.GetRolesAsync(admin)).ReturnsAsync(["Admin"]);
        users.Setup(x => x.GetUsersInRoleAsync("Admin")).ReturnsAsync([admin, otherAdmin]);
        users.Setup(x => x.RemoveFromRolesAsync(admin, It.Is<IEnumerable<string>>(roles => roles.Contains("Admin"))))
            .ReturnsAsync(IdentityResult.Success);

        var controller = CreateController(users, admin.Id);
        var result = await controller.SetRoles(admin.Id, new UsersController.SetRolesRequest([]), CancellationToken.None);

        Assert.IsType<NoContentResult>(result);
        users.Verify(x => x.RemoveFromRolesAsync(admin, It.IsAny<IEnumerable<string>>()), Times.Once);
    }

    [Fact]
    public async Task SetRoles_revokes_existing_sessions_and_trusted_devices_when_roles_change()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var currentUser = new Mock<ICurrentUserService>();
        currentUser.SetupGet(service => service.UserId).Returns("admin-1");
        var options = new DbContextOptionsBuilder<JobTrackerContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new JobTrackerContext(options, currentUser.Object);
        await db.Database.EnsureCreatedAsync();

        var member = User("user-1");
        db.Users.Add(member);
        db.UserSessions.Add(new UserSession
        {
            Id = "member-session",
            UserId = member.Id,
            CreatedAtUtc = DateTimeOffset.UtcNow,
            LastSeenAtUtc = DateTimeOffset.UtcNow,
            ExpiresAtUtc = DateTimeOffset.UtcNow.AddDays(1)
        });
        db.TrustedDevices.Add(new TrustedDevice
        {
            UserId = member.Id,
            TokenHash = "device-hash",
            CreatedAtUtc = DateTimeOffset.UtcNow,
            ExpiresAtUtc = DateTimeOffset.UtcNow.AddDays(30)
        });
        await db.SaveChangesAsync();

        var users = TestHostFactory.CreateUserManager(member);
        users.Setup(x => x.GetRolesAsync(member)).ReturnsAsync([]);
        users.Setup(x => x.AddToRoleAsync(member, "Admin")).ReturnsAsync(IdentityResult.Success);
        var controller = CreateController(users, "admin-1", db);

        var result = await controller.SetRoles(member.Id, new UsersController.SetRolesRequest(["Admin"]), CancellationToken.None);

        Assert.IsType<NoContentResult>(result);
        var session = await db.UserSessions.IgnoreQueryFilters().SingleAsync(x => x.Id == "member-session");
        Assert.NotNull(session.RevokedAtUtc);
        Assert.Empty(await db.TrustedDevices.IgnoreQueryFilters().Where(x => x.UserId == member.Id).ToListAsync());
    }

    [Fact]
    public async Task Delete_rejects_deleting_the_final_administrator()
    {
        var admin = User("admin-1");
        var users = TestHostFactory.CreateUserManager(admin);
        users.Setup(x => x.IsInRoleAsync(admin, "Admin")).ReturnsAsync(true);
        users.Setup(x => x.GetUsersInRoleAsync("Admin")).ReturnsAsync([admin]);

        var controller = CreateController(users, admin.Id);
        var result = await controller.Delete(admin.Id, CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        users.Verify(x => x.DeleteAsync(It.IsAny<ApplicationUser>()), Times.Never);
    }

    [Fact]
    public async Task Delete_never_falls_back_to_identity_only_removal()
    {
        var member = User("user-1");
        var users = TestHostFactory.CreateUserManager(member);
        users.Setup(x => x.IsInRoleAsync(member, "Admin")).ReturnsAsync(false);
        var controller = CreateController(users, "admin-1");

        var result = Assert.IsType<ObjectResult>(await controller.Delete(member.Id, CancellationToken.None));

        Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode);
        users.Verify(x => x.DeleteAsync(It.IsAny<ApplicationUser>()), Times.Never);
    }

    [Fact]
    public async Task List_marks_the_current_user_and_last_admin_safety_state()
    {
        var admin = User("admin-1");
        var member = User("user-1");
        var users = TestHostFactory.CreateUserManager();
        users.SetupGet(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>([admin, member]));
        users.Setup(x => x.GetUsersInRoleAsync("Admin")).ReturnsAsync([admin]);
        users.Setup(x => x.GetRolesAsync(admin)).ReturnsAsync(["Admin"]);
        users.Setup(x => x.GetRolesAsync(member)).ReturnsAsync([]);

        var controller = CreateController(users, admin.Id);
        var action = await controller.List(CancellationToken.None);

        var rows = Assert.IsType<List<UsersController.UserDto>>(Assert.IsType<OkObjectResult>(action.Result).Value);
        var adminRow = Assert.Single(rows, row => row.Id == admin.Id);
        Assert.True(adminRow.IsCurrentUser);
        Assert.False(adminRow.CanRemoveAdmin);
        Assert.True(Assert.Single(rows, row => row.Id == member.Id).CanRemoveAdmin);
    }

    [Fact]
    public async Task List_uses_a_fixed_number_of_role_queries_for_many_users()
    {
        var counter = new CommandCounter();
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var currentUser = new Mock<ICurrentUserService>();
        currentUser.SetupGet(service => service.UserId).Returns("admin-1");
        var options = new DbContextOptionsBuilder<JobTrackerContext>()
            .UseSqlite(connection)
            .AddInterceptors(counter)
            .Options;
        await using var db = new JobTrackerContext(options, currentUser.Object);
        await db.Database.EnsureCreatedAsync();
        var adminRole = new IdentityRole("Admin") { Id = "role-admin", NormalizedName = "ADMIN" };
        var memberRole = new IdentityRole("Member") { Id = "role-member", NormalizedName = "MEMBER" };
        db.Roles.AddRange(adminRole, memberRole);
        var usersToAdd = Enumerable.Range(1, 75).Select(index => User(index == 1 ? "admin-1" : $"user-{index}")).ToList();
        db.Users.AddRange(usersToAdd);
        db.UserRoles.Add(new IdentityUserRole<string> { UserId = "admin-1", RoleId = adminRole.Id });
        foreach (var user in usersToAdd.Skip(1))
            db.UserRoles.Add(new IdentityUserRole<string> { UserId = user.Id, RoleId = memberRole.Id });
        await db.SaveChangesAsync();
        counter.Reset();

        var manager = TestHostFactory.CreateUserManager();
        var controller = CreateController(manager, "admin-1", db);
        var action = await controller.List(CancellationToken.None);

        var rows = Assert.IsType<List<UsersController.UserDto>>(Assert.IsType<OkObjectResult>(action.Result).Value);
        Assert.Equal(75, rows.Count);
        Assert.Equal(2, counter.ReaderCount);
        manager.Verify(userManager => userManager.GetRolesAsync(It.IsAny<ApplicationUser>()), Times.Never);
        manager.Verify(userManager => userManager.GetUsersInRoleAsync(It.IsAny<string>()), Times.Never);
    }

    private static ApplicationUser User(string id) => new()
    {
        Id = id,
        Email = $"{id}@example.com",
        UserName = $"{id}@example.com"
    };

    private static UsersController CreateController(Mock<UserManager<ApplicationUser>> users, string currentUserId, JobTrackerContext? db = null)
    {
        var roleStore = new Mock<IRoleStore<IdentityRole>>();
        var roles = new Mock<RoleManager<IdentityRole>>(
            roleStore.Object,
            Array.Empty<IRoleValidator<IdentityRole>>(),
            new UpperInvariantLookupNormalizer(),
            new IdentityErrorDescriber(),
            new NullLogger<RoleManager<IdentityRole>>());
        roles.Setup(manager => manager.RoleExistsAsync(It.IsAny<string>())).ReturnsAsync(true);

        var controller = new UsersController(
            users.Object,
            roles.Object,
            Mock.Of<IAppEmailSender>(),
            new ConfigurationBuilder().Build(),
            new NullLogger<UsersController>(),
            ExternalOrigin.Parse("http://localhost:3000", production: false),
            db: db);

        var identity = new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, currentUserId)], "test");
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
        };
        return controller;
    }

    private sealed class CommandCounter : DbCommandInterceptor
    {
        public int ReaderCount { get; private set; }

        public void Reset() => ReaderCount = 0;

        public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
            DbCommand command,
            CommandEventData eventData,
            InterceptionResult<DbDataReader> result,
            CancellationToken cancellationToken = default)
        {
            ReaderCount++;
            return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
        }
    }

    private sealed class TestAsyncQueryProvider<TEntity>(IQueryProvider inner) : IAsyncQueryProvider
    {
        public IQueryable CreateQuery(Expression expression) => new TestAsyncEnumerable<TEntity>(expression);
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression) => new TestAsyncEnumerable<TElement>(expression);
        public object? Execute(Expression expression) => inner.Execute(expression);
        public TResult Execute<TResult>(Expression expression) => inner.Execute<TResult>(expression);
        public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default)
            => (TResult)typeof(Task)
                .GetMethod(nameof(Task.FromResult))!
                .MakeGenericMethod(typeof(TResult).GetGenericArguments()[0])
                .Invoke(null, [Execute(expression)])!;
    }

    private sealed class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
    {
        public TestAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { }
        public TestAsyncEnumerable(Expression expression) : base(expression) { }

        public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
            => new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());

        IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider<T>(this);
    }

    private sealed class TestAsyncEnumerator<T>(IEnumerator<T> inner) : IAsyncEnumerator<T>
    {
        public T Current => inner.Current;
        public ValueTask DisposeAsync()
        {
            inner.Dispose();
            return ValueTask.CompletedTask;
        }

        public ValueTask<bool> MoveNextAsync() => ValueTask.FromResult(inner.MoveNext());
    }
}
