using System.Reflection;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Primitives;
using Nexus.Api.Data;
using Nexus.Api.DTOs;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

/// <summary>
/// Tests for AuthService login, change-password, admin-reset, and related flows.
/// These are unit-level tests using an in-memory EF Core database so no
/// external PostgreSQL instance is needed.
/// </summary>
public sealed class AuthServiceTests
{
    // ── Fixture helpers ─────────────────────────────────────────────────

    /// <summary>
    /// Creates a test fixture with an in-memory database, a UserRepository,
    /// and an AuthService backed by an in-memory configuration.
    /// </summary>
    private static (NexusDbContext db, IUserRepository repo, AuthService auth) CreateFixture()
    {
        var options = new DbContextOptionsBuilder<NexusDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        var db = new NexusDbContext(options);
        var repo = new UserRepository(db);

        // In-memory config with minimum required JWT settings
        var config = new MemoryConfig(new Dictionary<string, string?>
        {
            ["Jwt:Key"] = "this-is-a-test-key-that-is-at-least-32-bytes-long!",
            ["Jwt:Issuer"] = "nexus-test",
            ["Jwt:Audience"] = "nexus-test-web",
        });

        var logger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AuthService>.Instance;
        var auth = new AuthService(repo, config, logger);
        return (db, repo, auth);
    }

    private static LoginRequest Login(string email, string password)
        => new() { Email = email, Password = password };

    private static async Task<NexusUser> SeedUserAsync(NexusDbContext db, string email, string password, string role = "user")
    {
        var user = new NexusUser
        {
            Email = email,
            NormalizedEmail = AuthService.NormalizeEmail(email),
            DisplayName = email.Split('@')[0],
            PasswordHash = PasswordSecurity.Hash(password),
            Role = role
        };
        db.Users.Add(user);
        await db.SaveChangesAsync();
        return user;
    }

    // ══════════════════════════════════════════════════════════════════
    // Password Security Unit Tests
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public void Hash_And_Verify_RoundTrip_Succeeds()
    {
        const string password = "MyTestPassword123!";
        var hash = PasswordSecurity.Hash(password);
        Assert.NotNull(hash);
        Assert.StartsWith("v1.", hash);

        var ok = PasswordSecurity.Verify(password, hash, out var needsUpgrade);
        Assert.True(ok);
        Assert.False(needsUpgrade);
    }

    [Fact]
    public void Verify_WrongPassword_Fails()
    {
        var hash = PasswordSecurity.Hash("CorrectPassword123!");
        Assert.False(PasswordSecurity.Verify("WrongPassword456!", hash, out _));
    }

    [Fact]
    public void Verify_EmptyHash_ReturnsFalse()
    {
        Assert.False(PasswordSecurity.Verify("password", "", out _));
    }

    [Fact]
    public void Verify_LegacySha256_PassesAndFlagsUpgrade()
    {
        const string password = "OldFormatPassword123!";
        var legacyHash = Convert.ToHexString(
            System.Security.Cryptography.SHA256.HashData(
                System.Text.Encoding.UTF8.GetBytes(password)));

        var ok = PasswordSecurity.Verify(password, legacyHash, out var needsUpgrade);
        Assert.True(ok);
        Assert.True(needsUpgrade);
    }

    // ══════════════════════════════════════════════════════════════════
    // Login Tests
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public async Task Login_WithValidCredentials_Succeeds()
    {
        var (db, repo, auth) = CreateFixture();
        const string password = "ValidPassword123!";
        await SeedUserAsync(db, "test@example.com", password);

        var session = await auth.LoginAsync(Login("test@example.com", password));
        Assert.NotNull(session);
        Assert.Equal("test", session.User.DisplayName);
    }

    [Fact]
    public async Task Login_WithWrongPassword_ReturnsNull()
    {
        var (db, repo, auth) = CreateFixture();
        await SeedUserAsync(db, "test@example.com", "CorrectPassword123!");

        Assert.Null(await auth.LoginAsync(Login("test@example.com", "WrongPassword456!")));
    }

    [Fact]
    public async Task Login_WithNonexistentEmail_ReturnsNull()
    {
        var (db, repo, auth) = CreateFixture();
        Assert.Null(await auth.LoginAsync(Login("nobody@example.com", "SomePassword123!")));
    }

    [Fact]
    public async Task Login_UpdatesLastLoginAt()
    {
        var (db, repo, auth) = CreateFixture();
        const string password = "TestPassword123!";
        var user = await SeedUserAsync(db, "test@example.com", password);

        var beforeLogin = user.LastLoginAt;
        await Task.Delay(10);

        Assert.NotNull(await auth.LoginAsync(Login("test@example.com", password)));

        var updated = await repo.GetByIdAsync(user.Id);
        Assert.NotNull(updated!.LastLoginAt);
        Assert.True(updated.LastLoginAt > beforeLogin || beforeLogin is null);
    }

    /// <summary>
    /// Validates that LoginAsync persists a password hash upgrade AND login
    /// timestamps even when there are NO expired refresh tokens. Previously
    /// the code relied on RemoveExpiredTokensAsync calling SaveChangesAsync,
    /// but that only happens when oldTokens.Count > 0.
    /// </summary>
    [Fact]
    public async Task Login_WithLegacyHash_UpgradesAndPersistsWithoutExpiredTokens()
    {
        var (db, repo, auth) = CreateFixture();
        const string password = "LegacyUpgradePassword123!";

        var legacyHash = Convert.ToHexString(
            System.Security.Cryptography.SHA256.HashData(
                System.Text.Encoding.UTF8.GetBytes(password)));

        var user = new NexusUser
        {
            Email = "legacy@example.com",
            NormalizedEmail = AuthService.NormalizeEmail("legacy@example.com"),
            DisplayName = "Legacy",
            PasswordHash = legacyHash,
            Role = "user"
        };
        db.Users.Add(user);
        await db.SaveChangesAsync();

        // Login triggers hash upgrade
        Assert.NotNull(await auth.LoginAsync(Login("legacy@example.com", password)));

        var updated = await repo.GetByIdAsync(user.Id);
        Assert.NotNull(updated);
        Assert.StartsWith("v1.", updated.PasswordHash);
        Assert.NotEqual(legacyHash, updated.PasswordHash);

        // Second login with the upgraded hash should also work
        Assert.NotNull(await auth.LoginAsync(Login("legacy@example.com", password)));
    }

    [Fact]
    public async Task Login_WithExistingHash_DoesNotChangeHash()
    {
        var (db, repo, auth) = CreateFixture();
        const string password = "StablePassword123!";
        var user = await SeedUserAsync(db, "stable@example.com", password);

        var originalHash = user.PasswordHash;
        Assert.NotNull(await auth.LoginAsync(Login("stable@example.com", password)));

        var updated = await repo.GetByIdAsync(user.Id);
        Assert.NotNull(updated);
        Assert.Equal(originalHash, updated.PasswordHash);
    }

    [Fact]
    public async Task RefreshToken_SurvivesServiceRestart_WhenDatabasePersists()
    {
        var databaseRoot = new InMemoryDatabaseRoot();
        var databaseName = Guid.NewGuid().ToString();
        var configuration = new MemoryConfig(new Dictionary<string, string?>
        {
            ["Jwt:Key"] = "this-is-a-test-key-that-is-at-least-32-bytes-long!",
            ["Jwt:Issuer"] = "nexus-test",
            ["Jwt:Audience"] = "nexus-test-web",
        });
        var logger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AuthService>.Instance;

        AuthSession initialSession;
        await using (var firstDb = new NexusDbContext(
                         new DbContextOptionsBuilder<NexusDbContext>()
                             .UseInMemoryDatabase(databaseName, databaseRoot)
                             .Options))
        {
            await SeedUserAsync(firstDb, "restart@example.com", "RestartPassword123!");
            var firstService = new AuthService(new UserRepository(firstDb), configuration, logger);
            initialSession = Assert.IsType<AuthSession>(
                await firstService.LoginAsync(Login("restart@example.com", "RestartPassword123!")));
        }

        await using var restartedDb = new NexusDbContext(
            new DbContextOptionsBuilder<NexusDbContext>()
                .UseInMemoryDatabase(databaseName, databaseRoot)
                .Options);
        var restartedService = new AuthService(
            new UserRepository(restartedDb),
            configuration,
            logger);

        var refreshed = await restartedService.RefreshAsync(initialSession.RefreshToken);

        Assert.NotNull(refreshed);
        Assert.NotEqual(initialSession.RefreshToken, refreshed.RefreshToken);
        Assert.Equal(initialSession.User.Id, refreshed.User.Id);
    }

    // ══════════════════════════════════════════════════════════════════
    // Change Password Tests
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public async Task ChangePassword_WithCorrectCurrentPassword_Succeeds()
    {
        var (db, repo, auth) = CreateFixture();
        const string oldPw = "OldPassword123!";
        const string newPw = "NewPassword456!";
        var user = await SeedUserAsync(db, "changepw@example.com", oldPw);

        var result = await auth.ChangePasswordAsync(user.Id, new ChangePasswordRequest
        {
            CurrentPassword = oldPw,
            NewPassword = newPw
        });
        Assert.True(result);

        Assert.Null(await auth.LoginAsync(Login("changepw@example.com", oldPw)));
        Assert.NotNull(await auth.LoginAsync(Login("changepw@example.com", newPw)));
    }

    [Fact]
    public async Task ChangePassword_WithWrongCurrentPassword_Fails()
    {
        var (db, repo, auth) = CreateFixture();
        var user = await SeedUserAsync(db, "wrongpw@example.com", "ActualPassword123!");

        Assert.False(await auth.ChangePasswordAsync(user.Id, new ChangePasswordRequest
        {
            CurrentPassword = "WrongPassword456!",
            NewPassword = "NewPassword789!"
        }));
    }

    // ══════════════════════════════════════════════════════════════════
    // Admin Reset Password Tests
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public async Task AdminResetPassword_WithValidToken_Succeeds()
    {
        var (db, repo, auth) = CreateFixture();
        Environment.SetEnvironmentVariable("Admin__ResetToken", "test-admin-token-123");

        const string oldPw = "OldPassword123!";
        const string newPw = "NewAdminPassword456!";
        await SeedUserAsync(db, "adminreset@example.com", oldPw);

        Assert.True(await auth.AdminResetPasswordAsync("adminreset@example.com", newPw, "test-admin-token-123"));
        Assert.Null(await auth.LoginAsync(Login("adminreset@example.com", oldPw)));
        Assert.NotNull(await auth.LoginAsync(Login("adminreset@example.com", newPw)));
    }

    [Fact]
    public async Task AdminResetPassword_WithInvalidToken_Fails()
    {
        var (db, repo, auth) = CreateFixture();
        Environment.SetEnvironmentVariable("Admin__ResetToken", "real-token-xyz");
        await SeedUserAsync(db, "badreset@example.com", "OriginalPassword123!");

        Assert.False(await auth.AdminResetPasswordAsync("badreset@example.com", "NewPassword456!", "wrong-token"));
    }

    [Fact]
    public async Task AdminResetPassword_NonexistentUser_Fails()
    {
        var (db, repo, auth) = CreateFixture();
        Environment.SetEnvironmentVariable("Admin__ResetToken", "test-token");
        Assert.False(await auth.AdminResetPasswordAsync("nobody@example.com", "NewPassword456!", "test-token"));
    }

    [Fact]
    public async Task AdminResetPassword_ShortPassword_Fails()
    {
        var (db, repo, auth) = CreateFixture();
        Environment.SetEnvironmentVariable("Admin__ResetToken", "test-token");
        Assert.False(await auth.AdminResetPasswordAsync("test@example.com", "short", "test-token"));
    }

    // ══════════════════════════════════════════════════════════════════
    // Profile Update Tests
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public async Task UpdateProfile_ChangesDisplayName()
    {
        var (db, repo, auth) = CreateFixture();
        const string password = "Password123!";
        var user = await SeedUserAsync(db, "profile@example.com", password);

        var updated = await auth.UpdateProfileAsync(user.Id, new UpdateProfileRequest
        {
            DisplayName = "New Name"
        });
        Assert.NotNull(updated);
        Assert.Equal("New Name", updated.DisplayName);
    }

    // ══════════════════════════════════════════════════════════════════
    // NormalizeEmail
    // ══════════════════════════════════════════════════════════════════

    [Fact]
    public void NormalizeEmail_TrimsAndUppercases()
    {
        Assert.Equal("TEST@EXAMPLE.COM", AuthService.NormalizeEmail("  test@Example.com  "));
        Assert.Equal("A@B.COM", AuthService.NormalizeEmail("a@b.com"));
    }
}

/// <summary>
/// Minimal in-memory IConfiguration implementation for unit tests.
/// Reads from a case-insensitive dictionary.
/// </summary>
internal sealed class MemoryConfig : Microsoft.Extensions.Configuration.IConfiguration
{
    private readonly Dictionary<string, string?> _data;
    private readonly Dictionary<string, MemoryConfigSection> _sections;

    public MemoryConfig(Dictionary<string, string?> data)
    {
        _data = new Dictionary<string, string?>(data, StringComparer.OrdinalIgnoreCase);
        _sections = new Dictionary<string, MemoryConfigSection>(StringComparer.OrdinalIgnoreCase);
    }

    public string? this[string key]
    {
        get => _data.TryGetValue(key, out var val) ? val : null;
        set => _data[key] = value ?? string.Empty;
    }

    public Microsoft.Extensions.Configuration.IConfigurationSection GetSection(string key)
    {
        if (!_sections.TryGetValue(key, out var section))
        {
            section = new MemoryConfigSection(key, this);
            _sections[key] = section;
        }
        return section;
    }

    public IEnumerable<Microsoft.Extensions.Configuration.IConfigurationSection> GetChildren()
        => Enumerable.Empty<Microsoft.Extensions.Configuration.IConfigurationSection>();

    public IChangeToken GetReloadToken()
        => NeverToken.Instance;
}

internal sealed class MemoryConfigSection(string path, MemoryConfig root) : Microsoft.Extensions.Configuration.IConfigurationSection
{
    public string Key => path.Split(':').Last();
    public string Path => path;
    public string? Value { get => root[path]; set => root[path] = value; }

    public string? this[string key]
    {
        get => root[$"{path}:{key}"];
        set => root[$"{path}:{key}"] = value;
    }

    public Microsoft.Extensions.Configuration.IConfigurationSection GetSection(string key)
        => root.GetSection($"{path}:{key}");

    public IEnumerable<Microsoft.Extensions.Configuration.IConfigurationSection> GetChildren()
        => Enumerable.Empty<Microsoft.Extensions.Configuration.IConfigurationSection>();

    public IChangeToken GetReloadToken()
        => NeverToken.Instance;
}

/// <summary>A change token that never signals — for test-use IConfiguration stubs.</summary>
internal sealed class NeverToken : IChangeToken
{
    public static readonly NeverToken Instance = new();
    public bool HasChanged => false;
    public bool ActiveChangeCallbacks => false;
    public IDisposable RegisterChangeCallback(Action<object?> callback, object? state) => NoopDisposable.Instance;
}

internal sealed class NoopDisposable : IDisposable
{
    public static readonly NoopDisposable Instance = new();
    public void Dispose() { }
}
