<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Repository\Auth\EmailVerificationRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Auth\EmailVerificationService;
use MintyPHP\Service\Mail\MailService;
use PHPUnit\Framework\TestCase;

class EmailVerificationServiceTest extends TestCase
{
    public function testSendVerificationReturnsUserNotFoundWhenUserDoesNotExist(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->expects($this->once())->method('find')->with(9)->willReturn(null);

        $service = $this->newService(userReadRepository: $userReadRepository);
        $result = $service->sendVerification(9);

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

    public function testSendVerificationReturnsEmailRequiredWhenEmailIsMissing(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->expects($this->once())->method('find')->with(10)->willReturn([
            'id' => 10,
            'email' => '',
        ]);

        $service = $this->newService(userReadRepository: $userReadRepository);
        $result = $service->sendVerification(10);

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

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

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->expects($this->once())->method('invalidateForUser')->with(11);
        $verificationRepository->expects($this->once())
            ->method('create')
            ->with(
                11,
                $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,
            emailVerificationRepository: $verificationRepository,
            mailService: $mailService
        );
        $result = $service->sendVerification(11, 'en');

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

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

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->expects($this->once())->method('invalidateForUser')->with(12);
        $verificationRepository->expects($this->once())->method('create')->willReturn(120);

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

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            emailVerificationRepository: $verificationRepository,
            mailService: $mailService
        );
        $result = $service->sendVerification(12, '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 testVerifyCodeReturnsAlreadyVerifiedWhenUserIsAlreadyVerified(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn([
            'id' => 13,
            'email_verified_at' => gmdate('Y-m-d H:i:s'),
        ]);

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

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

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

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

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            emailVerificationRepository: $verificationRepository
        );
        $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' => 15,
            'email_verified_at' => null,
        ]);

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->method('findActiveByUserId')->willReturn([
            'id' => 150,
            'attempts' => 0,
            'code_hash' => password_hash('654321', PASSWORD_DEFAULT),
        ]);
        $verificationRepository->expects($this->once())->method('incrementAttempts')->with(150)->willReturn(true);

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            emailVerificationRepository: $verificationRepository
        );
        $result = $service->verifyCode('user@example.com', '123456');

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

    public function testVerifyCodeMarksRequestUsedAndUserAsVerifiedOnSuccess(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn([
            'id' => 16,
            'email_verified_at' => null,
        ]);

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->method('findActiveByUserId')->willReturn([
            'id' => 160,
            'attempts' => 1,
            'code_hash' => password_hash('123456', PASSWORD_DEFAULT),
        ]);
        $verificationRepository->expects($this->once())->method('markUsed')->with(160)->willReturn(true);

        $userWriteRepository = $this->createMock(UserWriteRepositoryInterface::class);
        $userWriteRepository->expects($this->once())->method('setEmailVerified')->with(16)->willReturn(true);

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            userWriteRepository: $userWriteRepository,
            emailVerificationRepository: $verificationRepository
        );
        $result = $service->verifyCode('user@example.com', '123456');

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

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

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

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

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

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->expects($this->never())->method('create');

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            emailVerificationRepository: $verificationRepository
        );
        $result = $service->resendVerification('missing@example.com');

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

    public function testResendVerificationReturnsAlreadyVerifiedWhenUserIsAlreadyVerified(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->method('findByEmail')->willReturn([
            'id' => 17,
            'email_verified_at' => gmdate('Y-m-d H:i:s'),
        ]);

        $service = $this->newService(userReadRepository: $userReadRepository);
        $result = $service->resendVerification('user@example.com');

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

    public function testResendVerificationDelegatesToSendVerificationForUnverifiedUsers(): void
    {
        $userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
        $userReadRepository->expects($this->once())->method('findByEmail')->with('user@example.com')->willReturn([
            'id' => 18,
            'email_verified_at' => null,
        ]);
        $userReadRepository->expects($this->once())->method('find')->with(18)->willReturn([
            'id' => 18,
            'email' => 'user@example.com',
            'locale' => 'en',
        ]);

        $verificationRepository = $this->createMock(EmailVerificationRepositoryInterface::class);
        $verificationRepository->expects($this->once())->method('invalidateForUser')->with(18);
        $verificationRepository->expects($this->once())->method('create')->willReturn(180);

        $mailService = $this->createMock(MailService::class);
        $mailService->expects($this->once())->method('sendTemplate')->willReturn(['ok' => true]);

        $service = $this->newService(
            userReadRepository: $userReadRepository,
            emailVerificationRepository: $verificationRepository,
            mailService: $mailService
        );
        $result = $service->resendVerification('user@example.com', 'en');

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

    private function newService(
        ?UserReadRepositoryInterface $userReadRepository = null,
        ?UserWriteRepositoryInterface $userWriteRepository = null,
        ?EmailVerificationRepositoryInterface $emailVerificationRepository = null,
        ?MailService $mailService = null
    ): EmailVerificationService {
        $userReadRepository ??= $this->createMock(UserReadRepositoryInterface::class);
        $userWriteRepository ??= $this->createMock(UserWriteRepositoryInterface::class);
        $emailVerificationRepository ??= $this->createMock(EmailVerificationRepositoryInterface::class);
        $mailService ??= $this->createMock(MailService::class);

        return new EmailVerificationService(
            $userReadRepository,
            $userWriteRepository,
            $emailVerificationRepository,
            $mailService
        );
    }
}
