<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\EncryptedExportService;
use OCA\Mitgliederverwaltung\Service\PermissionService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for EncryptedExportService.
 *
 * Part of Issue #48.
 */
class EncryptedExportServiceTest extends TestCase {

    private EncryptedExportService $service;
    private AuditService&MockObject $auditService;
    private PermissionService&MockObject $permissionService;
    private IUserSession&MockObject $userSession;
    private LoggerInterface&MockObject $logger;

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

        $this->auditService = $this->createMock(AuditService::class);
        $this->permissionService = $this->createMock(PermissionService::class);
        $this->userSession = $this->createMock(IUserSession::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        // Default: user is logged in
        $user = $this->createMock(IUser::class);
        $user->method('getUID')->willReturn('testuser');
        $this->userSession->method('getUser')->willReturn($user);

        $this->service = new EncryptedExportService(
            $this->auditService,
            $this->permissionService,
            $this->userSession,
            $this->logger
        );
    }

    // ── createEncryptedExport() ─────────────────────────────────────

    public function testCreateEncryptedExportReturnsZipContent(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $this->auditService->expects($this->once())
            ->method('logCreate');

        $result = $this->service->createEncryptedExport(
            'Name;Vorname\nMueller;Hans',
            'Mitgliederliste_2026-04-07.csv',
            'sicheresPasswort123',
            'Mitgliederliste'
        );

        // ZIP file starts with PK magic bytes
        $this->assertStringStartsWith("PK", $result['content'],
            'Result should be a valid ZIP file');

        // Filename matches pattern
        $this->assertStringStartsWith('Export_Mitgliederliste_', $result['filename']);
        $this->assertStringEndsWith('.zip', $result['filename']);
    }

    public function testCreateEncryptedExportContainsInnerFile(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $csvContent = "Name;Vorname\r\nMueller;Hans\r\n";
        $innerName = 'test_export.csv';

        $result = $this->service->createEncryptedExport(
            $csvContent,
            $innerName,
            'sicheresPasswort123',
            'Test'
        );

        // Write ZIP to temp file and verify structure
        $tmpFile = tempnam(sys_get_temp_dir(), 'test_zip_');
        file_put_contents($tmpFile, $result['content']);

        $zip = new \ZipArchive();
        $zip->open($tmpFile);
        $zip->setPassword('sicheresPasswort123');

        $this->assertSame(1, $zip->numFiles, 'ZIP should contain exactly one file');
        $this->assertSame($innerName, $zip->getNameIndex(0),
            'Inner file should have the specified name');

        $zip->close();
        unlink($tmpFile);
    }

    public function testCreateEncryptedExportLogsAudit(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $this->auditService->expects($this->once())
            ->method('logCreate')
            ->with(
                $this->callback(function (array $data): bool {
                    return $data['export_type'] === 'Bankverbindungen'
                        && $data['encrypted'] === 'true'
                        && $data['user'] === 'testuser';
                }),
                'encrypted_export',
                0
            );

        $this->service->createEncryptedExport(
            'test content',
            'test.csv',
            'sicheresPasswort123',
            'Bankverbindungen'
        );
    }

    // ── Password validation ─────────────────────────────────────────

    public function testRejectsShortPassword(): void {
        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('mindestens 8 Zeichen');

        $this->service->createEncryptedExport(
            'content',
            'file.csv',
            'short',
            'Test'
        );
    }

    public function testRejectsEmptyPassword(): void {
        $this->expectException(ValidationException::class);

        $this->service->createEncryptedExport(
            'content',
            'file.csv',
            '',
            'Test'
        );
    }

    // ── assertBankingExportPermission() ─────────────────────────────

    public function testAssertBankingPermissionPassesWhenAllowed(): void {
        $this->permissionService->method('canSeeBanking')
            ->with('testuser')
            ->willReturn(true);

        // Should not throw
        $this->service->assertBankingExportPermission();
        $this->assertTrue(true);
    }

    public function testAssertBankingPermissionThrowsWhenDenied(): void {
        $this->permissionService->method('canSeeBanking')
            ->with('testuser')
            ->willReturn(false);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('Bankdaten-Sichtbarkeit');

        $this->service->assertBankingExportPermission();
    }

    // ── createEncryptedExport additional tests ─────────────────────

    public function testCreateEncryptedExportFilenameContainsExportType(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $result = $this->service->createEncryptedExport(
            'test content',
            'inner.csv',
            'sicheresPasswort123',
            'Bankverbindungen'
        );

        $this->assertStringContainsString('Bankverbindungen', $result['filename']);
    }

    public function testCreateEncryptedExportWithNoUserSession(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $userSession = $this->createMock(IUserSession::class);
        $userSession->method('getUser')->willReturn(null);

        $service = new EncryptedExportService(
            $this->auditService,
            $this->permissionService,
            $userSession,
            $this->logger
        );

        $this->auditService->expects($this->once())
            ->method('logCreate')
            ->with(
                $this->callback(function (array $data): bool {
                    return $data['user'] === 'system';
                }),
                'encrypted_export',
                0
            );

        $result = $service->createEncryptedExport(
            'content',
            'file.csv',
            'sicheresPasswort123',
            'Test'
        );

        $this->assertStringStartsWith('PK', $result['content']);
    }

    public function testRejectsPasswordExactly7Chars(): void {
        $this->expectException(ValidationException::class);

        $this->service->createEncryptedExport(
            'content',
            'file.csv',
            '1234567',
            'Test'
        );
    }

    public function testAcceptsPasswordExactly8Chars(): void {
        if (!class_exists(\ZipArchive::class)) {
            $this->markTestSkipped('ZipArchive not available');
        }

        $result = $this->service->createEncryptedExport(
            'content',
            'file.csv',
            '12345678',
            'Test'
        );

        $this->assertStringStartsWith('PK', $result['content']);
    }

    public function testAssertBankingPermissionWithNoUserSession(): void {
        $userSession = $this->createMock(IUserSession::class);
        $userSession->method('getUser')->willReturn(null);

        $service = new EncryptedExportService(
            $this->auditService,
            $this->permissionService,
            $userSession,
            $this->logger
        );

        $this->permissionService->method('canSeeBanking')
            ->with('system')
            ->willReturn(true);

        // Should not throw
        $service->assertBankingExportPermission();
        $this->assertTrue(true);
    }

    // ── getEncryptionMethod() via reflection ──────────────────────

    public function testGetEncryptionMethodReturnsAes256WhenAvailable(): void {
        $method = new \ReflectionMethod(EncryptedExportService::class, 'getEncryptionMethod');
        $method->setAccessible(true);

        $result = $method->invoke($this->service);

        // AES-256 should be available in modern PHP
        if (defined('ZipArchive::EM_AES_256')) {
            $this->assertEquals(\ZipArchive::EM_AES_256, $result);
        } else {
            $this->assertIsInt($result);
        }
    }

    // ── validatePassword() via reflection ─────────────────────────

    public function testValidatePasswordAccepts8Chars(): void {
        $method = new \ReflectionMethod(EncryptedExportService::class, 'validatePassword');
        $method->setAccessible(true);

        // Should not throw for 8 char password
        $method->invoke($this->service, '12345678');
        $this->assertTrue(true);
    }

    public function testValidatePasswordAcceptsLongPassword(): void {
        $method = new \ReflectionMethod(EncryptedExportService::class, 'validatePassword');
        $method->setAccessible(true);

        $method->invoke($this->service, 'a_very_long_and_secure_password_that_is_way_longer_than_8_chars');
        $this->assertTrue(true);
    }

    public function testValidatePasswordRejects7Chars(): void {
        $method = new \ReflectionMethod(EncryptedExportService::class, 'validatePassword');
        $method->setAccessible(true);

        $this->expectException(ValidationException::class);
        $method->invoke($this->service, '1234567');
    }

    // ── getCurrentUserId() via reflection ─────────────────────────

    public function testGetCurrentUserIdReturnsUserId(): void {
        $method = new \ReflectionMethod(EncryptedExportService::class, 'getCurrentUserId');
        $method->setAccessible(true);

        $result = $method->invoke($this->service);

        $this->assertEquals('testuser', $result);
    }

    public function testGetCurrentUserIdReturnsSystemWhenNoUser(): void {
        $userSession = $this->createMock(IUserSession::class);
        $userSession->method('getUser')->willReturn(null);

        $service = new EncryptedExportService(
            $this->auditService,
            $this->permissionService,
            $userSession,
            $this->logger
        );

        $method = new \ReflectionMethod(EncryptedExportService::class, 'getCurrentUserId');
        $method->setAccessible(true);

        $result = $method->invoke($service);

        $this->assertEquals('system', $result);
    }
}
