<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Service\EncryptionService;
use OCP\Security\ICrypto;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for EncryptionService.
 *
 * Part of Issue #60.
 */
class EncryptionServiceTest extends TestCase {

    private EncryptionService $service;
    private ICrypto&MockObject $crypto;
    private LoggerInterface&MockObject $logger;

    protected function setUp(): void {
        parent::setUp();

        $this->crypto = $this->createMock(ICrypto::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new EncryptionService(
            $this->crypto,
            $this->logger
        );
    }

    // ── encrypt() ───────────────────────────────────────────────────

    public function testEncryptReturnsNullForNull(): void {
        $this->assertNull($this->service->encrypt(null));
    }

    public function testEncryptReturnsEmptyForEmpty(): void {
        $this->assertSame('', $this->service->encrypt(''));
    }

    public function testEncryptPrefixesWithMarker(): void {
        $this->crypto->method('encrypt')
            ->with('DE89370400440532013000')
            ->willReturn('encrypted_iban_data');

        $result = $this->service->encrypt('DE89370400440532013000');

        $this->assertSame('ENC:encrypted_iban_data', $result);
    }

    public function testEncryptThrowsExceptionOnError(): void {
        $this->crypto->method('encrypt')
            ->willThrowException(new \Exception('Crypto error'));

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Verschluesselung fehlgeschlagen');

        $this->service->encrypt('sensitive data');
    }

    // ── decrypt() ───────────────────────────────────────────────────

    public function testDecryptReturnsNullForNull(): void {
        $this->assertNull($this->service->decrypt(null));
    }

    public function testDecryptReturnsEmptyForEmpty(): void {
        $this->assertSame('', $this->service->decrypt(''));
    }

    public function testDecryptStripsPrefix(): void {
        $this->crypto->method('decrypt')
            ->with('encrypted_iban_data')
            ->willReturn('DE89370400440532013000');

        $result = $this->service->decrypt('ENC:encrypted_iban_data');

        $this->assertSame('DE89370400440532013000', $result);
    }

    public function testDecryptReturnsPlaintextIfNotEncrypted(): void {
        $result = $this->service->decrypt('plaintext value');

        $this->assertSame('plaintext value', $result,
            'Non-encrypted values should pass through (migration support)');
    }

    public function testDecryptReturnsPlaceholderOnError(): void {
        $this->crypto->method('decrypt')
            ->willThrowException(new \Exception('Decryption failed'));

        $result = $this->service->decrypt('ENC:corrupted_data');

        $this->assertSame('[Entschluesselung fehlgeschlagen]', $result);
    }

    // ── isEncrypted() ───────────────────────────────────────────────

    public function testIsEncryptedReturnsTrueForPrefixed(): void {
        $this->assertTrue($this->service->isEncrypted('ENC:some_data'));
    }

    public function testIsEncryptedReturnsFalseForPlaintext(): void {
        $this->assertFalse($this->service->isEncrypted('plain text'));
    }

    public function testIsEncryptedReturnsFalseForNull(): void {
        $this->assertFalse($this->service->isEncrypted(null));
    }

    public function testIsEncryptedReturnsFalseForEmpty(): void {
        $this->assertFalse($this->service->isEncrypted(''));
    }

    // ── decryptIfAuthorized() ───────────────────────────────────────

    public function testDecryptIfAuthorizedDecryptsWhenPermitted(): void {
        $this->crypto->method('decrypt')
            ->with('encrypted_data')
            ->willReturn('secret value');

        $result = $this->service->decryptIfAuthorized('ENC:encrypted_data', true);

        $this->assertSame('secret value', $result);
    }

    public function testDecryptIfAuthorizedRedactsWhenNotPermitted(): void {
        $result = $this->service->decryptIfAuthorized('ENC:encrypted_data', false);

        $this->assertSame('[Zugriff verweigert]', $result);
    }

    public function testDecryptIfAuthorizedReturnsNullForNull(): void {
        $this->assertNull($this->service->decryptIfAuthorized(null, true));
    }

    public function testDecryptIfAuthorizedReturnsEmptyForEmpty(): void {
        $this->assertSame('', $this->service->decryptIfAuthorized('', false));
    }

    // ── encryptIfNeeded() ───────────────────────────────────────────

    public function testEncryptIfNeededEncryptsPlaintext(): void {
        $this->crypto->method('encrypt')
            ->with('plain value')
            ->willReturn('encrypted_value');

        $result = $this->service->encryptIfNeeded('plain value');

        $this->assertSame('ENC:encrypted_value', $result);
    }

    public function testEncryptIfNeededSkipsAlreadyEncrypted(): void {
        // Should not call encrypt at all for already-encrypted values
        $this->crypto->expects($this->never())->method('encrypt');

        $result = $this->service->encryptIfNeeded('ENC:already_encrypted');

        $this->assertSame('ENC:already_encrypted', $result);
    }

    public function testEncryptIfNeededReturnsNullForNull(): void {
        $this->assertNull($this->service->encryptIfNeeded(null));
    }

    public function testEncryptIfNeededReturnsEmptyForEmpty(): void {
        $this->assertSame('', $this->service->encryptIfNeeded(''));
    }

    // ── Round-trip test ─────────────────────────────────────────────

    public function testEncryptDecryptRoundTrip(): void {
        $original = 'DE89370400440532013000';

        $this->crypto->method('encrypt')
            ->with($original)
            ->willReturn('mock_cipher');

        $this->crypto->method('decrypt')
            ->with('mock_cipher')
            ->willReturn($original);

        $encrypted = $this->service->encrypt($original);
        $decrypted = $this->service->decrypt($encrypted);

        $this->assertSame($original, $decrypted,
            'Encrypt followed by decrypt should return the original value');
    }
}
