<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Address;
use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\Email;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Service\ContactsSyncService;
use OCA\Mitgliederverwaltung\Service\PermissionService;
use OCP\Contacts\IManager as IContactsManager;
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 ContactsSyncServiceTest extends TestCase {

    private ContactsSyncService $service;
    private MemberMapper&MockObject $memberMapper;
    private AddressMapper&MockObject $addressMapper;
    private PhoneMapper&MockObject $phoneMapper;
    private EmailMapper&MockObject $emailMapper;
    private IContactsManager&MockObject $contactsManager;
    private PermissionService&MockObject $permissionService;
    private IUserSession&MockObject $userSession;
    private IDBConnection&MockObject $db;
    private LoggerInterface&MockObject $logger;

    protected function setUp(): void {
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->addressMapper = $this->createMock(AddressMapper::class);
        $this->phoneMapper = $this->createMock(PhoneMapper::class);
        $this->emailMapper = $this->createMock(EmailMapper::class);
        $this->contactsManager = $this->createMock(IContactsManager::class);
        $this->permissionService = $this->createMock(PermissionService::class);
        $this->userSession = $this->createMock(IUserSession::class);
        $this->db = $this->createMock(IDBConnection::class);
        $this->logger = $this->createMock(LoggerInterface::class);

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

        // Default: contacts manager not enabled (falls back to staging table)
        $this->contactsManager->method('isEnabled')->willReturn(false);

        $this->service = new ContactsSyncService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->contactsManager,
            $this->permissionService,
            $this->userSession,
            $this->db,
            $this->logger
        );
    }

    /**
     * Helper to create a service instance with a specific user context.
     */
    private function createServiceWithUser(string $userId, bool $canWrite): ContactsSyncService {
        $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);

        $contactsManager = $this->createMock(IContactsManager::class);
        $contactsManager->method('isEnabled')->willReturn(false);

        return new ContactsSyncService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $contactsManager,
            $permissionService,
            $userSession,
            $this->db,
            $this->logger
        );
    }

    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 $contactUri = null): Member {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2000-03-15');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus($status);
        if ($contactUri !== null) {
            $member->setContactVcardUri($contactUri);
        }
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, $id);
        return $member;
    }

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

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

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

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

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

        $member = $this->createMemberEntity(1, 'aktiv');
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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

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

        $member = $this->createMemberEntity(1, 'inaktiv', 'mv-contact-1.vcf');

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

    public function testSyncMemberUpdatesExistingContact(): void {
        if (!class_exists(\Sabre\VObject\Component\VCard::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', 'mv-contact-1.vcf');
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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

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

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

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

        $address = new Address();
        $address->setMemberId(1);
        $address->setStrasse('Musterstr. 1');
        $address->setPlz('12345');
        $address->setOrt('Berlin');
        $address->setLand('Deutschland');

        $phone = new Phone();
        $phone->setMemberId(1);
        $phone->setNumberE164('+4917612345678');

        $email = new Email();
        $email->setMemberId(1);
        $email->setEmail('max@test.com');

        $this->addressMapper->method('findByMemberId')->willReturn([$address]);
        $this->phoneMapper->method('findByMemberId')->willReturn([$phone]);
        $this->emailMapper->method('findByMemberId')->willReturn([$email]);

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

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

        // No shouldHaveContact and no hasContact -> do nothing
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

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

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

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

        $activeMember = $this->createMemberEntity(1, 'aktiv');
        $inactiveMember = $this->createMemberEntity(2, 'inaktiv', 'mv-contact-2.vcf');

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

        $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();

        $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);

        $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);
    }

    // ── deleteContact() ────────────────────────────────────────────

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

        $member = $this->createMemberEntity(1, 'inaktiv', 'mv-contact-1.vcf');

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

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

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

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

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

        // No shouldHaveContact, no hasContact -> no action
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

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

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

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

        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

        // Active Erziehungsberechtigter should still get a contact (unlike calendar)
        $this->service->syncMember($member);
        $this->assertTrue(true);
    }

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

    public function testFullSyncCountsCorrectly(): void {
        if (!class_exists(\Sabre\VObject\Component\VCard::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');
        // Active with URI = update
        $m2 = $this->createMemberEntity(2, 'aktiv', 'mv-contact-2.vcf');
        // Inactive with URI = delete
        $m3 = $this->createMemberEntity(3, 'inaktiv', 'mv-contact-3.vcf');
        // Inactive without URI = no action
        $m4 = $this->createMemberEntity(4, 'inaktiv');

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

        $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\VCard::class)) {
            $this->markTestSkipped('Sabre VObject not available');
        }

        $member = $this->createMemberEntity(1, 'aktiv');
        $this->memberMapper->method('findById')->with(1)->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

        // Build a fresh QBMock without default executeQuery
        $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);

        $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' => 'contacts', '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' => 'contacts', '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);
    }

    // ── VCard with labels ─────────────────────────────────────────

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

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

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

        $address = new Address();
        $address->setMemberId(1);
        $address->setStrasse('Musterstr. 1');
        $address->setPlz('12345');
        $address->setOrt('Berlin');
        $address->setLand('Deutschland');
        $address->setLabel('Home');

        $phone = new Phone();
        $phone->setMemberId(1);
        $phone->setNumberE164('+4917612345678');
        $phone->setLabel('Mobil');

        $email = new Email();
        $email->setMemberId(1);
        $email->setEmail('max@test.com');
        $email->setLabel('Privat');

        $this->addressMapper->method('findByMemberId')->willReturn([$address]);
        $this->phoneMapper->method('findByMemberId')->willReturn([$phone]);
        $this->emailMapper->method('findByMemberId')->willReturn([$email]);

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

    // ── VCard with errors in sub-queries ──────────────────────────

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

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

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

        $this->addressMapper->method('findByMemberId')
            ->willThrowException(new \RuntimeException('DB error'));
        $this->phoneMapper->method('findByMemberId')
            ->willThrowException(new \RuntimeException('DB error'));
        $this->emailMapper->method('findByMemberId')
            ->willThrowException(new \RuntimeException('DB error'));

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

    // ── shouldHaveContact edge cases ──────────────────────────────

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

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

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

    // ── shouldHaveContact() via reflection ────────────────────────

    public function testShouldHaveContactActiveMember(): void {
        $method = new \ReflectionMethod(ContactsSyncService::class, 'shouldHaveContact');
        $method->setAccessible(true);

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

    public function testShouldNotHaveContactInactive(): void {
        $method = new \ReflectionMethod(ContactsSyncService::class, 'shouldHaveContact');
        $method->setAccessible(true);

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

    public function testShouldNotHaveContactDeleted(): void {
        $method = new \ReflectionMethod(ContactsSyncService::class, 'shouldHaveContact');
        $method->setAccessible(true);

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

    public function testShouldHaveContactErziehungsberechtigterIfActive(): void {
        $method = new \ReflectionMethod(ContactsSyncService::class, 'shouldHaveContact');
        $method->setAccessible(true);

        // Unlike calendar, contacts include Erziehungsberechtigte if active
        $member = $this->createMemberEntity(1, 'aktiv');
        $member->setRolle('erziehungsberechtigter');
        $this->assertTrue($method->invoke($this->service, $member));
    }

    // ── generateVcardUri() via reflection ─────────────────────────

    public function testGenerateVcardUri(): void {
        $method = new \ReflectionMethod(ContactsSyncService::class, 'generateVcardUri');
        $method->setAccessible(true);

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

        $this->assertEquals('mv-contact-42.vcf', $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');

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

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

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

    public function testDeleteContactDeniedWithoutWritePermission(): void {
        $service = $this->createServiceWithUser('nowrite-user', false);
        $member = $this->createMemberEntity(1, 'inaktiv', 'mv-contact-1.vcf');

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

    public function testSyncMemberAllowedWithWritePermission(): void {
        if (!class_exists(\Sabre\VObject\Component\VCard::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');
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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

    public function testSyncMemberAllowedInBackgroundContext(): void {
        if (!class_exists(\Sabre\VObject\Component\VCard::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');
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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