<?php

declare(strict_types=1);

namespace App\Tests\Unit\Administration\Infrastructure\Api\Processor;

use ApiPlatform\Metadata\Post;
use App\Administration\Application\Command\RequestPasswordReset\RequestPasswordResetHandler;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Infrastructure\Api\Processor\RequestPasswordResetProcessor;
use App\Administration\Infrastructure\Api\Resource\RequestPasswordResetInput;
use App\Administration\Infrastructure\Api\Resource\RequestPasswordResetOutput;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryPasswordResetTokenRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
use App\Shared\Infrastructure\Tenant\TenantResolver;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\InMemoryStore;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;

/**
 * Tests for RequestPasswordResetProcessor.
 *
 * Key invariants:
 * - Always returns success to prevent email enumeration
 * - Rate limited by email (silent) and IP (visible)
 * - Tenant is resolved from request host
 */
final class RequestPasswordResetProcessorTest extends TestCase
{
    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

    private InMemoryUserRepository $userRepository;
    private InMemoryPasswordResetTokenRepository $tokenRepository;
    private RequestStack $requestStack;
    private TenantResolver $tenantResolver;
    private MessageBusInterface $eventBus;
    private Clock $clock;

    protected function setUp(): void
    {
        Request::setTrustedHosts(['^.*$']);
        $this->userRepository = new InMemoryUserRepository();
        $this->clock = new class implements Clock {
            public function now(): DateTimeImmutable
            {
                return new DateTimeImmutable('2026-02-15 10:00:00');
            }
        };
        $this->tokenRepository = new InMemoryPasswordResetTokenRepository($this->clock);
        $this->requestStack = new RequestStack();
        $this->tenantResolver = $this->createMock(TenantResolver::class);
        $this->eventBus = $this->createMock(MessageBusInterface::class);
    }

    #[Test]
    public function returnsSuccessOnValidRequest(): void
    {
        $this->saveUserInRepository('user@example.com');

        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'localhost']);
        $this->requestStack->push($request);

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $output = $processor->process($input, new Post());

        self::assertInstanceOf(RequestPasswordResetOutput::class, $output);
    }

    #[Test]
    public function alwaysReturnsSuccessEvenForNonexistentEmail(): void
    {
        // No user saved in repository

        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'localhost']);
        $this->requestStack->push($request);

        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'nonexistent@example.com';

        $output = $processor->process($input, new Post());

        self::assertInstanceOf(RequestPasswordResetOutput::class, $output);
    }

    #[Test]
    public function throwsWhenRequestNotAvailable(): void
    {
        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage('Request not available');

        $processor->process($input, new Post());
    }

    #[Test]
    public function throwsWhenIpRateLimitExceeded(): void
    {
        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'localhost']);
        $this->requestStack->push($request);

        $processor = $this->createProcessorWithRejectedIpLimiter();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $this->expectException(TooManyRequestsHttpException::class);

        $processor->process($input, new Post());
    }

    #[Test]
    public function returnsSuccessSilentlyWhenEmailRateLimitExceeded(): void
    {
        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'localhost']);
        $this->requestStack->push($request);

        // Handler should NOT dispatch events since rate limited
        $this->eventBus->expects(self::never())->method('dispatch');

        $processor = $this->createProcessorWithRejectedEmailLimiter();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $output = $processor->process($input, new Post());

        self::assertInstanceOf(RequestPasswordResetOutput::class, $output);
    }

    #[Test]
    public function resolvesTenantFromHost(): void
    {
        $this->saveUserInRepository('user@example.com');

        $tenantConfig = new TenantConfig(
            \App\Shared\Infrastructure\Tenant\TenantId::fromString(self::TENANT_ID),
            'ecole-alpha',
            'sqlite:///:memory:',
        );

        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'ecole-alpha.classeo.fr']);
        $this->requestStack->push($request);

        $this->tenantResolver->expects(self::once())
            ->method('resolve')
            ->with('ecole-alpha.classeo.fr')
            ->willReturn($tenantConfig);

        $this->eventBus->method('dispatch')
            ->willReturnCallback(static fn (object $msg) => new Envelope($msg));

        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $processor->process($input, new Post());
    }

    #[Test]
    public function usesDefaultTenantForLocalhost(): void
    {
        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'localhost']);
        $this->requestStack->push($request);

        $this->tenantResolver->expects(self::never())->method('resolve');

        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $processor->process($input, new Post());
    }

    #[Test]
    public function throwsWhenTenantNotFound(): void
    {
        $request = Request::create('/api/password/forgot', 'POST', server: ['HTTP_HOST' => 'unknown.classeo.fr']);
        $this->requestStack->push($request);

        $this->tenantResolver->method('resolve')
            ->willThrowException(TenantNotFoundException::withSubdomain('unknown'));

        $processor = $this->createProcessorWithAcceptingLimiters();

        $input = new RequestPasswordResetInput();
        $input->email = 'user@example.com';

        $this->expectException(BadRequestHttpException::class);
        $this->expectExceptionMessage('non reconnu');

        $processor->process($input, new Post());
    }

    private function saveUserInRepository(string $email): void
    {
        $user = User::creer(
            email: new Email($email),
            role: Role::PARENT,
            tenantId: TenantId::fromString(self::TENANT_ID),
            schoolName: 'Ecole Test',
            dateNaissance: null,
            createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'),
        );
        $this->userRepository->save($user);
    }

    private function createHandler(): RequestPasswordResetHandler
    {
        return new RequestPasswordResetHandler(
            $this->userRepository,
            $this->tokenRepository,
            $this->clock,
            $this->eventBus,
        );
    }

    private function createProcessorWithAcceptingLimiters(): RequestPasswordResetProcessor
    {
        return new RequestPasswordResetProcessor(
            $this->createHandler(),
            $this->requestStack,
            $this->tenantResolver,
            $this->createAcceptingLimiterFactory(),
            $this->createAcceptingLimiterFactory(),
        );
    }

    private function createProcessorWithRejectedIpLimiter(): RequestPasswordResetProcessor
    {
        return new RequestPasswordResetProcessor(
            $this->createHandler(),
            $this->requestStack,
            $this->tenantResolver,
            $this->createAcceptingLimiterFactory(),
            $this->createRejectedLimiterFactory(),
        );
    }

    private function createProcessorWithRejectedEmailLimiter(): RequestPasswordResetProcessor
    {
        return new RequestPasswordResetProcessor(
            $this->createHandler(),
            $this->requestStack,
            $this->tenantResolver,
            $this->createRejectedLimiterFactory(),
            $this->createAcceptingLimiterFactory(),
        );
    }

    private function createAcceptingLimiterFactory(): RateLimiterFactory
    {
        return new RateLimiterFactory(
            ['id' => 'test_accept', 'policy' => 'fixed_window', 'limit' => 1000, 'interval' => '1 hour'],
            new InMemoryStorage(),
            new LockFactory(new InMemoryStore()),
        );
    }

    /**
     * Creates a RateLimiterFactory that rejects ANY key on first consume.
     *
     * Uses sliding_window with limit=1, pre-consumed for every possible key
     * by wrapping the factory to exhaust its limit before returning.
     */
    private function createRejectedLimiterFactory(): RateLimiterFactory
    {
        $storage = new InMemoryStorage();
        $lockFactory = new LockFactory(new InMemoryStore());

        $factory = new RateLimiterFactory(
            ['id' => 'test_reject', 'policy' => 'fixed_window', 'limit' => 1, 'interval' => '1 hour'],
            $storage,
            $lockFactory,
        );

        // Pre-exhaust for the keys the processor will use:
        // IP limiter uses client IP (127.0.0.1 from Request::create)
        // Email limiter uses "{tenantId}:{email}"
        $factory->create('127.0.0.1')->consume();
        $factory->create(self::TENANT_ID . ':user@example.com')->consume();

        return $factory;
    }
}
