<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Service\CalendarSyncService;
use OCA\Mitgliederverwaltung\Service\PermissionService;
use OCP\App\IAppManager;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class CalendarSyncServiceTest extends TestCase {

    private CalendarSyncService $service;
    private MemberMapper&MockObject $memberMapper;
    private IDBConnection&MockObject $db;
    private PermissionService&MockObject $permissionService;
    private IUserSession&MockObject $userSession;
    private LoggerInterface&MockObject $logger;
    private CalDavBackend&MockObject $caldavBackend;
    private IAppManager&MockObject $appManager;

    protected function setUp(): void {
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->db = $this->createMock(IDBConnection::class);
        $this->permissionService = $this->createMock(PermissionService::class);
        $this->userSession = $this->createMock(IUserSession::class);
        $this->logger = $this->createMock(LoggerInterface::class);
        $this->caldavBackend = $this->createMock(CalDavBackend::class);
        $this->appManager = $this->createMock(IAppManager::class);

        // Default: no user session (background job context — permission check skipped)
        $this->userSession->method('getUser')->willReturn(null);

        $this->service = new CalendarSyncService(
            $this->memberMapper,
            $this->db,
            $this->permissionService,
            $this->userSession,
            $this->logger,
            $this->caldavBackend,
            $this->appManager
        );
    }

    /**
     * Helper to create a service instance with a specific user context.
     */
    private function createServiceWithUser(string $userId, bool $canWrite): CalendarSyncService {
        $user = $this->createMock(IUser::class);
        $user->method('getUID')->willReturn($userId);

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

        $permissionService = $this->createMock(PermissionService::class);
        $permissionService->method('canWrite')->with($userId)->willReturn($canWrite);

        return new CalendarSyncService(
            $this->memberMapper,
            $this->db,
            $permissionService,
            $userSession,
            $this->logger,
            $this->caldavBackend,
            $this->appManager
        );
    }

    private function createQueryBuilderMock(): IQueryBuilder&MockObject {
        $qb = $this->createMock(IQueryBuilder::class);
        $expr = $this->createMock(IExpressionBuilder::class);
        $expr->method('eq')->willReturn('');
        $qb->method('expr')->willReturn($expr);
        $qb->method('insert')->willReturnSelf();
        $qb->method('delete')->willReturnSelf();
        $qb->method('update')->willReturnSelf();
        $qb->method('select')->willReturnSelf();
        $qb->method('from')->willReturnSelf();
        $qb->method('where')->willReturnSelf();
        $qb->method('andWhere')->willReturnSelf();
        $qb->method('values')->willReturnSelf();
        $qb->method('set')->willReturnSelf();
        $qb->method('orderBy')->willReturnSelf();
        $qb->method('setMaxResults')->willReturnSelf();
        $qb->method('createNamedParameter')->willReturn('?');
        $qb->method('executeStatement')->willReturn(1);

        $result = $this->createMock(\OCP\DB\IResult::class);
        $result->method('fetch')->willReturn(false);
        $result->method('closeCursor')->willReturn(true);
        $qb->method('executeQuery')->willReturn($result);

        return $qb;
    }

    private function createMemberEntity(int $id, string $status = 'aktiv', string $rolle = 'mitglied', ?string $calUri = null): Member {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2000-03-15');
        $member->setRolle($rolle);
        $member->setEintritt('2020-01-01');
        $member->setStatus($status);
        if ($calUri !== null) {
            $member->setCalendarEventUri($calUri);
        }
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, $id);
        return $member;
    }

    // ── Constructor / smoke test ───────────────────────────────────

    public function testConstructor(): void {
        $this->assertInstanceOf(CalendarSyncService::class, $this->service);
    }

    // ── syncMember() ───────────────────────────────────────────────

    public function testSyncMemberCreatesEventForActiveMember(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');

        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    public function testSyncMemberDeletesEventForInactiveMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied', 'mv-birthday-1.ics');

        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    public function testSyncMemberSkipsErziehungsberechtigter(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'erziehungsberechtigter');

        // No event should be created for Erziehungsberechtigter
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    public function testSyncMemberUpdatesExistingEvent(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $qb = $this->createQueryBuilderMock();
        $result = $this->createMock(\OCP\DB\IResult::class);
        $result->method('fetch')->willReturn(['id' => 1]);
        $result->method('closeCursor')->willReturn(true);
        $qb->method('executeQuery')->willReturn($result);
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied', 'mv-birthday-1.ics');

        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    // ── fullSync() ─────────────────────────────────────────────────

    public function testFullSyncReturnsStats(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $activeMember = $this->createMemberEntity(1, 'aktiv', 'mitglied');
        $inactiveMember = $this->createMemberEntity(2, 'inaktiv', 'mitglied', 'mv-birthday-2.ics');

        $this->memberMapper->method('findAll')->willReturn([$activeMember, $inactiveMember]);

        $stats = $this->service->fullSync();

        $this->assertArrayHasKey('created', $stats);
        $this->assertArrayHasKey('updated', $stats);
        $this->assertArrayHasKey('deleted', $stats);
    }

    public function testFullSyncHandlesException(): void {
        $this->memberMapper->method('findAll')
            ->willThrowException(new \RuntimeException('DB error'));

        $stats = $this->service->fullSync();

        // Should return default stats even on error
        $this->assertEquals(0, $stats['created']);
    }

    // ── enqueueSync() ──────────────────────────────────────────────

    public function testEnqueueSync(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $this->service->enqueueSync(1);
        $this->assertTrue(true);
    }

    public function testEnqueueSyncHandlesException(): void {
        $qb = $this->createQueryBuilderMock();
        $qb->method('executeStatement')
            ->willThrowException(new \RuntimeException('DB error'));
        $this->db->method('getQueryBuilder')->willReturn($qb);

        // Should not throw, just log warning
        $this->service->enqueueSync(1);
        $this->assertTrue(true);
    }

    // ── processQueue() ─────────────────────────────────────────────

    public function testProcessQueueEmptyQueue(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $processed = $this->service->processQueue();

        $this->assertEquals(0, $processed);
    }

    // ── deleteEvent() ──────────────────────────────────────────────

    public function testDeleteEventWithUri(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied', 'mv-birthday-1.ics');

        $this->service->deleteEvent($member);
        $this->assertTrue(true);
    }

    public function testDeleteEventWithoutUri(): void {
        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied');

        // No URI means nothing to delete
        $this->service->deleteEvent($member);
        $this->assertTrue(true);
    }

    // ── syncMember branching logic ────────────────────────────────

    public function testSyncMemberNoActionForActiveWithoutBirthday(): void {
        // Active member without calendar URI - should create
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(5, 'aktiv', 'mitglied');
        $member->setGeburtsdatum('1995-06-15');

        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    public function testSyncMemberDeletesEventForErziehungsberechtigterWithUri(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'erziehungsberechtigter', 'mv-birthday-1.ics');

        // Erziehungsberechtigter with existing URI should delete
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    public function testSyncMemberNoActionForInactiveWithoutUri(): void {
        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied');

        // Inactive without URI - no action needed
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    // ── fullSync counts ───────────────────────────────────────────

    public function testFullSyncCountsCorrectly(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        // Active without URI = create
        $m1 = $this->createMemberEntity(1, 'aktiv', 'mitglied');
        // Active with URI = update
        $m2 = $this->createMemberEntity(2, 'aktiv', 'mitglied', 'mv-birthday-2.ics');
        // Inactive with URI = delete
        $m3 = $this->createMemberEntity(3, 'inaktiv', 'mitglied', 'mv-birthday-3.ics');
        // Inactive without URI = no action
        $m4 = $this->createMemberEntity(4, 'inaktiv', 'mitglied');

        $this->memberMapper->method('findAll')->willReturn([$m1, $m2, $m3, $m4]);

        $stats = $this->service->fullSync();

        $this->assertEquals(1, $stats['created']);
        $this->assertEquals(1, $stats['updated']);
        $this->assertEquals(1, $stats['deleted']);
    }

    // ── processQueue with entries ─────────────────────────────────

    public function testProcessQueueWithEntries(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');
        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        // Build a fresh QBMock without default executeQuery so we can control it
        $qb = $this->createMock(IQueryBuilder::class);
        $expr = $this->createMock(IExpressionBuilder::class);
        $expr->method('eq')->willReturn('');
        $qb->method('expr')->willReturn($expr);
        $qb->method('insert')->willReturnSelf();
        $qb->method('delete')->willReturnSelf();
        $qb->method('update')->willReturnSelf();
        $qb->method('select')->willReturnSelf();
        $qb->method('from')->willReturnSelf();
        $qb->method('where')->willReturnSelf();
        $qb->method('andWhere')->willReturnSelf();
        $qb->method('values')->willReturnSelf();
        $qb->method('set')->willReturnSelf();
        $qb->method('orderBy')->willReturnSelf();
        $qb->method('setMaxResults')->willReturnSelf();
        $qb->method('createNamedParameter')->willReturn('?');
        $qb->method('executeStatement')->willReturn(1);

        // Result mock: first fetch returns queue row, subsequent return false
        $resultMock = $this->createMock(\OCP\DB\IResult::class);
        $callCount = 0;
        $resultMock->method('fetch')->willReturnCallback(function () use (&$callCount) {
            if ($callCount++ === 0) {
                return ['id' => 1, 'entity_id' => 1, 'entity_type' => 'calendar', 'created_at' => '2024-01-01'];
            }
            return false;
        });
        $resultMock->method('closeCursor')->willReturn(true);
        $qb->method('executeQuery')->willReturn($resultMock);

        $this->db->method('getQueryBuilder')->willReturn($qb);

        $processed = $this->service->processQueue();

        $this->assertEquals(1, $processed);
    }

    public function testProcessQueueHandlesFailedMember(): void {
        $this->memberMapper->method('findById')
            ->willThrowException(new \OCP\AppFramework\Db\DoesNotExistException('Not found'));

        $resultMock = $this->createMock(\OCP\DB\IResult::class);
        $callCount = 0;
        $resultMock->method('fetch')->willReturnCallback(function () use (&$callCount) {
            if ($callCount++ === 0) {
                return ['id' => 1, 'entity_id' => 999, 'entity_type' => 'calendar', 'created_at' => '2024-01-01'];
            }
            return false;
        });
        $resultMock->method('closeCursor')->willReturn(true);

        $qb = $this->createQueryBuilderMock();
        $qb->method('executeQuery')->willReturn($resultMock);
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $processed = $this->service->processQueue();

        $this->assertEquals(0, $processed);
    }

    // ── shouldHaveEvent edge cases ────────────────────────────────

    public function testSyncMemberDeletedMemberGetsEventRemoved(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied', 'mv-birthday-1.ics');
        $member->setDeletedAt('2024-01-01 00:00:00');

        // Deleted member with URI should get event deleted
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

    // ── shouldHaveEvent() via reflection ──────────────────────────

    public function testShouldHaveEventActiveNonErziehungsberechtigter(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'shouldHaveEvent');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');
        $this->assertTrue($method->invoke($this->service, $member));
    }

    public function testShouldNotHaveEventInactive(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'shouldHaveEvent');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied');
        $this->assertFalse($method->invoke($this->service, $member));
    }

    public function testShouldNotHaveEventErziehungsberechtigter(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'shouldHaveEvent');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(1, 'aktiv', 'erziehungsberechtigter');
        $this->assertFalse($method->invoke($this->service, $member));
    }

    public function testShouldNotHaveEventDeleted(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'shouldHaveEvent');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');
        $member->setDeletedAt('2024-01-01 00:00:00');
        $this->assertFalse($method->invoke($this->service, $member));
    }

    public function testShouldHaveEventLeiter(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'shouldHaveEvent');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(1, 'aktiv', 'leiter');
        $this->assertTrue($method->invoke($this->service, $member));
    }

    // ── generateEventUri() via reflection ─────────────────────────

    public function testGenerateEventUri(): void {
        $method = new \ReflectionMethod(CalendarSyncService::class, 'generateEventUri');
        $method->setAccessible(true);

        $member = $this->createMemberEntity(42, 'aktiv', 'mitglied');
        $uri = $method->invoke($this->service, $member);

        $this->assertEquals('mv-birthday-42.ics', $uri);
    }

    // ── enqueueSync additional test ───────────────────────────────

    public function testEnqueueSyncCreatesQueueEntry(): void {
        $qb = $this->createQueryBuilderMock();
        $qb->expects($this->once())->method('executeStatement');
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $this->service->enqueueSync(42);
        $this->assertTrue(true);
    }

    // ── Permission checks (Issue #170) ──────────────────────────

    public function testSyncMemberDeniedWithoutWritePermission(): void {
        $service = $this->createServiceWithUser('nowrite-user', false);
        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Calendar sync requires write permission');
        $service->syncMember($member);
    }

    public function testFullSyncDeniedWithoutWritePermission(): void {
        $service = $this->createServiceWithUser('nowrite-user', false);

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Calendar sync requires write permission');
        $service->fullSync();
    }

    public function testDeleteEventDeniedWithoutWritePermission(): void {
        $service = $this->createServiceWithUser('nowrite-user', false);
        $member = $this->createMemberEntity(1, 'inaktiv', 'mitglied', 'mv-birthday-1.ics');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Calendar sync requires write permission');
        $service->deleteEvent($member);
    }

    public function testSyncMemberAllowedWithWritePermission(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $service = $this->createServiceWithUser('writer-user', true);

        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');

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

    public function testSyncMemberAllowedInBackgroundContext(): void {
        if (!class_exists(\Sabre\VObject\Component\VCalendar::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        // Default setUp has no user session (background context)
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $member = $this->createMemberEntity(1, 'aktiv', 'mitglied');

        // Should not throw - background context skips permission check
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }
}
