<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Lager;
use OCA\Mitgliederverwaltung\Db\LagerMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Service\FileLinkService;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for FileLinkService path computation and settings.
 *
 * Part of Issue #52.
 */
class FileLinkServiceTest extends TestCase {

    private FileLinkService $service;
    private IRootFolder|MockObject $rootFolder;
    private IConfig|MockObject $config;
    private MemberMapper|MockObject $memberMapper;
    private LagerMapper|MockObject $lagerMapper;
    private LoggerInterface|MockObject $logger;

    protected function setUp(): void {
        $this->rootFolder = $this->createMock(IRootFolder::class);
        $this->config = $this->createMock(IConfig::class);
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->lagerMapper = $this->createMock(LagerMapper::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new FileLinkService(
            $this->rootFolder,
            $this->config,
            $this->memberMapper,
            $this->lagerMapper,
            $this->logger,
            'testuser'
        );
    }

    // ── Path computation tests ──────────────────────────────────────

    public function testGetMemberFolderPathDefaultPattern(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');

        $path = $this->service->getMemberFolderPath($member);

        $this->assertEquals('/Verein/Mitglieder/Mueller_Max', $path);
    }

    public function testGetMemberFolderPathCustomPattern(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Dokumente/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Vorname}_{Nachname}'],
            ]);

        $member = new Member();
        $member->setVorname('Anna');
        $member->setNachname('Schmidt');

        $path = $this->service->getMemberFolderPath($member);

        $this->assertEquals('/Dokumente/Mitglieder/Anna_Schmidt', $path);
    }

    public function testGetMemberFolderPathSanitizesSpecialCharacters(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Hans/Peter');
        $member->setNachname('O\'Brien');

        $path = $this->service->getMemberFolderPath($member);

        // Slashes should be replaced, apostrophes kept (they're valid in paths)
        $this->assertStringNotContainsString('//', $path . '_end'); // No double slashes from sanitization
        $this->assertStringContainsString('O\'Brien', $path);
        $this->assertStringContainsString('Hans_Peter', $path);
    }

    public function testGetMemberFolderPathTrimsTrailingSlash(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder///'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Test');
        $member->setNachname('User');

        $path = $this->service->getMemberFolderPath($member);

        $this->assertEquals('/Verein/Mitglieder/User_Test', $path);
    }

    public function testGetLagerFolderPath(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_lager_base_path', '/Verein/Lager/', '/Verein/Lager/'],
            ]);

        $lager = new Lager();
        $lager->setName('Sommerlager 2026');

        $path = $this->service->getLagerFolderPath($lager);

        $this->assertEquals('/Verein/Lager/Sommerlager 2026', $path);
    }

    // ── Settings tests ──────────────────────────────────────────────

    public function testGetSettingsReturnsDefaults(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
                ['mitgliederverwaltung', 'file_auto_create', 'false', 'false'],
                ['mitgliederverwaltung', 'file_lager_base_path', '/Verein/Lager/', '/Verein/Lager/'],
            ]);

        $settings = $this->service->getSettings();

        $this->assertEquals('/Verein/Mitglieder/', $settings['basePath']);
        $this->assertEquals('{Nachname}_{Vorname}', $settings['subfolderPattern']);
        $this->assertFalse($settings['autoCreate']);
        $this->assertEquals('/Verein/Lager/', $settings['lagerBasePath']);
    }

    public function testIsAutoCreateEnabled(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_auto_create', 'false', 'true'],
            ]);

        $this->assertTrue($this->service->isAutoCreateEnabled());
    }

    public function testIsAutoCreateDisabledByDefault(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_auto_create', 'false', 'false'],
            ]);

        $this->assertFalse($this->service->isAutoCreateEnabled());
    }

    public function testUpdateSettingsCallsSetAppValue(): void {
        $calls = [];
        $this->config->expects($this->exactly(4))
            ->method('setAppValue')
            ->willReturnCallback(function (string $app, string $key, string $value) use (&$calls) {
                $calls[] = [$app, $key, $value];
            });

        $this->service->updateSettings([
            'basePath' => '/Custom/Path/',
            'subfolderPattern' => '{Vorname}_{Nachname}',
            'autoCreate' => true,
            'lagerBasePath' => '/Custom/Lager/',
        ]);

        $this->assertContains(['mitgliederverwaltung', 'file_base_path', '/Custom/Path/'], $calls);
        $this->assertContains(['mitgliederverwaltung', 'file_subfolder_pattern', '{Vorname}_{Nachname}'], $calls);
        $this->assertContains(['mitgliederverwaltung', 'file_auto_create', 'true'], $calls);
        $this->assertContains(['mitgliederverwaltung', 'file_lager_base_path', '/Custom/Lager/'], $calls);
    }

    public function testUpdateSettingsPartialUpdate(): void {
        $this->config->expects($this->once())
            ->method('setAppValue')
            ->with('mitgliederverwaltung', 'file_base_path', '/New/Path/');

        $this->service->updateSettings([
            'basePath' => '/New/Path/',
        ]);
    }

    public function testUpdateSettingsAutoCreateFalse(): void {
        $this->config->expects($this->once())
            ->method('setAppValue')
            ->with('mitgliederverwaltung', 'file_auto_create', 'false');

        $this->service->updateSettings([
            'autoCreate' => false,
        ]);
    }

    // ── ensureMemberFolder() ──────────────────────────────────────

    public function testEnsureMemberFolderExisting(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')->willReturn($this->createMock(\OCP\Files\Folder::class));
        $this->rootFolder->method('getUserFolder')->with('testuser')->willReturn($userFolder);

        $result = $this->service->ensureMemberFolder($member);

        $this->assertTrue($result);
    }

    public function testEnsureMemberFolderAutoCreateEnabled(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
                ['mitgliederverwaltung', 'file_auto_create', 'false', 'true'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')
            ->willThrowException(new \OCP\Files\NotFoundException());
        $userFolder->expects($this->once())->method('newFolder');
        $this->rootFolder->method('getUserFolder')->with('testuser')->willReturn($userFolder);

        $result = $this->service->ensureMemberFolder($member);

        $this->assertTrue($result);
    }

    public function testEnsureMemberFolderAutoCreateDisabled(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
                ['mitgliederverwaltung', 'file_auto_create', 'false', 'false'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')
            ->willThrowException(new \OCP\Files\NotFoundException());
        $this->rootFolder->method('getUserFolder')->with('testuser')->willReturn($userFolder);

        $result = $this->service->ensureMemberFolder($member);

        $this->assertFalse($result);
    }

    public function testEnsureMemberFolderNoUserContext(): void {
        $serviceNoUser = new FileLinkService(
            $this->rootFolder,
            $this->config,
            $this->memberMapper,
            $this->lagerMapper,
            $this->logger,
            null
        );

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');

        $result = $serviceNoUser->ensureMemberFolder($member);

        $this->assertFalse($result);
    }

    public function testEnsureMemberFolderHandlesException(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->rootFolder->method('getUserFolder')
            ->willThrowException(new \RuntimeException('Storage error'));

        $result = $this->service->ensureMemberFolder($member);

        $this->assertFalse($result);
    }

    // ── getMemberFiles() ──────────────────────────────────────────

    public function testGetMemberFilesExisting(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $file = $this->createMock(\OCP\Files\File::class);
        $file->method('getName')->willReturn('document.pdf');
        $file->method('getSize')->willReturn(1024);
        $file->method('getMTime')->willReturn(1700000000);
        $file->method('getMimetype')->willReturn('application/pdf');
        $file->method('getPath')->willReturn('/path/document.pdf');
        $file->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FILE);

        $folder = $this->createMock(\OCP\Files\Folder::class);
        $folder->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FOLDER);
        $folder->method('getDirectoryListing')->willReturn([$file]);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')->willReturn($folder);
        $this->rootFolder->method('getUserFolder')->with('testuser')->willReturn($userFolder);

        $result = $this->service->getMemberFiles($member);

        $this->assertTrue($result['exists']);
        $this->assertCount(1, $result['files']);
        $this->assertEquals('document.pdf', $result['files'][0]['name']);
    }

    public function testGetMemberFilesNotFound(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')
            ->willThrowException(new \OCP\Files\NotFoundException());
        $this->rootFolder->method('getUserFolder')->with('testuser')->willReturn($userFolder);

        $result = $this->service->getMemberFiles($member);

        $this->assertFalse($result['exists']);
        $this->assertEmpty($result['files']);
    }

    public function testGetMemberFilesHandlesGenericException(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->rootFolder->method('getUserFolder')
            ->willThrowException(new \RuntimeException('Storage error'));

        $result = $this->service->getMemberFiles($member);

        $this->assertFalse($result['exists']);
    }

    // ── getMemberFilesById() ──────────────────────────────────────

    public function testGetMemberFilesById(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')
            ->willThrowException(new \OCP\Files\NotFoundException());
        $this->rootFolder->method('getUserFolder')->willReturn($userFolder);

        $result = $this->service->getMemberFilesById(1);

        $this->assertFalse($result['exists']);
    }

    // ── ensureMemberFolderById() ──────────────────────────────────

    public function testEnsureMemberFolderById(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Nachname}_{Vorname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')->willReturn($this->createMock(\OCP\Files\Folder::class));
        $this->rootFolder->method('getUserFolder')->willReturn($userFolder);

        $result = $this->service->ensureMemberFolderById(1);

        $this->assertTrue($result['exists']);
        $this->assertStringContainsString('Mueller_Max', $result['path']);
    }

    // ── listFolderContents() ──────────────────────────────────────

    public function testListFolderContentsNoUserContext(): void {
        $serviceNoUser = new FileLinkService(
            $this->rootFolder,
            $this->config,
            $this->memberMapper,
            $this->lagerMapper,
            $this->logger,
            null
        );

        $this->expectException(\RuntimeException::class);
        $serviceNoUser->listFolderContents('/some/path');
    }

    public function testListFolderContentsNotAFolder(): void {
        $file = $this->createMock(\OCP\Files\File::class);
        $file->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FILE);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')->willReturn($file);
        $this->rootFolder->method('getUserFolder')->willReturn($userFolder);

        $this->expectException(\RuntimeException::class);
        $this->service->listFolderContents('/some/path');
    }

    public function testListFolderContentsSortsFoldersFirst(): void {
        $file = $this->createMock(\OCP\Files\File::class);
        $file->method('getName')->willReturn('b_file.txt');
        $file->method('getSize')->willReturn(100);
        $file->method('getMTime')->willReturn(1700000000);
        $file->method('getMimetype')->willReturn('text/plain');
        $file->method('getPath')->willReturn('/b_file.txt');
        $file->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FILE);

        $subfolder = $this->createMock(\OCP\Files\Folder::class);
        $subfolder->method('getName')->willReturn('a_folder');
        $subfolder->method('getSize')->willReturn(0);
        $subfolder->method('getMTime')->willReturn(1700000001);
        $subfolder->method('getMimetype')->willReturn('httpd/unix-directory');
        $subfolder->method('getPath')->willReturn('/a_folder');
        $subfolder->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FOLDER);

        $folder = $this->createMock(\OCP\Files\Folder::class);
        $folder->method('getType')->willReturn(\OCP\Files\FileInfo::TYPE_FOLDER);
        $folder->method('getDirectoryListing')->willReturn([$file, $subfolder]);

        $userFolder = $this->createMock(\OCP\Files\Folder::class);
        $userFolder->method('get')->willReturn($folder);
        $this->rootFolder->method('getUserFolder')->willReturn($userFolder);

        $result = $this->service->listFolderContents('/some/path');

        $this->assertCount(2, $result);
        $this->assertEquals('folder', $result[0]['type']); // Folders first
        $this->assertEquals('file', $result[1]['type']);
    }

    // ── Pattern with {Id} ─────────────────────────────────────────

    public function testGetMemberFolderPathWithIdPattern(): void {
        $this->config->method('getAppValue')
            ->willReturnMap([
                ['mitgliederverwaltung', 'file_base_path', '/Verein/Mitglieder/', '/Verein/Mitglieder/'],
                ['mitgliederverwaltung', 'file_subfolder_pattern', '{Nachname}_{Vorname}', '{Id}_{Nachname}'],
            ]);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 42);

        $path = $this->service->getMemberFolderPath($member);

        $this->assertEquals('/Verein/Mitglieder/42_Mueller', $path);
    }
}
