<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Tests\Translation;

use Ferienpass\CmsBundle\Translation\DeeplTranslator;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

class MachineTranslatorTest extends TestCase
{
    public function testGetTargetLang(): void
    {
        $httpClient = $this->createStub(HttpClientInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $translator = new DeeplTranslator($httpClient, $cache);

        $this->assertSame('EN-US', $translator->getTargetLang('en'));
        $this->assertSame('EN-US', $translator->getTargetLang('EN'));
        $this->assertSame('FR', $translator->getTargetLang('fr'));
    }

    public function testTranslateSuccess(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated text'],
            ],
        ]);

        $httpClient->expects($this->once())
            ->method('request')
            ->with('POST', '/v2/translate', $this->callback(static function (array $options) {
                return ['content'] === $options['body']['text']
                    && 'EN-US' === $options['body']['target_lang']
                    && 'DE' === $options['body']['source_lang']
                    && 'html' === $options['body']['tag_handling'];
            }))
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        $this->assertSame('translated text', $translator->translate('content', 'EN-US'));
    }

    public function testTranslateFreeApiUrl(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated text'],
            ],
        ]);

        $httpClient->expects($this->once())
            ->method('request')
            ->with('POST', '/v2/translate')
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        $translator->translate('content', 'EN-US');
    }

    public function testTranslateWithPii(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated <span id="deepl-ignore-0"></span> and <span id="deepl-ignore-1"></span> text'],
            ],
        ]);

        $content = 'some <div translate="no|pii">private</div> and <span translate="no|pii">secret</span> text';

        $httpClient->expects($this->once())
            ->method('request')
            ->with('POST', $this->anything(), $this->callback(static function (array $options) {
                // The content sent to DeepL should NOT contain "private" or "secret"
                return !str_contains($options['body']['text'][0], 'private')
                    && !str_contains($options['body']['text'][0], 'secret')
                    && str_contains($options['body']['text'][0], 'deepl-ignore-0')
                    && str_contains($options['body']['text'][0], 'deepl-ignore-1');
            }))
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        $result = $translator->translate($content, 'EN-US');

        $this->assertSame('translated <div translate="no|pii">private</div> and <span translate="no|pii">secret</span> text', $result);
    }

    public function testTranslateWithNestedPii(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated <span id="deepl-ignore-0"></span>'],
            ],
        ]);

        $content = '<div translate="no|pii">Outer <div translate="no|pii">Inner</div> End</div>';

        $httpClient->expects($this->once())
            ->method('request')
            ->with('POST', $this->anything(), $this->callback(static function (array $options) {
                return str_contains($options['body']['text'][0], 'deepl-ignore-0')
                    && !str_contains($options['body']['text'][0], 'Outer');
            }))
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        $result = $translator->translate($content, 'EN-US');

        $this->assertSame('translated <div translate="no|pii">Outer <div translate="no|pii">Inner</div> End</div>', $result);
    }

    public function testTranslateWithInvalidHtml(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated <span id="deepl-ignore-0"></span>No closing tag'],
            ],
        ]);

        $content = '<p translate="no|pii">No closing tag';

        $httpClient->expects($this->once())
            ->method('request')
            ->with('POST', $this->anything(), $this->callback(static function (array $options) {
                return str_contains($options['body']['text'][0], 'deepl-ignore-0')
                    && !str_contains($options['body']['text'][0], 'translate="no|pii"');
            }))
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        $result = $translator->translate($content, 'EN-US');

        // Note: the regex version just replaces the opening tag if no closing tag is found
        $this->assertSame('translated <p translate="no|pii">No closing tag', $result);
    }

    public function testTranslateReturnsCachedResultWhenHit(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(true);
        $cacheItem->method('get')->willReturn('cached translation');

        // HTTP client should NOT be called when cache hits
        $httpClient->expects($this->never())->method('request');

        $translator = new DeeplTranslator($httpClient, $cache);

        $this->assertSame('cached translation', $translator->translate('content', 'EN-US'));
    }

    public function testTranslateReturnsNullOnHttpException(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);

        $httpClient->expects($this->once())
            ->method('request')
            ->willThrowException(new \RuntimeException('Network error'));

        $translator = new DeeplTranslator($httpClient, $cache);

        $this->assertNull($translator->translate('content', 'EN-US'));
    }

    public function testTranslateWithSelfClosingPiiTag(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);
        $cacheItem->method('set')->willReturnSelf();
        $cacheItem->method('expiresAfter')->willReturnSelf();

        $response->method('toArray')->willReturn([
            'translations' => [
                ['text' => 'translated <span id="deepl-ignore-0"></span> content'],
            ],
        ]);

        $httpClient->expects($this->once())
            ->method('request')
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);

        // Self-closing void element like <br>
        $content = 'before <br translate="no|pii" /> after';
        $result = $translator->translate($content, 'EN-US');

        // The br tag should be restored
        $this->assertStringContainsString('<br', $result);
    }

    public function testTranslateReturnsNullWhenApiReturnsNoTranslation(): void
    {
        $httpClient = $this->createMock(HttpClientInterface::class);
        $response = $this->createStub(ResponseInterface::class);
        $cache = $this->createStub(CacheItemPoolInterface::class);
        $cacheItem = $this->createStub(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);

        // API returns response without any translations
        $response->method('toArray')->willReturn(['translations' => []]);

        $httpClient->expects($this->once())
            ->method('request')
            ->willReturn($response);

        $translator = new DeeplTranslator($httpClient, $cache);
        $result = $translator->translate('content', 'EN-US');

        $this->assertNull($result);
    }
}
