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 ClaimLocalAdminHandlerTests
{
    private InMemoryTvContext _db = null!;
    private IConfigElementRepository _configElementRepository = null!;
    private ILocalPasswordHasher _passwordHasher = null!;

    [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 ClaimLocalAdminHandler MakeHandler() =>
        new(_db.Factory, _passwordHasher);

    [Test]
    public async Task Handle_Should_Claim_Fresh_Admin_And_Persist_All_Config_Elements()
    {
        ClaimLocalAdminHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new ClaimLocalAdmin("Operator", "supersecret"),
            CancellationToken.None);

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

        Option<string> storedUser = await _configElementRepository.GetValue<string>(
            ConfigElementKey.AuthLocalAdminUsername,
            CancellationToken.None);
        storedUser.IfNone("").ShouldBe("Operator");

        Option<string> storedStamp = await _configElementRepository.GetValue<string>(
            ConfigElementKey.AuthSecurityStamp,
            CancellationToken.None);
        storedStamp.IfNone("").ShouldBe(principal.SecurityStamp);

        Option<string> storedHash = await _configElementRepository.GetValue<string>(
            ConfigElementKey.AuthLocalAdminPasswordHash,
            CancellationToken.None);
        storedHash.IsSome.ShouldBeTrue();
        _passwordHasher.Verify(storedHash.IfNone(""), "supersecret")
            .ShouldNotBe(LocalPasswordVerification.Failed);
    }

    [Test]
    public async Task Handle_Should_Refuse_Second_Claim_When_Admin_Already_Configured()
    {
        ClaimLocalAdminHandler handler = MakeHandler();
        (await handler.Handle(new ClaimLocalAdmin("First", "supersecret"), CancellationToken.None))
            .IsRight.ShouldBeTrue();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new ClaimLocalAdmin("Second", "anothersecret"),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();

        // The original credential is untouched.
        Option<string> storedUser = await _configElementRepository.GetValue<string>(
            ConfigElementKey.AuthLocalAdminUsername,
            CancellationToken.None);
        storedUser.IfNone("").ShouldBe("First");
    }

    [Test]
    public async Task Handle_Should_Reject_Whitespace_Username_And_Persist_Nothing()
    {
        ClaimLocalAdminHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new ClaimLocalAdmin("   ", "supersecret"),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
        await AssertNothingPersisted();
    }

    [Test]
    public async Task Handle_Should_Reject_Short_Password_And_Persist_Nothing()
    {
        string shortPassword = new('a', AuthConstants.MinPasswordLength - 1);
        ClaimLocalAdminHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new ClaimLocalAdmin("Operator", shortPassword),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
        await AssertNothingPersisted();
    }

    [Test]
    public async Task Handle_Should_Reject_Over_Long_Password_And_Persist_Nothing()
    {
        // LocalAdminHelpers.MaxPasswordLength (1024) is internal; use the documented bound directly.
        string longPassword = new('a', 1024 + 1);
        ClaimLocalAdminHandler handler = MakeHandler();

        Either<BaseError, LocalAdminPrincipal> result = await handler.Handle(
            new ClaimLocalAdmin("Operator", longPassword),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
        await AssertNothingPersisted();
    }

    private async Task AssertNothingPersisted()
    {
        (await _configElementRepository.GetConfigElement(
            ConfigElementKey.AuthLocalAdminUsername,
            CancellationToken.None)).IsNone.ShouldBeTrue();
        (await _configElementRepository.GetConfigElement(
            ConfigElementKey.AuthLocalAdminPasswordHash,
            CancellationToken.None)).IsNone.ShouldBeTrue();
        (await _configElementRepository.GetConfigElement(
            ConfigElementKey.AuthSecurityStamp,
            CancellationToken.None)).IsNone.ShouldBeTrue();
    }
}
