<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Repository\Auth\PasswordResetRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Service\Auth\PasswordResetService;
use MintyPHP\Service\Auth\RememberMeService;
use MintyPHP\Service\Mail\MailService;
use MintyPHP\Service\User\UserPasswordService;
use PHPUnit\Framework\TestCase;

class PasswordResetServiceTest extends TestCase
{
    public function testRequestResetRejectsEmptyEmail(): void
    {
        $service = $this->newService();

        $result = $service->requestReset('  ');

        $this->assertFalse($result['ok']);
        $this->assertSame('email_required', $result['error']);
    }

    public function testRequestResetReturnsOkForUnknownEmailWithoutCreatingReset(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->expects($this->once())
            ->method('findByEmail')
            ->with('missing@example.com')
            ->willReturn(null);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->expects($this->never())->method('create');
        $passwordResetRepository->expects($this->never())->method('invalidateForUser');

        $mailService = $this->createMock(MailService::class);
        $mailService->expects($this->never())->method('sendTemplate');

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository,
            mailService: $mailService
        );

        $result = $service->requestReset('missing@example.com');

        $this->assertTrue($result['ok']);
    }

    public function testRequestResetReturnsCreateFailedWhenRepositoryCreateFails(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn([
            'id' => 7,
            'email' => 'user@example.com',
            'locale' => 'en',
            'first_name' => 'Ada',
            'last_name' => 'Lovelace',
        ]);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->expects($this->once())->method('invalidateForUser')->with(7);
        $passwordResetRepository->expects($this->once())
            ->method('create')
            ->with(
                7,
                $this->callback(static fn (string $hash): bool => (password_get_info($hash)['algo'] ?? null) !== null),
                $this->callback(static fn (string $expiresAt): bool => strtotime($expiresAt . ' UTC') !== false)
            )
            ->willReturn(null);

        $mailService = $this->createMock(MailService::class);
        $mailService->expects($this->never())->method('sendTemplate');

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository,
            mailService: $mailService
        );

        $result = $service->requestReset('user@example.com', 'en');

        $this->assertFalse($result['ok']);
        $this->assertSame('create_failed', $result['error']);
    }

    public function testRequestResetCreatesResetAndSendsMailWhenUserExists(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn([
            'id' => 8,
            'email' => 'user@example.com',
            'locale' => 'en',
            'first_name' => 'Ada',
            'last_name' => 'Lovelace',
        ]);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->expects($this->once())->method('invalidateForUser')->with(8);
        $passwordResetRepository->expects($this->once())->method('create')->willReturn(42);

        $mailService = $this->createMock(MailService::class);
        $mailService->expects($this->once())
            ->method('sendTemplate')
            ->with(
                'reset_code',
                $this->callback(static function (array $vars): bool {
                    return preg_match('/^\d{6}$/', (string) ($vars['code'] ?? '')) === 1
                        && (int) ($vars['expires_minutes'] ?? 0) === 15
                        && str_contains((string) ($vars['verify_url'] ?? ''), 'password/verify');
                }),
                'user@example.com',
                $this->isString(),
                'en'
            )
            ->willReturn(['ok' => true]);

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository,
            mailService: $mailService
        );

        $result = $service->requestReset('user@example.com', 'en');

        $this->assertTrue($result['ok']);
    }

    public function testVerifyCodeRejectsEmptyInput(): void
    {
        $service = $this->newService();

        $result = $service->verifyCode('', '');

        $this->assertFalse($result['ok']);
        $this->assertSame('invalid', $result['error']);
    }

    public function testVerifyCodeReturnsTooManyAttemptsWhenLimitReached(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn(['id' => 9]);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findActiveByUserId')->willReturn([
            'id' => 91,
            'attempts' => 5,
            'code_hash' => password_hash('123456', PASSWORD_DEFAULT),
        ]);
        $passwordResetRepository->expects($this->never())->method('incrementAttempts');

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository
        );

        $result = $service->verifyCode('user@example.com', '123456');

        $this->assertFalse($result['ok']);
        $this->assertSame('too_many_attempts', $result['error']);
    }

    public function testVerifyCodeIncrementsAttemptsOnWrongCode(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn(['id' => 11]);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findActiveByUserId')->willReturn([
            'id' => 111,
            'attempts' => 1,
            'code_hash' => password_hash('654321', PASSWORD_DEFAULT),
        ]);
        $passwordResetRepository->expects($this->once())->method('incrementAttempts')->with(111)->willReturn(true);

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository
        );

        $result = $service->verifyCode('user@example.com', '123456');

        $this->assertFalse($result['ok']);
        $this->assertSame('invalid', $result['error']);
    }

    public function testVerifyCodeReturnsOkForValidCode(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn(['id' => 12]);

        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findActiveByUserId')->willReturn([
            'id' => 121,
            'attempts' => 0,
            'code_hash' => password_hash('123456', PASSWORD_DEFAULT),
        ]);
        $passwordResetRepository->expects($this->never())->method('incrementAttempts');

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            passwordResetRepository: $passwordResetRepository
        );

        $result = $service->verifyCode('user@example.com', '123456');

        $this->assertTrue($result['ok']);
        $this->assertSame(121, (int) ($result['reset_id'] ?? 0));
        $this->assertSame(12, (int) ($result['user_id'] ?? 0));
    }

    public function testResetPasswordReturnsErrorWhenResetRequestMissing(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn(null);

        $service = $this->newService(passwordResetRepository: $passwordResetRepository);

        $result = $service->resetPassword(77, 'StrongPass1!', 'StrongPass1!');

        $this->assertFalse($result['ok']);
        $this->assertSame([t('Reset request not found')], $result['errors']);
    }

    public function testResetPasswordReturnsErrorWhenResetRequestAlreadyUsed(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn([
            'id' => 80,
            'user_id' => 5,
            'used_at' => gmdate('Y-m-d H:i:s'),
        ]);

        $service = $this->newService(passwordResetRepository: $passwordResetRepository);

        $result = $service->resetPassword(80, 'StrongPass1!', 'StrongPass1!');

        $this->assertFalse($result['ok']);
        $this->assertSame([t('Reset request already used')], $result['errors']);
    }

    public function testResetPasswordReturnsErrorWhenResetRequestExpired(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn([
            'id' => 81,
            'user_id' => 5,
            'used_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() - 60),
        ]);

        $service = $this->newService(passwordResetRepository: $passwordResetRepository);

        $result = $service->resetPassword(81, 'StrongPass1!', 'StrongPass1!');

        $this->assertFalse($result['ok']);
        $this->assertSame([t('Reset request expired')], $result['errors']);
    }

    public function testResetPasswordReturnsErrorWhenResetUserIdIsInvalid(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn([
            'id' => 82,
            'user_id' => 0,
            'used_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 300),
        ]);

        $service = $this->newService(passwordResetRepository: $passwordResetRepository);

        $result = $service->resetPassword(82, 'StrongPass1!', 'StrongPass1!');

        $this->assertFalse($result['ok']);
        $this->assertSame([t('Reset request not found')], $result['errors']);
    }

    public function testResetPasswordReturnsValidationErrorsFromUserPasswordService(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn([
            'id' => 83,
            'user_id' => 17,
            'used_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 300),
        ]);
        $passwordResetRepository->expects($this->never())->method('markUsed');

        $userPasswordService = $this->createMock(UserPasswordService::class);
        $userPasswordService->expects($this->once())
            ->method('resetPassword')
            ->with(17, 'weak', 'weak')
            ->willReturn(['ok' => false, 'errors' => ['weak']]);

        $rememberMeService = $this->createMock(RememberMeService::class);
        $rememberMeService->expects($this->never())->method('forgetAllForUser');

        $service = $this->newService(
            passwordResetRepository: $passwordResetRepository,
            userPasswordService: $userPasswordService,
            rememberMeService: $rememberMeService
        );

        $result = $service->resetPassword(83, 'weak', 'weak');

        $this->assertSame(['ok' => false, 'errors' => ['weak']], $result);
    }

    public function testResetPasswordMarksRequestUsedAndExpiresRememberTokensOnSuccess(): void
    {
        $passwordResetRepository = $this->createMock(PasswordResetRepositoryInterface::class);
        $passwordResetRepository->method('findById')->willReturn([
            'id' => 84,
            'user_id' => 18,
            'used_at' => null,
            'expires_at' => gmdate('Y-m-d H:i:s', time() + 300),
        ]);
        $passwordResetRepository->expects($this->once())->method('markUsed')->with(84)->willReturn(true);

        $userPasswordService = $this->createMock(UserPasswordService::class);
        $userPasswordService->expects($this->once())
            ->method('resetPassword')
            ->with(18, 'StrongPass1!', 'StrongPass1!')
            ->willReturn(['ok' => true]);

        $rememberMeService = $this->createMock(RememberMeService::class);
        $rememberMeService->expects($this->once())->method('forgetAllForUser')->with(18);

        $service = $this->newService(
            passwordResetRepository: $passwordResetRepository,
            userPasswordService: $userPasswordService,
            rememberMeService: $rememberMeService
        );

        $result = $service->resetPassword(84, 'StrongPass1!', 'StrongPass1!');

        $this->assertTrue($result['ok']);
    }

    private function newService(
        ?UserReadRepositoryInterface $userReadRepository = null,
        ?PasswordResetRepositoryInterface $passwordResetRepository = null,
        ?UserPasswordService $userPasswordService = null,
        ?RememberMeService $rememberMeService = null,
        ?MailService $mailService = null
    ): PasswordResetService {
        $userReadRepository ??= $this->createMock(UserReadRepositoryInterface::class);
        $passwordResetRepository ??= $this->createMock(PasswordResetRepositoryInterface::class);
        $userPasswordService ??= $this->createMock(UserPasswordService::class);
        $rememberMeService ??= $this->createMock(RememberMeService::class);
        $mailService ??= $this->createMock(MailService::class);

        return new PasswordResetService(
            $userReadRepository,
            $passwordResetRepository,
            $userPasswordService,
            $rememberMeService,
            $mailService
        );
    }
}
