<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
use OCA\Mitgliederverwaltung\Db\FeeRuleMapper;
use OCA\Mitgliederverwaltung\Db\InjuryMapper;
use OCA\Mitgliederverwaltung\Db\LagerMapper;
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmerMapper;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\SavedQueryMapper;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Db\StufeMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\Family;
use OCA\Mitgliederverwaltung\Db\Address;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\Email;
use OCA\Mitgliederverwaltung\Db\Stufe;
use OCA\Mitgliederverwaltung\Db\StufeHistory;
use OCA\Mitgliederverwaltung\Db\FeeRule;
use OCA\Mitgliederverwaltung\Db\FeeRecord;
use OCA\Mitgliederverwaltung\Db\Lager;
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmer;
use OCA\Mitgliederverwaltung\Db\Injury;
use OCA\Mitgliederverwaltung\Db\SavedQuery;
use OCA\Mitgliederverwaltung\Service\EntityExportService;
use OCA\Mitgliederverwaltung\Service\EncryptionService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class EntityExportServiceTest extends TestCase {

    private EntityExportService $service;
    private MemberMapper&MockObject $memberMapper;
    private FamilyMapper&MockObject $familyMapper;
    private AddressMapper&MockObject $addressMapper;
    private PhoneMapper&MockObject $phoneMapper;
    private EmailMapper&MockObject $emailMapper;
    private StufeMapper&MockObject $stufeMapper;
    private StufeHistoryMapper&MockObject $stufeHistoryMapper;
    private FeeRuleMapper&MockObject $feeRuleMapper;
    private FeeRecordMapper&MockObject $feeRecordMapper;
    private LagerMapper&MockObject $lagerMapper;
    private LagerTeilnehmerMapper&MockObject $lagerTeilnehmerMapper;
    private InjuryMapper&MockObject $injuryMapper;
    private SavedQueryMapper&MockObject $savedQueryMapper;
    private EncryptionService&MockObject $encryptionService;
    private LoggerInterface&MockObject $logger;

    protected function setUp(): void {
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->familyMapper = $this->createMock(FamilyMapper::class);
        $this->addressMapper = $this->createMock(AddressMapper::class);
        $this->phoneMapper = $this->createMock(PhoneMapper::class);
        $this->emailMapper = $this->createMock(EmailMapper::class);
        $this->stufeMapper = $this->createMock(StufeMapper::class);
        $this->stufeHistoryMapper = $this->createMock(StufeHistoryMapper::class);
        $this->feeRuleMapper = $this->createMock(FeeRuleMapper::class);
        $this->feeRecordMapper = $this->createMock(FeeRecordMapper::class);
        $this->lagerMapper = $this->createMock(LagerMapper::class);
        $this->lagerTeilnehmerMapper = $this->createMock(LagerTeilnehmerMapper::class);
        $this->injuryMapper = $this->createMock(InjuryMapper::class);
        $this->savedQueryMapper = $this->createMock(SavedQueryMapper::class);
        $this->encryptionService = $this->createMock(EncryptionService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new EntityExportService(
            $this->memberMapper,
            $this->familyMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->stufeMapper,
            $this->stufeHistoryMapper,
            $this->feeRuleMapper,
            $this->feeRecordMapper,
            $this->lagerMapper,
            $this->lagerTeilnehmerMapper,
            $this->injuryMapper,
            $this->savedQueryMapper,
            $this->encryptionService,
            $this->logger
        );
    }

    // ── getEntityTypes() ───────────────────────────────────────────

    public function testGetEntityTypesReturnsAllTypes(): void {
        $types = $this->service->getEntityTypes();

        $this->assertCount(13, $types);
        $keys = array_column($types, 'key');
        $this->assertContains('mitglieder', $keys);
        $this->assertContains('familien', $keys);
        $this->assertContains('adressen', $keys);
        $this->assertContains('telefonnummern', $keys);
        $this->assertContains('emails', $keys);
        $this->assertContains('stufen', $keys);
        $this->assertContains('stufenverlauf', $keys);
        $this->assertContains('beitragsregeln', $keys);
        $this->assertContains('beitraege', $keys);
        $this->assertContains('lager', $keys);
        $this->assertContains('lagerteilnehmer', $keys);
        $this->assertContains('verletzungen', $keys);
        $this->assertContains('gespeicherte_abfragen', $keys);
    }

    public function testGetEntityTypesHasLabels(): void {
        $types = $this->service->getEntityTypes();

        foreach ($types as $type) {
            $this->assertArrayHasKey('key', $type);
            $this->assertArrayHasKey('label', $type);
            $this->assertNotEmpty($type['label']);
        }
    }

    // ── getColumnHeaders() ─────────────────────────────────────────

    public function testGetColumnHeadersMitglieder(): void {
        $headers = $this->service->getColumnHeaders('mitglieder');
        $this->assertContains('ID', $headers);
        $this->assertContains('Vorname', $headers);
        $this->assertContains('Nachname', $headers);
        $this->assertContains('Geburtsdatum', $headers);
        $this->assertCount(22, $headers);
    }

    public function testGetColumnHeadersFamilien(): void {
        $headers = $this->service->getColumnHeaders('familien');
        $this->assertContains('ID', $headers);
        $this->assertContains('Name', $headers);
        $this->assertContains('IBAN', $headers);
        $this->assertCount(6, $headers);
    }

    public function testGetColumnHeadersStufen(): void {
        $headers = $this->service->getColumnHeaders('stufen');
        $this->assertContains('Name', $headers);
        $this->assertCount(6, $headers);
    }

    public function testGetColumnHeadersLager(): void {
        $headers = $this->service->getColumnHeaders('lager');
        $this->assertContains('Name', $headers);
        $this->assertContains('Startdatum', $headers);
        $this->assertCount(6, $headers);
    }

    public function testGetColumnHeadersAdressen(): void {
        $headers = $this->service->getColumnHeaders('adressen');
        $this->assertContains('Strasse', $headers);
        $this->assertCount(9, $headers);
    }

    public function testGetColumnHeadersTelefonnummern(): void {
        $headers = $this->service->getColumnHeaders('telefonnummern');
        $this->assertContains('Nummer', $headers);
        $this->assertCount(5, $headers);
    }

    public function testGetColumnHeadersEmails(): void {
        $headers = $this->service->getColumnHeaders('emails');
        $this->assertContains('E-Mail-Adresse', $headers);
        $this->assertCount(5, $headers);
    }

    public function testGetColumnHeadersStufenverlauf(): void {
        $headers = $this->service->getColumnHeaders('stufenverlauf');
        $this->assertCount(7, $headers);
    }

    public function testGetColumnHeadersBeitragsregeln(): void {
        $headers = $this->service->getColumnHeaders('beitragsregeln');
        $this->assertCount(7, $headers);
    }

    public function testGetColumnHeadersBeitraege(): void {
        $headers = $this->service->getColumnHeaders('beitraege');
        $this->assertCount(9, $headers);
    }

    public function testGetColumnHeadersLagerteilnehmer(): void {
        $headers = $this->service->getColumnHeaders('lagerteilnehmer');
        $this->assertCount(6, $headers);
    }

    public function testGetColumnHeadersVerletzungen(): void {
        $headers = $this->service->getColumnHeaders('verletzungen');
        $this->assertCount(9, $headers);
    }

    public function testGetColumnHeadersGespeicherteAbfragen(): void {
        $headers = $this->service->getColumnHeaders('gespeicherte_abfragen');
        $this->assertCount(6, $headers);
    }

    public function testGetColumnHeadersThrowsOnUnknownType(): void {
        $this->expectException(\InvalidArgumentException::class);
        $this->service->getColumnHeaders('unknown');
    }

    // ── exportEntity() ─────────────────────────────────────────────

    public function testExportEntityThrowsOnUnknownType(): void {
        $this->expectException(\InvalidArgumentException::class);
        $this->service->exportEntity('unknown_type');
    }

    public function testExportEntityStufenGeneratesValidCsv(): void {
        $stufe = new Stufe();
        $stufe->setName('Woelflinge');
        $stufe->setSortOrder(1);
        $stufe->setAgeRangeMin(7);
        $stufe->setAgeRangeMax(10);
        $stufe->setColor('#ff0000');
        // Use reflection to set ID
        $ref = new \ReflectionProperty($stufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($stufe, 1);

        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

        $result = $this->service->exportEntity('stufen');

        $this->assertEquals('Stufen.csv', $result['filename']);
        // Check UTF-8 BOM
        $this->assertStringStartsWith("\xEF\xBB\xBF", $result['content']);
        // Check headers
        $this->assertStringContainsString('ID;Name;Sortierung;Mindestalter;Hoechstalter;Farbe', $result['content']);
        // Check data row
        $this->assertStringContainsString('1;Woelflinge;1;7;10;#ff0000', $result['content']);
    }

    public function testExportEntityUsesCorrectFilename(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $result = $this->service->exportEntity('stufen');
        $this->assertEquals('Stufen.csv', $result['filename']);
    }

    public function testExportEntityMitgliederMasksEncryptedFields(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setAllergienEncrypted('ENC:encrypted_data');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('mitglieder', false);

        $this->assertStringContainsString('[verschluesselt]', $result['content']);
    }

    public function testExportEntityMitgliederShowsEncryptedFieldsWhenSensitive(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setAllergienEncrypted('ENC:encrypted_data');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn('Erdnuesse');

        $result = $this->service->exportEntity('mitglieder', true);

        $this->assertStringContainsString('Erdnuesse', $result['content']);
        $this->assertStringNotContainsString('[verschluesselt]', $result['content']);
    }

    public function testExportEntityMitgliederEmptyAllergien(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        // no allergienEncrypted set - null by default
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('mitglieder', false);

        // When allergienEncrypted is null, should show empty, not [verschluesselt]
        $this->assertStringNotContainsString('[verschluesselt]', $result['content']);
    }

    public function testExportEntityFamilienMasksEncryptedFields(): void {
        $family = new Family();
        $family->setName('Mueller');
        $family->setKontoinhaberEncrypted('ENC:encrypted');
        $family->setIbanEncrypted('ENC:iban');
        $family->setBic('DEUTDEDB');
        $family->setKreditinstitut('Deutsche Bank');
        $ref = new \ReflectionProperty($family, 'id');
        $ref->setAccessible(true);
        $ref->setValue($family, 1);

        $this->familyMapper->method('findAll')->willReturn([$family]);

        $result = $this->service->exportEntity('familien', false);

        // Both kontoinhaber and IBAN should be masked
        $content = $result['content'];
        $lines = explode("\r\n", $content);
        // Data row is line index 1 (after BOM+header)
        $this->assertStringContainsString('[verschluesselt]', $lines[1]);
    }

    public function testExportEntityFamilienShowsSensitive(): void {
        $family = new Family();
        $family->setName('Mueller');
        $family->setKontoinhaberEncrypted('ENC:encrypted');
        $family->setIbanEncrypted('ENC:iban');
        $family->setBic('DEUTDEDB');
        $family->setKreditinstitut('Deutsche Bank');
        $ref = new \ReflectionProperty($family, 'id');
        $ref->setAccessible(true);
        $ref->setValue($family, 1);

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->encryptionService->method('decrypt')
            ->willReturnCallback(function (string $val) {
                return match ($val) {
                    'ENC:encrypted' => 'Hans Mueller',
                    'ENC:iban' => 'DE89370400440532013000',
                    default => $val,
                };
            });

        $result = $this->service->exportEntity('familien', true);

        $this->assertStringContainsString('Hans Mueller', $result['content']);
        $this->assertStringContainsString('DE89370400440532013000', $result['content']);
    }

    public function testExportEntityCsvSemicolonDelimiter(): void {
        $stufe = new Stufe();
        $stufe->setName('Test');
        $stufe->setSortOrder(1);
        $ref = new \ReflectionProperty($stufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($stufe, 1);

        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

        $result = $this->service->exportEntity('stufen');

        // Check that semicolons are used as delimiter
        $lines = explode("\r\n", $result['content']);
        $headerLine = str_replace("\xEF\xBB\xBF", '', $lines[0]);
        $this->assertEquals(5, substr_count($headerLine, ';'));
    }

    public function testExportEntityCsvCrlfLineEnding(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('stufen');

        $this->assertStringContainsString("\r\n", $result['content']);
    }

    // ── Foreign key resolution ─────────────────────────────────────

    public function testExportEntityResolvesStufeNames(): void {
        $stufe = new Stufe();
        $stufe->setName('Woelflinge');
        $stufe->setSortOrder(1);
        $ref = new \ReflectionProperty($stufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($stufe, 5);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setStufeId(5);
        $ref2 = new \ReflectionProperty($member, 'id');
        $ref2->setAccessible(true);
        $ref2->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);
        $this->familyMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('mitglieder', false);

        $this->assertStringContainsString('Woelflinge', $result['content']);
    }

    public function testExportEntityResolvesFamilyNames(): void {
        $family = new Family();
        $family->setName('Mueller');
        $ref = new \ReflectionProperty($family, 'id');
        $ref->setAccessible(true);
        $ref->setValue($family, 3);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setFamilyId(3);
        $ref2 = new \ReflectionProperty($member, 'id');
        $ref2->setAccessible(true);
        $ref2->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([$family]);

        $result = $this->service->exportEntity('mitglieder', false);

        $this->assertStringContainsString('Mueller', $result['content']);
    }

    // ── buildCsv() ─────────────────────────────────────────────────

    public function testBuildCsvStartsWithBom(): void {
        $csv = $this->service->buildCsv(['A', 'B'], [['1', '2']]);
        $this->assertStringStartsWith("\xEF\xBB\xBF", $csv);
    }

    public function testBuildCsvEscapesFieldsWithDelimiter(): void {
        $csv = $this->service->buildCsv(['A'], [['value;with;semicolons']]);
        $this->assertStringContainsString('"value;with;semicolons"', $csv);
    }

    public function testBuildCsvEscapesFieldsWithQuotes(): void {
        $csv = $this->service->buildCsv(['A'], [['value "with" quotes']]);
        $this->assertStringContainsString('"value ""with"" quotes"', $csv);
    }

    public function testBuildCsvEscapesFieldsWithNewlines(): void {
        $csv = $this->service->buildCsv(['A'], [["value\nwith\nnewlines"]]);
        $this->assertStringContainsString("\"value\nwith\nnewlines\"", $csv);
    }

    public function testBuildCsvNoEscapingForSimpleFields(): void {
        $csv = $this->service->buildCsv(['Name'], [['Max']]);
        $bom = "\xEF\xBB\xBF";
        $this->assertEquals($bom . "Name\r\nMax\r\n", $csv);
    }

    // ── exportEntity() for additional entity types ─────────────────

    public function testExportEntityLager(): void {
        $lager = new Lager();
        $lager->setName('Sommerlager');
        $lager->setStartdatum('2024-07-01');
        $lager->setEnddatum('2024-07-14');
        $lager->setOrt('Wald');
        $lager->setBeschreibung('Tolles Lager');
        $ref = new \ReflectionProperty($lager, 'id');
        $ref->setAccessible(true);
        $ref->setValue($lager, 1);

        $this->lagerMapper->method('findAll')->willReturn([$lager]);

        $result = $this->service->exportEntity('lager');

        $this->assertEquals('Lager.csv', $result['filename']);
        $this->assertStringContainsString('Sommerlager', $result['content']);
        $this->assertStringContainsString('2024-07-01', $result['content']);
    }

    public function testExportEntityEmptyTable(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('stufen');

        // Should still have BOM and header line
        $this->assertStringStartsWith("\xEF\xBB\xBF", $result['content']);
        $this->assertStringContainsString('ID;Name;Sortierung', $result['content']);
        // Only the header line (no data rows)
        $nonEmptyLines = array_filter(explode("\r\n", $result['content']), fn($l) => $l !== '');
        $this->assertCount(1, $nonEmptyLines);
    }

    // ── exportFullBundle() ─────────────────────────────────────────

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

        // Stub all mappers to return empty arrays
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->addressMapper->method('findAll')->willReturn([]);
        $this->phoneMapper->method('findAll')->willReturn([]);
        $this->emailMapper->method('findAll')->willReturn([]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->stufeHistoryMapper->method('findAll')->willReturn([]);
        $this->feeRuleMapper->method('findAll')->willReturn([]);
        $this->feeRecordMapper->method('findAll')->willReturn([]);
        $this->lagerMapper->method('findAll')->willReturn([]);
        $this->lagerTeilnehmerMapper->method('findAll')->willReturn([]);
        $this->injuryMapper->method('findAll')->willReturn([]);
        $this->savedQueryMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportFullBundle(false);

        $this->assertArrayHasKey('path', $result);
        $this->assertArrayHasKey('filename', $result);
        $this->assertFileExists($result['path']);
        $this->assertStringContainsString('Vollstaendiger_Export', $result['filename']);
        $this->assertStringEndsWith('.zip', $result['filename']);

        // Verify ZIP has the right number of files
        $zip = new \ZipArchive();
        $zip->open($result['path']);
        $this->assertEquals(13, $zip->numFiles);
        $zip->close();

        // Cleanup
        unlink($result['path']);
    }

    // ── Row builders for remaining entity types ──────────────────────

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

        $address = new Address();
        $address->setMemberId(1);
        $address->setLabel('Home');
        $address->setStrasse('Musterstr. 1');
        $address->setPlz('12345');
        $address->setOrt('Berlin');
        $address->setLand('Deutschland');
        $address->setIsPrimary(true);
        $refA = new \ReflectionProperty($address, 'id');
        $refA->setAccessible(true);
        $refA->setValue($address, 10);

        $this->addressMapper->method('findAll')->willReturn([$address]);
        $this->memberMapper->method('findAll')->willReturn([$member]);

        $result = $this->service->exportEntity('adressen');

        $this->assertEquals('Adressen.csv', $result['filename']);
        $this->assertStringContainsString('Musterstr. 1', $result['content']);
        $this->assertStringContainsString('12345', $result['content']);
        $this->assertStringContainsString('Berlin', $result['content']);
        $this->assertStringContainsString('Max Mueller', $result['content']);
        $this->assertStringContainsString('Ja', $result['content']);
    }

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

        $phone = new Phone();
        $phone->setMemberId(1);
        $phone->setLabel('Mobil');
        $phone->setNumberE164('+4917612345678');
        $refP = new \ReflectionProperty($phone, 'id');
        $refP->setAccessible(true);
        $refP->setValue($phone, 10);

        $this->phoneMapper->method('findAll')->willReturn([$phone]);
        $this->memberMapper->method('findAll')->willReturn([$member]);

        $result = $this->service->exportEntity('telefonnummern');

        $this->assertEquals('Telefonnummern.csv', $result['filename']);
        $this->assertStringContainsString('+4917612345678', $result['content']);
        $this->assertStringContainsString('Mobil', $result['content']);
    }

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

        $email = new Email();
        $email->setMemberId(1);
        $email->setLabel('Privat');
        $email->setEmail('max@test.de');
        $refE = new \ReflectionProperty($email, 'id');
        $refE->setAccessible(true);
        $refE->setValue($email, 10);

        $this->emailMapper->method('findAll')->willReturn([$email]);
        $this->memberMapper->method('findAll')->willReturn([$member]);

        $result = $this->service->exportEntity('emails');

        $this->assertEquals('E-Mails.csv', $result['filename']);
        $this->assertStringContainsString('max@test.de', $result['content']);
    }

    public function testExportEntityStufenverlauf(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $refM = new \ReflectionProperty($member, 'id');
        $refM->setAccessible(true);
        $refM->setValue($member, 1);

        $stufe = new Stufe();
        $stufe->setName('Woelflinge');
        $refS = new \ReflectionProperty($stufe, 'id');
        $refS->setAccessible(true);
        $refS->setValue($stufe, 2);

        $history = new StufeHistory();
        $history->setMemberId(1);
        $history->setStufeId(2);
        $history->setChangedAt('2024-01-01');
        $history->setChangedBy('admin');
        $refH = new \ReflectionProperty($history, 'id');
        $refH->setAccessible(true);
        $refH->setValue($history, 10);

        $this->stufeHistoryMapper->method('findAll')->willReturn([$history]);
        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

        $result = $this->service->exportEntity('stufenverlauf');

        $this->assertEquals('Stufenverlauf.csv', $result['filename']);
        $this->assertStringContainsString('Max Mueller', $result['content']);
        $this->assertStringContainsString('Woelflinge', $result['content']);
    }

    public function testExportEntityBeitragsregeln(): void {
        $rule = new FeeRule();
        $rule->setYearFrom(2026);
        $rule->setBaseRate('120.00');
        $rule->setFamilyRulesJson('{"1":100}');
        $rule->setInactiveRule('freeze');
        $rule->setCreatedAt('2026-01-01');
        $rule->setCreatedBy('admin');
        $refR = new \ReflectionProperty($rule, 'id');
        $refR->setAccessible(true);
        $refR->setValue($rule, 1);

        $this->feeRuleMapper->method('findAll')->willReturn([$rule]);

        $result = $this->service->exportEntity('beitragsregeln');

        $this->assertEquals('Beitragsregeln.csv', $result['filename']);
        $this->assertStringContainsString('2026', $result['content']);
        $this->assertStringContainsString('120.00', $result['content']);
    }

    public function testExportEntityBeitraege(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $refM = new \ReflectionProperty($member, 'id');
        $refM->setAccessible(true);
        $refM->setValue($member, 1);

        $record = new FeeRecord();
        $record->setMemberId(1);
        $record->setYear(2026);
        $record->setAmount('120.00');
        $record->setPaid(true);
        $record->setPaymentDate('2026-03-01');
        $record->setManuellAngepasst(false);
        $record->setNotes('TestNote');
        $refR = new \ReflectionProperty($record, 'id');
        $refR->setAccessible(true);
        $refR->setValue($record, 10);

        $this->feeRecordMapper->method('findAll')->willReturn([$record]);
        $this->memberMapper->method('findAll')->willReturn([$member]);

        $result = $this->service->exportEntity('beitraege');

        $this->assertEquals('Beitraege.csv', $result['filename']);
        $this->assertStringContainsString('120.00', $result['content']);
        $this->assertStringContainsString('Ja', $result['content']);
        $this->assertStringContainsString('Max Mueller', $result['content']);
    }

    public function testExportEntityLagerteilnehmer(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $refM = new \ReflectionProperty($member, 'id');
        $refM->setAccessible(true);
        $refM->setValue($member, 1);

        $lager = new Lager();
        $lager->setName('Sommerlager');
        $refL = new \ReflectionProperty($lager, 'id');
        $refL->setAccessible(true);
        $refL->setValue($lager, 2);

        $teilnehmer = new LagerTeilnehmer();
        $teilnehmer->setLagerId(2);
        $teilnehmer->setMemberId(1);
        $teilnehmer->setRolle('Teilnehmer');
        $refT = new \ReflectionProperty($teilnehmer, 'id');
        $refT->setAccessible(true);
        $refT->setValue($teilnehmer, 10);

        $this->lagerTeilnehmerMapper->method('findAll')->willReturn([$teilnehmer]);
        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->lagerMapper->method('findAll')->willReturn([$lager]);

        $result = $this->service->exportEntity('lagerteilnehmer');

        $this->assertEquals('Lagerteilnehmer.csv', $result['filename']);
        $this->assertStringContainsString('Sommerlager', $result['content']);
        $this->assertStringContainsString('Max Mueller', $result['content']);
    }

    public function testExportEntityVerletzungen(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $refM = new \ReflectionProperty($member, 'id');
        $refM->setAccessible(true);
        $refM->setValue($member, 1);

        $lager = new Lager();
        $lager->setName('Sommerlager');
        $refL = new \ReflectionProperty($lager, 'id');
        $refL->setAccessible(true);
        $refL->setValue($lager, 2);

        $injury = new Injury();
        $injury->setDatum('2024-07-05');
        $injury->setMemberId(1);
        $injury->setLagerId(2);
        $injury->setActivityName('Wandern');
        $injury->setBeschreibung('Knie verletzt');
        $injury->setCreatedBy('admin');
        $injury->setCreatedAt('2024-07-05 12:00:00');
        $refI = new \ReflectionProperty($injury, 'id');
        $refI->setAccessible(true);
        $refI->setValue($injury, 10);

        $this->injuryMapper->method('findAll')->willReturn([$injury]);
        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->lagerMapper->method('findAll')->willReturn([$lager]);

        $result = $this->service->exportEntity('verletzungen');

        $this->assertEquals('Verletzungen.csv', $result['filename']);
        $this->assertStringContainsString('Knie verletzt', $result['content']);
        $this->assertStringContainsString('Wandern', $result['content']);
    }

    public function testExportEntityGespeicherteAbfragen(): void {
        $query = new SavedQuery();
        $query->setName('TestQuery');
        $query->setQueryJson('{"and":[]}');
        $query->setSortJson('{"field":"nachname"}');
        $query->setCreatedBy('admin');
        $query->setCreatedAt('2024-01-01');
        $refQ = new \ReflectionProperty($query, 'id');
        $refQ->setAccessible(true);
        $refQ->setValue($query, 1);

        $this->savedQueryMapper->method('findAll')->willReturn([$query]);

        $result = $this->service->exportEntity('gespeicherte_abfragen');

        $this->assertEquals('Gespeicherte_Abfragen.csv', $result['filename']);
        $this->assertStringContainsString('TestQuery', $result['content']);
    }

    // ── FK resolution edge cases ──────────────────────────────────

    public function testExportEntityMitgliederResolvesNullStufe(): void {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $member->setStufeId(null);
        $member->setFamilyId(null);
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 1);

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportEntity('mitglieder', false);

        // Should not crash with null FK IDs
        $this->assertStringContainsString('Max', $result['content']);
    }

    public function testExportEntityResolvesMemberNameForAddress(): void {
        $member = new Member();
        $member->setVorname('Anna');
        $member->setNachname('Schmidt');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, 5);

        $address = new Address();
        $address->setMemberId(5);
        $address->setStrasse('Test');
        $address->setPlz('00000');
        $address->setOrt('Hamburg');
        $address->setIsPrimary(false);
        $refA = new \ReflectionProperty($address, 'id');
        $refA->setAccessible(true);
        $refA->setValue($address, 1);

        $this->addressMapper->method('findAll')->willReturn([$address]);
        $this->memberMapper->method('findAll')->willReturn([$member]);

        $result = $this->service->exportEntity('adressen');

        $this->assertStringContainsString('Anna Schmidt', $result['content']);
        $this->assertStringContainsString('Nein', $result['content']); // isPrimary = false
    }

    public function testExportEntityResolvesUnknownMemberId(): void {
        $address = new Address();
        $address->setMemberId(999); // Non-existent member
        $address->setStrasse('Test');
        $address->setPlz('00000');
        $address->setOrt('Unknown');
        $address->setIsPrimary(false);
        $refA = new \ReflectionProperty($address, 'id');
        $refA->setAccessible(true);
        $refA->setValue($address, 1);

        $this->addressMapper->method('findAll')->willReturn([$address]);
        $this->memberMapper->method('findAll')->willReturn([]); // No members

        $result = $this->service->exportEntity('adressen');

        // Should still work, just empty member name
        $this->assertStringContainsString('Test', $result['content']);
    }

    public function testExportEntityResolvesLagerNameForTeilnehmer(): void {
        $lager = new Lager();
        $lager->setName('Winterlager');
        $refL = new \ReflectionProperty($lager, 'id');
        $refL->setAccessible(true);
        $refL->setValue($lager, 3);

        $teilnehmer = new LagerTeilnehmer();
        $teilnehmer->setLagerId(3);
        $teilnehmer->setMemberId(999); // Non-existent member
        $teilnehmer->setRolle('Leiter');
        $refT = new \ReflectionProperty($teilnehmer, 'id');
        $refT->setAccessible(true);
        $refT->setValue($teilnehmer, 1);

        $this->lagerTeilnehmerMapper->method('findAll')->willReturn([$teilnehmer]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->lagerMapper->method('findAll')->willReturn([$lager]);

        $result = $this->service->exportEntity('lagerteilnehmer');

        $this->assertStringContainsString('Winterlager', $result['content']);
    }

    // ── Familien with empty encrypted fields ──────────────────────

    public function testExportEntityFamilienEmptyEncryptedFields(): void {
        $family = new Family();
        $family->setName('Schmidt');
        // No kontoinhaberEncrypted or ibanEncrypted set
        $ref = new \ReflectionProperty($family, 'id');
        $ref->setAccessible(true);
        $ref->setValue($family, 1);

        $this->familyMapper->method('findAll')->willReturn([$family]);

        $result = $this->service->exportEntity('familien', false);

        // Should not show [verschluesselt] for null encrypted fields
        $this->assertStringNotContainsString('[verschluesselt]', $result['content']);
    }

    public function testExportEntityFamilienSensitiveEmptyEncrypted(): void {
        $family = new Family();
        $family->setName('Schmidt');
        // No kontoinhaber or iban encrypted
        $ref = new \ReflectionProperty($family, 'id');
        $ref->setAccessible(true);
        $ref->setValue($family, 1);

        $this->familyMapper->method('findAll')->willReturn([$family]);

        $result = $this->service->exportEntity('familien', true);

        // Should show empty fields, not [verschluesselt]
        $this->assertStringNotContainsString('[verschluesselt]', $result['content']);
    }

    // ── CSV escaping edge cases ───────────────────────────────────

    public function testBuildCsvEscapesCarriageReturn(): void {
        $csv = $this->service->buildCsv(['A'], [["value\rwith\rCR"]]);
        $this->assertStringContainsString("\"value\rwith\rCR\"", $csv);
    }

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

        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->addressMapper->method('findAll')->willReturn([]);
        $this->phoneMapper->method('findAll')->willReturn([]);
        $this->emailMapper->method('findAll')->willReturn([]);
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->stufeHistoryMapper->method('findAll')->willReturn([]);
        $this->feeRuleMapper->method('findAll')->willReturn([]);
        $this->feeRecordMapper->method('findAll')->willReturn([]);
        $this->lagerMapper->method('findAll')->willReturn([]);
        $this->lagerTeilnehmerMapper->method('findAll')->willReturn([]);
        $this->injuryMapper->method('findAll')->willReturn([]);
        $this->savedQueryMapper->method('findAll')->willReturn([]);

        $result = $this->service->exportFullBundle(true);

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

        // Cleanup
        unlink($result['path']);
    }
}
