<?php

namespace MintyPHP\Tests\Unit\Support;

use MintyPHP\Support\Crypto;
use PHPUnit\Framework\TestCase;

final class CryptoTest extends TestCase
{
    protected function setUp(): void
    {
        if (!defined('APP_CRYPTO_KEY')) {
            define('APP_CRYPTO_KEY', str_repeat('ab', 32));
        }
    }

    public function testIsConfiguredReturnsTrueWhenKeyIsSet(): void
    {
        self::assertTrue(Crypto::isConfigured());
    }

    public function testEncryptDecryptRoundTrip(): void
    {
        $plaintext = 'client-secret-for-sso';
        $encrypted = Crypto::encryptString($plaintext);

        self::assertNotSame($plaintext, $encrypted);
        self::assertSame($plaintext, Crypto::decryptString($encrypted));
    }

    public function testEncryptProducesDifferentCiphertextEachTime(): void
    {
        $plaintext = 'same-settings-value';

        self::assertNotSame(
            Crypto::encryptString($plaintext),
            Crypto::encryptString($plaintext)
        );
    }

    public function testDecryptThrowsOnInvalidOrTamperedPayload(): void
    {
        $encrypted = Crypto::encryptString('secret');
        $tampered = substr($encrypted, 0, 5) . 'Z' . substr($encrypted, 6);

        try {
            Crypto::decryptString('garbage-data');
            self::fail('Expected invalid payload to throw.');
        } catch (\RuntimeException) {
        }

        try {
            Crypto::decryptString(base64_encode('{}'));
            self::fail('Expected malformed payload to throw.');
        } catch (\RuntimeException) {
        }

        $this->expectException(\RuntimeException::class);
        Crypto::decryptString($tampered);
    }

    public function testEncryptEmptyStringProducesMalformedPayloadOnDecrypt(): void
    {
        $encrypted = Crypto::encryptString('');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('malformed');
        Crypto::decryptString($encrypted);
    }

    public function testDecryptThrowsOnEmptyString(): void
    {
        $this->expectException(\RuntimeException::class);
        Crypto::decryptString('');
    }

    public function testDecryptThrowsOnMissingPayloadFields(): void
    {
        // Valid base64 wrapping valid JSON, but missing required fields.
        $payload = base64_encode(json_encode(['v' => 1]));

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('malformed');
        Crypto::decryptString($payload);
    }

    public function testRoundTripWithSpecialCharacters(): void
    {
        $values = [
            'unicode: Ünterström ñ 日本語',
            "newlines:\nand\ttabs",
            'json-like: {"key": "value"}',
            str_repeat('x', 10000),
        ];

        foreach ($values as $plaintext) {
            $encrypted = Crypto::encryptString($plaintext);
            self::assertSame($plaintext, Crypto::decryptString($encrypted), "Round-trip failed for: " . substr($plaintext, 0, 40));
        }
    }

    public function testRoundTripWithBinaryLikeContent(): void
    {
        $plaintext = "null-bytes:\x00\x01\x02 and high-bytes:\xFE\xFF";
        $encrypted = Crypto::encryptString($plaintext);

        self::assertSame($plaintext, Crypto::decryptString($encrypted));
    }

    public function testDecryptThrowsOnNonBase64Input(): void
    {
        $this->expectException(\RuntimeException::class);
        Crypto::decryptString('not!valid@base64###');
    }

    public function testDecryptThrowsOnTruncatedPayload(): void
    {
        $encrypted = Crypto::encryptString('secret');
        $truncated = substr($encrypted, 0, (int) (strlen($encrypted) / 2));

        $this->expectException(\RuntimeException::class);
        Crypto::decryptString($truncated);
    }
}
