<?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\Family;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\FeeRecord;
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\StufeHistory;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\DsgvoService;
use OCA\Mitgliederverwaltung\Service\EncryptionService;
use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class DsgvoServiceTest extends TestCase {

    private DsgvoService $service;
    private MemberMapper&MockObject $memberMapper;
    private AddressMapper&MockObject $addressMapper;
    private PhoneMapper&MockObject $phoneMapper;
    private EmailMapper&MockObject $emailMapper;
    private FamilyMapper&MockObject $familyMapper;
    private FeeRecordMapper&MockObject $feeRecordMapper;
    private StufeHistoryMapper&MockObject $stufeHistoryMapper;
    private EncryptionService&MockObject $encryptionService;
    private AuditService&MockObject $auditService;
    private LoggerInterface&MockObject $logger;

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

        $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->familyMapper = $this->createMock(FamilyMapper::class);
        $this->feeRecordMapper = $this->createMock(FeeRecordMapper::class);
        $this->stufeHistoryMapper = $this->createMock(StufeHistoryMapper::class);
        $this->encryptionService = $this->createMock(EncryptionService::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new DsgvoService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->feeRecordMapper,
            $this->stufeHistoryMapper,
            $this->encryptionService,
            $this->auditService,
            $this->logger
        );
    }

    // ── Helper factories ────────────────────────────────────────────

    private function createMember(
        int $id = 1,
        string $vorname = 'Max',
        string $nachname = 'Mustermann',
        ?int $familyId = null,
        ?string $allergienEncrypted = null
    ): Member {
        $member = new Member();
        $member->setId($id);
        $member->setVorname($vorname);
        $member->setNachname($nachname);
        $member->setGeburtsdatum('2010-01-15');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setFamilyId($familyId);
        $member->setAllergienEncrypted($allergienEncrypted);
        $member->setCreatedAt('2020-01-01 00:00:00');
        $member->setUpdatedAt('2020-01-01 00:00:00');
        return $member;
    }

    private function createFamily(int $id = 1, string $name = 'Mustermann'): Family {
        $family = new Family();
        $family->setId($id);
        $family->setName($name);
        $family->setKontoinhaberEncrypted('ENC:Max Mustermann');
        $family->setIbanEncrypted('ENC:DE89370400440532013000');
        $family->setBic('COBADEFFXXX');
        $family->setKreditinstitut('Commerzbank');
        $family->setCreatedAt('2020-01-01 00:00:00');
        $family->setUpdatedAt('2020-01-01 00:00:00');
        return $family;
    }

    private function createFeeRecord(int $id = 1, int $memberId = 1): FeeRecord {
        $record = new FeeRecord();
        $record->setId($id);
        $record->setMemberId($memberId);
        $record->setYear(2026);
        $record->setAmount('120.00');
        $record->setPaid(true);
        $record->setCreatedAt('2026-01-01 00:00:00');
        $record->setUpdatedAt('2026-01-01 00:00:00');
        return $record;
    }

    private function createStufeHistory(int $id = 1, int $memberId = 1): StufeHistory {
        $history = new StufeHistory();
        $history->setId($id);
        $history->setMemberId($memberId);
        $history->setStufeId(2);
        $history->setChangedAt('2025-09-01');
        $history->setChangedBy('admin');
        return $history;
    }

    // ── exportMemberData() ──────────────────────────────────────────

    public function testExportMemberDataReturnsCompleteStructure(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', 5, 'ENC:Nussallergie');

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

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

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

        $feeRecord = $this->createFeeRecord(1, 1);
        $stufeHistory = $this->createStufeHistory(1, 1);
        $family = $this->createFamily(5, 'Mustermann');

        $this->memberMapper->method('findById')->with(1)->willReturn($member);
        $this->addressMapper->method('findByMemberId')->with(1)->willReturn([$address]);
        $this->phoneMapper->method('findByMemberId')->with(1)->willReturn([$phone]);
        $this->emailMapper->method('findByMemberId')->with(1)->willReturn([$email]);
        $this->feeRecordMapper->method('findByMemberId')->with(1)->willReturn([$feeRecord]);
        $this->stufeHistoryMapper->method('findByMemberId')->with(1)->willReturn([$stufeHistory]);
        $this->familyMapper->method('findById')->with(5)->willReturn($family);

        $this->encryptionService->method('decrypt')
            ->willReturnCallback(function (?string $value) {
                if ($value === 'ENC:Nussallergie') return 'Nussallergie';
                if ($value === 'ENC:Max Mustermann') return 'Max Mustermann';
                if ($value === 'ENC:DE89370400440532013000') return 'DE89370400440532013000';
                return $value;
            });

        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

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

        // Check top-level structure
        $this->assertArrayHasKey('exportDatum', $result);
        $this->assertArrayHasKey('mitglied', $result);
        $this->assertArrayHasKey('adressen', $result);
        $this->assertArrayHasKey('telefonnummern', $result);
        $this->assertArrayHasKey('emailAdressen', $result);
        $this->assertArrayHasKey('familie', $result);
        $this->assertArrayHasKey('beitragshistorie', $result);
        $this->assertArrayHasKey('stufenhistorie', $result);
        $this->assertArrayHasKey('auditLog', $result);
    }

    public function testExportMemberDataDecryptsAllergien(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', null, 'ENC:Nussallergie');

        $this->memberMapper->method('findById')->with(1)->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

        $this->encryptionService->method('decrypt')
            ->with('ENC:Nussallergie')
            ->willReturn('Nussallergie');

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

        $this->assertSame('Nussallergie', $result['mitglied']['allergien']);
    }

    public function testExportMemberDataDecryptsFamilyBankingData(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', 5);
        $family = $this->createFamily(5, 'Mustermann');

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->familyMapper->method('findById')->with(5)->willReturn($family);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

        $this->encryptionService->method('decrypt')
            ->willReturnCallback(function (?string $value) {
                if ($value === 'ENC:Max Mustermann') return 'Max Mustermann';
                if ($value === 'ENC:DE89370400440532013000') return 'DE89370400440532013000';
                return $value;
            });

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

        $this->assertNotNull($result['familie']);
        $this->assertSame('Max Mustermann', $result['familie']['kontoinhaber']);
        $this->assertSame('DE89370400440532013000', $result['familie']['iban']);
        $this->assertSame('COBADEFFXXX', $result['familie']['bic']);
        $this->assertSame('Commerzbank', $result['familie']['kreditinstitut']);
    }

    public function testExportMemberDataWithoutFamily(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', null);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

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

        $this->assertNull($result['familie']);
    }

    public function testExportMemberDataHandlesDeletedFamily(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', 5);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->familyMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));
        $this->encryptionService->method('decrypt')->willReturn(null);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

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

        $this->assertNull($result['familie']);
    }

    public function testExportMemberDataHandlesPurgedSubEntities(): void {
        $member = $this->createMember(1);
        $member->setDeletedAt('2026-01-01 00:00:00');

        $this->memberMapper->method('findById')->willReturn($member);
        // Sub-entities throw exceptions (purged during soft-delete)
        $this->addressMapper->method('findByMemberId')
            ->willThrowException(new \Exception('No addresses'));
        $this->phoneMapper->method('findByMemberId')
            ->willThrowException(new \Exception('No phones'));
        $this->emailMapper->method('findByMemberId')
            ->willThrowException(new \Exception('No emails'));
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

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

        $this->assertSame([], $result['adressen']);
        $this->assertSame([], $result['telefonnummern']);
        $this->assertSame([], $result['emailAdressen']);
    }

    public function testExportMemberDataIncludesAuditLog(): void {
        $member = $this->createMember(1);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);

        $auditData = [['aktion' => 'create', 'entitaet' => 'member']];
        $this->auditService->method('getLog')
            ->with('member', 1)
            ->willReturn(['data' => $auditData, 'total' => 1]);

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

        $this->assertSame($auditData, $result['auditLog']);
    }

    public function testExportMemberDataLogsExportAuditEntry(): void {
        $member = $this->createMember(1);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

        $this->auditService->expects($this->once())
            ->method('logCreate')
            ->willReturnCallback(function (array $data, string $type, int $id) {
                $this->assertSame('dsgvo-export', $data['action']);
                $this->assertSame('dsgvo', $type);
                $this->assertSame(1, $id);
            });

        $this->service->exportMemberData(1);
    }

    public function testExportMemberDataThrowsForMissingMember(): void {
        $this->memberMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

        $this->expectException(DoesNotExistException::class);

        $this->service->exportMemberData(999);
    }

    public function testExportMemberDataIncludesAllMemberFields(): void {
        $member = $this->createMember(1);
        $member->setGeschlecht('maennlich');
        $member->setNotizen('Test notes');
        $member->setZusatzNotizen('Extra notes');
        $member->setKvTyp('gesetzlich');
        $member->setKvName('AOK');
        $member->setFrozenFeeRate('95.00');

        $this->memberMapper->method('findById')->willReturn($member);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);
        $this->auditService->method('getLog')->willReturn(['data' => [], 'total' => 0]);

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

        $mitglied = $result['mitglied'];
        $this->assertSame('Max', $mitglied['vorname']);
        $this->assertSame('Mustermann', $mitglied['nachname']);
        $this->assertSame('2010-01-15', $mitglied['geburtsdatum']);
        $this->assertSame('maennlich', $mitglied['geschlecht']);
        $this->assertSame('mitglied', $mitglied['rolle']);
        $this->assertSame('2020-01-01', $mitglied['eintritt']);
        $this->assertSame('aktiv', $mitglied['status']);
        $this->assertSame('Test notes', $mitglied['notizen']);
        $this->assertSame('Extra notes', $mitglied['zusatzNotizen']);
        $this->assertSame('gesetzlich', $mitglied['kvTyp']);
        $this->assertSame('AOK', $mitglied['kvName']);
        $this->assertSame('95.00', $mitglied['frozenFeeRate']);
    }

    // ── hardDeleteMember() ──────────────────────────────────────────

    public function testHardDeleteDeletesAllRelatedData(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann');
        $feeRecord = $this->createFeeRecord(1, 1);
        $stufeHistory = $this->createStufeHistory(1, 1);

        $this->memberMapper->method('findById')->with(1)->willReturn($member);
        $this->feeRecordMapper->method('findByMemberId')->with(1)->willReturn([$feeRecord]);
        $this->stufeHistoryMapper->method('findByMemberId')->with(1)->willReturn([$stufeHistory]);

        // Expect all delete operations
        $this->addressMapper->expects($this->once())->method('deleteByMemberId')->with(1);
        $this->phoneMapper->expects($this->once())->method('deleteByMemberId')->with(1);
        $this->emailMapper->expects($this->once())->method('deleteByMemberId')->with(1);
        $this->feeRecordMapper->expects($this->once())->method('delete')->with($feeRecord);
        $this->stufeHistoryMapper->expects($this->once())->method('delete')->with($stufeHistory);
        $this->memberMapper->expects($this->once())->method('delete')->with($member);

        $this->service->hardDeleteMember(1);
    }

    public function testHardDeleteLogsAuditBeforeDeletion(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann');

        $this->memberMapper->method('findById')->willReturn($member);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);

        $this->auditService->expects($this->once())
            ->method('logCreate')
            ->willReturnCallback(function (array $data, string $type, int $id) {
                $this->assertSame('dsgvo-loeschung', $data['action']);
                $this->assertSame('Max Mustermann', $data['name']);
                $this->assertSame('dsgvo', $type);
                $this->assertSame(1, $id);
            });

        $this->service->hardDeleteMember(1);
    }

    public function testHardDeleteLogsWarning(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann');

        $this->memberMapper->method('findById')->willReturn($member);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);

        $this->logger->expects($this->once())->method('warning');

        $this->service->hardDeleteMember(1);
    }

    public function testHardDeleteThrowsForMissingMember(): void {
        $this->memberMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

        $this->expectException(DoesNotExistException::class);

        $this->service->hardDeleteMember(999);
    }

    public function testHardDeleteWithMultipleFeeRecords(): void {
        $member = $this->createMember(1);
        $feeRecord1 = $this->createFeeRecord(1, 1);
        $feeRecord2 = $this->createFeeRecord(2, 1);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([$feeRecord1, $feeRecord2]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);

        $this->feeRecordMapper->expects($this->exactly(2))->method('delete');

        $this->service->hardDeleteMember(1);
    }

    public function testHardDeleteWithNoRelatedData(): void {
        $member = $this->createMember(1);

        $this->memberMapper->method('findById')->willReturn($member);
        $this->feeRecordMapper->method('findByMemberId')->willReturn([]);
        $this->stufeHistoryMapper->method('findByMemberId')->willReturn([]);

        // Should still delete the member itself
        $this->memberMapper->expects($this->once())->method('delete')->with($member);

        $this->service->hardDeleteMember(1);
    }
}
