<?php
declare(strict_types=1);

namespace Tests\Unit;

use App\Core\Exceptions\AllegroOAuthException;
use App\Modules\Settings\AllegroIntegrationRepository;
use App\Modules\Settings\AllegroOAuthClient;
use App\Modules\Settings\AllegroTokenManager;
use DateInterval;
use DateTimeImmutable;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

final class AllegroTokenManagerTest extends TestCase
{
    private AllegroIntegrationRepository&MockObject $repository;
    private AllegroOAuthClient&MockObject $oauthClient;
    private AllegroTokenManager $manager;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(AllegroIntegrationRepository::class);
        $this->oauthClient = $this->createMock(AllegroOAuthClient::class);
        $this->manager = new AllegroTokenManager($this->repository, $this->oauthClient);
    }

    public function testResolveTokenReturnsCachedTokenWhenFresh(): void
    {
        $expiresAt = (new DateTimeImmutable('now'))
            ->add(new DateInterval('PT30M'))
            ->format('Y-m-d H:i:s');

        $this->repository
            ->method('getTokenCredentials')
            ->willReturn([
                'environment' => 'sandbox',
                'client_id' => 'cid',
                'client_secret' => 'cs',
                'refresh_token' => 'rt',
                'access_token' => 'fresh-token',
                'token_expires_at' => $expiresAt,
            ]);

        $this->oauthClient
            ->expects($this->never())
            ->method('refreshAccessToken');

        $result = $this->manager->resolveToken();

        $this->assertSame('fresh-token', $result[0]);
        $this->assertSame('sandbox', $result[1]);
    }

    public function testResolveTokenRefreshesWhenExpiresWithinFiveMinutes(): void
    {
        $expiresAt = (new DateTimeImmutable('now'))
            ->add(new DateInterval('PT3M'))
            ->format('Y-m-d H:i:s');

        $credentials = [
            'environment' => 'production',
            'client_id' => 'cid',
            'client_secret' => 'cs',
            'refresh_token' => 'rt',
            'access_token' => 'old-token',
            'token_expires_at' => $expiresAt,
        ];

        $this->repository
            ->method('getTokenCredentials')
            ->willReturnOnConsecutiveCalls(
                $credentials,
                $credentials,
                array_merge($credentials, ['access_token' => 'new-token'])
            );

        $this->oauthClient
            ->expects($this->once())
            ->method('refreshAccessToken')
            ->with('production', 'cid', 'cs', 'rt')
            ->willReturn([
                'access_token' => 'new-token',
                'refresh_token' => 'new-rt',
                'token_type' => 'Bearer',
                'scope' => '',
                'expires_in' => 3600,
            ]);

        $this->repository
            ->expects($this->once())
            ->method('saveTokens');

        $result = $this->manager->resolveToken();

        $this->assertSame('new-token', $result[0]);
        $this->assertSame('production', $result[1]);
    }

    public function testResolveTokenRefreshesWhenAlreadyExpired(): void
    {
        $expiresAt = (new DateTimeImmutable('now'))
            ->sub(new DateInterval('PT10M'))
            ->format('Y-m-d H:i:s');

        $credentials = [
            'environment' => 'sandbox',
            'client_id' => 'cid',
            'client_secret' => 'cs',
            'refresh_token' => 'rt',
            'access_token' => 'expired-token',
            'token_expires_at' => $expiresAt,
        ];

        $this->repository
            ->method('getTokenCredentials')
            ->willReturnOnConsecutiveCalls(
                $credentials,
                $credentials,
                array_merge($credentials, ['access_token' => 'refreshed-token'])
            );

        $this->oauthClient
            ->expects($this->once())
            ->method('refreshAccessToken')
            ->willReturn([
                'access_token' => 'refreshed-token',
                'refresh_token' => 'rt',
                'token_type' => 'Bearer',
                'scope' => '',
                'expires_in' => 3600,
            ]);

        $this->repository->expects($this->once())->method('saveTokens');

        $result = $this->manager->resolveToken();

        $this->assertSame('refreshed-token', $result[0]);
    }

    public function testResolveTokenThrowsWhenNoOAuthConfig(): void
    {
        $this->repository
            ->method('getTokenCredentials')
            ->willReturn(null);

        $this->expectException(AllegroOAuthException::class);
        $this->expectExceptionMessage('Brak polaczenia OAuth Allegro');

        $this->manager->resolveToken();
    }

    public function testResolveTokenRefreshesWhenAccessTokenEmpty(): void
    {
        $credentials = [
            'environment' => 'sandbox',
            'client_id' => 'cid',
            'client_secret' => 'cs',
            'refresh_token' => 'rt',
            'access_token' => '',
            'token_expires_at' => '',
        ];

        $this->repository
            ->method('getTokenCredentials')
            ->willReturnOnConsecutiveCalls(
                $credentials,
                $credentials,
                array_merge($credentials, ['access_token' => 'brand-new'])
            );

        $this->oauthClient
            ->expects($this->once())
            ->method('refreshAccessToken')
            ->willReturn([
                'access_token' => 'brand-new',
                'refresh_token' => 'rt',
                'token_type' => 'Bearer',
                'scope' => '',
                'expires_in' => 3600,
            ]);

        $this->repository->expects($this->once())->method('saveTokens');

        $result = $this->manager->resolveToken();

        $this->assertSame('brand-new', $result[0]);
    }

    public function testForceRefreshReReadsTokenFromRepository(): void
    {
        $expiresAt = (new DateTimeImmutable('now'))
            ->sub(new DateInterval('PT1M'))
            ->format('Y-m-d H:i:s');

        $credentials = [
            'environment' => 'sandbox',
            'client_id' => 'cid',
            'client_secret' => 'cs',
            'refresh_token' => 'rt',
            'access_token' => 'stale',
            'token_expires_at' => $expiresAt,
        ];

        $this->repository
            ->expects($this->exactly(3))
            ->method('getTokenCredentials')
            ->willReturnOnConsecutiveCalls(
                $credentials,
                $credentials,
                array_merge($credentials, ['access_token' => 'from-db'])
            );

        $this->oauthClient
            ->method('refreshAccessToken')
            ->willReturn([
                'access_token' => 'api-returned',
                'refresh_token' => 'rt',
                'token_type' => 'Bearer',
                'scope' => '',
                'expires_in' => 3600,
            ]);

        $this->repository->expects($this->once())->method('saveTokens');

        $result = $this->manager->resolveToken();

        // Token comes from re-read (3rd call), not directly from API response
        $this->assertSame('from-db', $result[0]);
    }

    public function testResolveTokenRefreshesWhenExpiresAtInvalidFormat(): void
    {
        $credentials = [
            'environment' => 'sandbox',
            'client_id' => 'cid',
            'client_secret' => 'cs',
            'refresh_token' => 'rt',
            'access_token' => 'some-token',
            'token_expires_at' => 'not-a-date',
        ];

        $this->repository
            ->method('getTokenCredentials')
            ->willReturnOnConsecutiveCalls(
                $credentials,
                $credentials,
                array_merge($credentials, ['access_token' => 'refreshed'])
            );

        $this->oauthClient
            ->expects($this->once())
            ->method('refreshAccessToken')
            ->willReturn([
                'access_token' => 'refreshed',
                'refresh_token' => 'rt',
                'token_type' => 'Bearer',
                'scope' => '',
                'expires_in' => 3600,
            ]);

        $this->repository->expects($this->once())->method('saveTokens');

        $result = $this->manager->resolveToken();
        $this->assertSame('refreshed', $result[0]);
    }
}
