using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Mongo2Go;
using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Standard.Security;
using StellaOps.Authority.Plugin.Standard.Storage;
using StellaOps.Cryptography;

namespace StellaOps.Authority.Plugin.Standard.Tests;

public class StandardUserCredentialStoreTests : IAsyncLifetime
{
    private readonly MongoDbRunner runner;
    private readonly IMongoDatabase database;
    private readonly StandardPluginOptions options;
    private readonly StandardUserCredentialStore store;

    public StandardUserCredentialStoreTests()
    {
        runner = MongoDbRunner.Start(singleNodeReplSet: true);
        var client = new MongoClient(runner.ConnectionString);
        database = client.GetDatabase("authority-tests");
        options = new StandardPluginOptions
        {
            PasswordPolicy = new PasswordPolicyOptions
            {
                MinimumLength = 8,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
                RequireSymbol = false
            },
            Lockout = new LockoutOptions
            {
                Enabled = true,
                MaxAttempts = 2,
                WindowMinutes = 1
            },
            PasswordHashing = new PasswordHashOptions
            {
                Algorithm = PasswordHashAlgorithm.Argon2id,
                MemorySizeInKib = 8 * 1024,
                Iterations = 2,
                Parallelism = 1
            }
        };
        var cryptoProvider = new DefaultCryptoProvider();
        store = new StandardUserCredentialStore(
            "standard",
            database,
            options,
            new CryptoPasswordHasher(options, cryptoProvider),
            NullLogger<StandardUserCredentialStore>.Instance);
    }

    [Fact]
    public async Task VerifyPasswordAsync_ReturnsSuccess_ForValidCredentials()
    {
        var registration = new AuthorityUserRegistration(
            "alice",
            "Password1!",
            "Alice",
            null,
            false,
            new[] { "admin" },
            new Dictionary<string, string?>());

        var upsert = await store.UpsertUserAsync(registration, CancellationToken.None);
        Assert.True(upsert.Succeeded);

        var result = await store.VerifyPasswordAsync("alice", "Password1!", CancellationToken.None);
        Assert.True(result.Succeeded);
        Assert.Equal("alice", result.User?.Username);
        Assert.Empty(result.AuditProperties);
    }

    [Fact]
    public async Task VerifyPasswordAsync_EnforcesLockout_AfterRepeatedFailures()
    {
        await store.UpsertUserAsync(
            new AuthorityUserRegistration(
                "bob",
                "Password1!",
                "Bob",
                null,
                false,
                new[] { "operator" },
                new Dictionary<string, string?>()),
            CancellationToken.None);

        var first = await store.VerifyPasswordAsync("bob", "wrong", CancellationToken.None);
        Assert.False(first.Succeeded);
        Assert.Equal(AuthorityCredentialFailureCode.InvalidCredentials, first.FailureCode);

        var second = await store.VerifyPasswordAsync("bob", "stillwrong", CancellationToken.None);
        Assert.False(second.Succeeded);
        Assert.Equal(AuthorityCredentialFailureCode.LockedOut, second.FailureCode);
        Assert.NotNull(second.RetryAfter);
        Assert.True(second.RetryAfter.Value > System.TimeSpan.Zero);
        Assert.Contains(second.AuditProperties, property => property.Name == "plugin.lockout_until");
    }

    [Fact]
    public async Task VerifyPasswordAsync_RehashesLegacyHashesToArgon2()
    {
        var legacyHash = new Pbkdf2PasswordHasher().Hash(
            "Legacy1!",
            new PasswordHashOptions
            {
                Algorithm = PasswordHashAlgorithm.Pbkdf2,
                Iterations = 160_000
            });

        var document = new StandardUserDocument
        {
            Username = "legacy",
            NormalizedUsername = "legacy",
            PasswordHash = legacyHash,
            Roles = new List<string>(),
            Attributes = new Dictionary<string, string?>(),
            CreatedAt = DateTimeOffset.UtcNow.AddDays(-1),
            UpdatedAt = DateTimeOffset.UtcNow.AddDays(-1)
        };

        await database.GetCollection<StandardUserDocument>("authority_users_standard")
            .InsertOneAsync(document);

        var result = await store.VerifyPasswordAsync("legacy", "Legacy1!", CancellationToken.None);

        Assert.True(result.Succeeded);
        Assert.Equal("legacy", result.User?.Username);
        Assert.Contains(result.AuditProperties, property => property.Name == "plugin.rehashed");

        var updated = await database.GetCollection<StandardUserDocument>("authority_users_standard")
            .Find(u => u.NormalizedUsername == "legacy")
            .FirstOrDefaultAsync();

        Assert.NotNull(updated);
        Assert.StartsWith("$argon2id$", updated!.PasswordHash, StringComparison.Ordinal);
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync()
    {
        runner.Dispose();
        return Task.CompletedTask;
    }
}
