<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Entity\Image;
use App\Entity\Token;
use App\Entity\User;
use App\Enum\TokenType;
use App\Repository\TokenRepository;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class TokenServiceTest extends TestCase
{
    private function makeService(): array
    {
        $repo    = $this->createStub(TokenRepository::class);
        $em      = $this->createStub(EntityManagerInterface::class);
        $service = new TokenService($repo, $em);
        return [$service, $repo, $em];
    }

    private function makeServiceWithMockEm(): array
    {
        $repo    = $this->createStub(TokenRepository::class);
        $em      = $this->createMock(EntityManagerInterface::class);
        $service = new TokenService($repo, $em);
        return [$service, $repo, $em];
    }

    private function makeImage(): Image
    {
        $user  = new User();
        $image = new Image();
        $image->setUser($user)->setOriginalFilename('test.jpg')->setStoragePath('x');
        return $image;
    }

    public function test_issue_returns_token_with_correct_type(): void
    {
        [$service, , $em] = $this->makeServiceWithMockEm();
        $em->expects($this->once())->method('persist');

        $token = $service->issue(TokenType::ShareApprove, $this->makeImage(), null, 'a@b.com', 7);

        $this->assertSame(TokenType::ShareApprove, $token->getType());
    }

    public function test_issue_expiry_is_in_the_future(): void
    {
        [$service] = $this->makeService();

        $token = $service->issue(TokenType::ShareApprove, $this->makeImage(), null, null, 7);

        $this->assertGreaterThan(new \DateTimeImmutable(), $token->getExpiresAt());
    }

    public function test_issue_calls_em_persist(): void
    {
        [$service, , $em] = $this->makeServiceWithMockEm();
        $em->expects($this->once())->method('persist')->with($this->isInstanceOf(Token::class));

        $service->issue(TokenType::ShareApprove, $this->makeImage(), null, 'a@b.com', 7);
    }

    public function test_consume_calls_token_consume(): void
    {
        $repo    = $this->createStub(TokenRepository::class);
        $em      = $this->createMock(EntityManagerInterface::class);
        $service = new TokenService($repo, $em);

        /** @var Token&MockObject $token */
        $token = $this->createMock(Token::class);
        $token->expects($this->once())->method('consume');
        $em->expects($this->once())->method('flush');

        $repo->method('findValidToken')->willReturn($token);

        $service->consume('some-uuid', TokenType::ShareApprove);
    }

    public function test_consume_calls_em_flush(): void
    {
        $repo    = $this->createStub(TokenRepository::class);
        $em      = $this->createMock(EntityManagerInterface::class);
        $service = new TokenService($repo, $em);

        $token = $this->createStub(Token::class);
        $em->expects($this->once())->method('flush');

        $repo->method('findValidToken')->willReturn($token);

        $service->consume('some-uuid', TokenType::ShareApprove);
    }

    public function test_consume_returns_the_token(): void
    {
        $repo    = $this->createStub(TokenRepository::class);
        $em      = $this->createStub(EntityManagerInterface::class);
        $service = new TokenService($repo, $em);

        $token = $this->createStub(Token::class);
        $repo->method('findValidToken')->willReturn($token);

        $result = $service->consume('some-uuid', TokenType::ShareApprove);

        $this->assertSame($token, $result);
    }

    public function test_consume_throws_when_token_not_found(): void
    {
        [$service, $repo] = $this->makeService();
        $repo->method('findValidToken')->willReturn(null);

        $this->expectException(\RuntimeException::class);

        $service->consume('invalid-uuid', TokenType::ShareApprove);
    }

    /** T-05: expired token — repo already excludes it, returns null → RuntimeException */
    public function test_consume_throws_for_expired_token(): void
    {
        [$service, $repo] = $this->makeService();
        $repo->method('findValidToken')->willReturn(null);

        $this->expectException(\RuntimeException::class);

        $service->consume('expired-uuid', TokenType::ShareApprove);
    }

    /** T-06: already-used token — repo excludes usedAt IS NOT NULL, returns null → RuntimeException */
    public function test_consume_throws_for_already_used_token(): void
    {
        [$service, $repo] = $this->makeService();
        $repo->method('findValidToken')->willReturn(null);

        $this->expectException(\RuntimeException::class);

        $service->consume('used-uuid', TokenType::ShareApprove);
    }

    /** T-07: type mismatch — repo WHERE clause filters type, returns null → RuntimeException */
    public function test_consume_throws_for_type_mismatch(): void
    {
        [$service, $repo] = $this->makeService();
        $repo->method('findValidToken')->willReturn(null);

        $this->expectException(\RuntimeException::class);

        // UUID issued as ShareDecline but consumed as ShareApprove
        $service->consume('some-uuid', TokenType::ShareApprove);
    }
}
