using ErsatzTV.Application.Auth;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Tests.Support;
using LanguageExt;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Application.Auth;

[TestFixture]
public class VerifyLocalAdminLoginHandlerTests
{
    private InMemoryTvContext _db = null!;
    private IConfigElementRepository _configElementRepository = null!;
    private ILocalPasswordHasher _passwordHasher = null!;

    private const string Username = "Operator";
    private const string Password = "supersecret";

    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _configElementRepository = new ConfigElementRepository(_db.Factory);
        _passwordHasher = new LocalPasswordHasher();
    }

    [TearDown]
    public async Task TearDown() => await _db.DisposeAsync();

    private VerifyLocalAdminLoginHandler MakeHandler() =>
        new(_db.Factory, _passwordHasher);

    private async Task SeedAdmin()
    {
        var claim = new ClaimLocalAdminHandler(_db.Factory, _passwordHasher);
        (await claim.Handle(new ClaimLocalAdmin(Username, Password), CancellationToken.None))
            .IsRight.ShouldBeTrue();
    }

    private async Task<string> StoredStamp() =>
        (await _configElementRepository.GetValue<string>(
            ConfigElementKey.AuthSecurityStamp,
            CancellationToken.None)).IfNone("");

    [Test]
    public async Task Handle_Should_Return_Principal_With_Stored_Stamp_On_Valid_Credentials()
    {
        await SeedAdmin();
        VerifyLocalAdminLoginHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new VerifyLocalAdminLogin(Username, Password),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue();
        LocalAdminPrincipal principal = result.Match(
            Left: e => throw new ShouldAssertException(e.ToString()),
            Right: p => p);
        principal.Username.ShouldBe(Username);
        principal.SecurityStamp.ShouldBe(await StoredStamp());
    }

    [Test]
    public async Task Handle_Should_Fail_On_Wrong_Password()
    {
        await SeedAdmin();
        VerifyLocalAdminLoginHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new VerifyLocalAdminLogin(Username, "wrongpassword"),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
    }

    [Test]
    public async Task Handle_Should_Fail_On_Wrong_Username()
    {
        await SeedAdmin();
        VerifyLocalAdminLoginHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new VerifyLocalAdminLogin("SomebodyElse", Password),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
    }

    [Test]
    public async Task Handle_Should_Fail_On_Unconfigured_Db()
    {
        VerifyLocalAdminLoginHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new VerifyLocalAdminLogin(Username, Password),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
    }

    [Test]
    public async Task Handle_Should_Match_Username_Case_Insensitively()
    {
        await SeedAdmin();
        VerifyLocalAdminLoginHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new VerifyLocalAdminLogin(Username.ToUpperInvariant(), Password),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue();
        LocalAdminPrincipal principal = result.Match(
            Left: e => throw new ShouldAssertException(e.ToString()),
            Right: p => p);
        // The stored (canonical) username is returned, not the differently-cased input.
        principal.Username.ShouldBe(Username);
    }
}
