<?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\Injury;
use OCA\Mitgliederverwaltung\Db\InjuryMapper;
use OCA\Mitgliederverwaltung\Db\Lager;
use OCA\Mitgliederverwaltung\Db\LagerMapper;
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmer;
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmerMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\Stufe;
use OCA\Mitgliederverwaltung\Db\StufeMapper;
use OCA\Mitgliederverwaltung\Service\EncryptionService;
use OCA\Mitgliederverwaltung\Service\ReportService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class ReportServiceTest extends TestCase {

    private ReportService $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 StufeMapper&MockObject $stufeMapper;
    private LagerMapper&MockObject $lagerMapper;
    private LagerTeilnehmerMapper&MockObject $teilnehmerMapper;
    private InjuryMapper&MockObject $injuryMapper;
    private EncryptionService&MockObject $encryptionService;
    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->stufeMapper = $this->createMock(StufeMapper::class);
        $this->lagerMapper = $this->createMock(LagerMapper::class);
        $this->teilnehmerMapper = $this->createMock(LagerTeilnehmerMapper::class);
        $this->injuryMapper = $this->createMock(InjuryMapper::class);
        $this->encryptionService = $this->createMock(EncryptionService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new ReportService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->feeRecordMapper,
            $this->stufeMapper,
            $this->lagerMapper,
            $this->teilnehmerMapper,
            $this->injuryMapper,
            $this->encryptionService,
            $this->logger
        );
    }

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

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

    private function createStufe(int $id, string $name): Stufe {
        $stufe = new Stufe();
        $stufe->setId($id);
        $stufe->setName($name);
        $stufe->setSortOrder($id);
        return $stufe;
    }

    private function createFeeRecord(
        int $id = 1,
        int $memberId = 1,
        int $year = 2026,
        string $amount = '120.00',
        bool $paid = false,
        ?string $paymentDate = null
    ): FeeRecord {
        $record = new FeeRecord();
        $record->setId($id);
        $record->setMemberId($memberId);
        $record->setYear($year);
        $record->setAmount($amount);
        $record->setPaid($paid);
        $record->setPaymentDate($paymentDate);
        $record->setCreatedAt('2026-01-01 00:00:00');
        $record->setUpdatedAt('2026-01-01 00:00:00');
        return $record;
    }

    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 createLager(int $id = 1, string $name = 'Sommerlager 2026'): Lager {
        $lager = new Lager();
        $lager->setId($id);
        $lager->setName($name);
        $lager->setStartdatum('2026-07-15');
        $lager->setEnddatum('2026-07-22');
        $lager->setOrt('Schwarzwald');
        $lager->setCreatedAt('2026-01-01 00:00:00');
        $lager->setUpdatedAt('2026-01-01 00:00:00');
        return $lager;
    }

    private function createTeilnehmer(int $lagerId = 1, int $memberId = 1, string $rolle = 'Teilnehmer'): LagerTeilnehmer {
        $t = new LagerTeilnehmer();
        $t->setLagerId($lagerId);
        $t->setMemberId($memberId);
        $t->setRolle($rolle);
        return $t;
    }

    private function createInjury(int $id = 1, int $memberId = 1, ?int $lagerId = null): Injury {
        $injury = new Injury();
        $injury->setId($id);
        $injury->setDatum('2026-07-18');
        $injury->setMemberId($memberId);
        $injury->setLagerId($lagerId);
        $injury->setActivityName('Wanderung');
        $injury->setBeschreibung('Knieverletzung');
        $injury->setCreatedBy('admin');
        $injury->setCreatedAt('2026-07-18 14:00:00');
        return $injury;
    }

    // ── generateMitgliederliste() ───────────────────────────────────

    public function testMitgliederlisteReturnsCorrectStructure(): void {
        $stufe = $this->createStufe(1, 'Woelflinge');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', 1);

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

        $result = $this->service->generateMitgliederliste();

        $this->assertSame('Mitgliederliste', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Geburtsdatum', 'Stufe', 'Status', 'Eintritt'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

    public function testMitgliederlisteRowContainsCorrectData(): void {
        $stufe = $this->createStufe(1, 'Woelflinge');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', 1, '2020-03-15');

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

        $result = $this->service->generateMitgliederliste();
        $row = $result['rows'][0];

        $this->assertSame('Mustermann', $row[0]);
        $this->assertSame('Max', $row[1]);
        $this->assertSame('15.01.2010', $row[2]);
        $this->assertSame('Woelflinge', $row[3]);
        $this->assertSame('Aktiv', $row[4]);
        $this->assertSame('15.03.2020', $row[5]);
    }

    public function testMitgliederlisteWithStatusFilter(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv');

        $this->memberMapper->expects($this->once())
            ->method('findByStatus')
            ->with('aktiv')
            ->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([]);

        $result = $this->service->generateMitgliederliste('aktiv');

        $this->assertStringContainsString('Aktiv', $result['title']);
        $this->assertCount(1, $result['rows']);
    }

    public function testMitgliederlisteWithoutFilter(): void {
        $this->memberMapper->expects($this->once())
            ->method('findAll')
            ->willReturn([]);
        $this->stufeMapper->method('findAll')->willReturn([]);

        $result = $this->service->generateMitgliederliste();

        $this->assertSame('Mitgliederliste', $result['title']);
        $this->assertSame([], $result['rows']);
    }

    public function testMitgliederlisteMemberWithoutStufe(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', null);

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

        $result = $this->service->generateMitgliederliste();

        // No stufe should show dash
        $this->assertSame('–', $result['rows'][0][3]);
    }

    public function testMitgliederlisteEmptyStatusFilter(): void {
        $this->memberMapper->expects($this->once())
            ->method('findAll')
            ->willReturn([]);
        $this->stufeMapper->method('findAll')->willReturn([]);

        // Empty string filter should behave like no filter
        $result = $this->service->generateMitgliederliste('');
        $this->assertSame('Mitgliederliste', $result['title']);
    }

    // ── generateBeitragsliste() ─────────────────────────────────────

    public function testBeitragslisteReturnsCorrectStructure(): void {
        $record = $this->createFeeRecord(1, 1, 2026, '120.00', true, '2026-03-15');
        $member = $this->createMember(1, 'Max', 'Mustermann');

        $this->feeRecordMapper->method('findByYear')->with(2026)->willReturn([$record]);
        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        $result = $this->service->generateBeitragsliste(2026);

        $this->assertSame('Beitragsliste 2026', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Betrag', 'Bezahlt', 'Zahlungsdatum'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

    public function testBeitragslisteRowContainsCorrectData(): void {
        $record = $this->createFeeRecord(1, 1, 2026, '120.00', true, '2026-03-15');
        $member = $this->createMember(1, 'Max', 'Mustermann');

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

        $result = $this->service->generateBeitragsliste(2026);
        $row = $result['rows'][0];

        $this->assertSame('Mustermann', $row[0]);
        $this->assertSame('Max', $row[1]);
        $this->assertSame('120,00 EUR', $row[2]);
        $this->assertSame('Ja', $row[3]);
        $this->assertSame('15.03.2026', $row[4]);
    }

    public function testBeitragslisteUnpaidRecord(): void {
        $record = $this->createFeeRecord(1, 1, 2026, '120.00', false, null);
        $member = $this->createMember(1);

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

        $result = $this->service->generateBeitragsliste(2026);
        $row = $result['rows'][0];

        $this->assertSame('Nein', $row[3]);
        $this->assertSame('–', $row[4]);
    }

    public function testBeitragslisteDeletedMember(): void {
        $record = $this->createFeeRecord(1, 999, 2026);

        $this->feeRecordMapper->method('findByYear')->willReturn([$record]);
        $this->memberMapper->method('findById')
            ->willThrowException(new \Exception('Deleted'));

        $result = $this->service->generateBeitragsliste(2026);
        $row = $result['rows'][0];

        $this->assertSame('Unbekannt', $row[0]);
        $this->assertSame('', $row[1]);
    }

    public function testBeitragslisteWithYear(): void {
        $this->feeRecordMapper->expects($this->once())
            ->method('findByYear')
            ->with(2025)
            ->willReturn([]);

        $result = $this->service->generateBeitragsliste(2025);

        $this->assertSame('Beitragsliste 2025', $result['title']);
        $this->assertSame([], $result['rows']);
    }

    // ── generateGeburtstagsliste() ──────────────────────────────────

    public function testGeburtstagslisteReturnsCorrectStructure(): void {
        $this->memberMapper->method('findByBirthdayMonth')->willReturn([]);

        $result = $this->service->generateGeburtstagsliste(3);

        $this->assertStringContainsString('Geburtstagsliste', $result['title']);
        $this->assertStringContainsString('3 Monate', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Geburtsdatum', 'Alter', 'Monat'], $result['headers']);
    }

    public function testGeburtstagslisteDefault3Months(): void {
        $this->memberMapper->method('findByBirthdayMonth')->willReturn([]);

        $result = $this->service->generateGeburtstagsliste();

        $this->assertStringContainsString('3 Monate', $result['title']);
    }

    public function testGeburtstagslisteReturnsCorrectMonthName(): void {
        $currentMonth = (int)(new \DateTime())->format('m');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15');

        $this->memberMapper->method('findByBirthdayMonth')
            ->willReturnCallback(function (int $month) use ($currentMonth, $member) {
                return $month === $currentMonth ? [$member] : [];
            });

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

        $this->assertCount(1, $result['rows']);
        // Verify month name is present
        $row = $result['rows'][0];
        $this->assertNotEmpty($row[4]); // Month name
    }

    public function testGeburtstagslisteCalculatesAge(): void {
        $currentMonth = (int)(new \DateTime())->format('m');
        $now = new \DateTime();
        // Create a member born 16 years ago on the 1st of the current month (always in the past)
        $birthYear = (int)$now->format('Y') - 16;
        $birthday = "{$birthYear}-" . str_pad((string)$currentMonth, 2, '0', STR_PAD_LEFT) . "-01";
        $member = $this->createMember(1, 'Max', 'Mustermann', $birthday);

        $this->memberMapper->method('findByBirthdayMonth')
            ->willReturnCallback(function (int $month) use ($currentMonth, $member) {
                return $month === $currentMonth ? [$member] : [];
            });

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

        $this->assertCount(1, $result['rows']);
        // Calculate expected age
        $expectedAge = $now->diff(new \DateTime($birthday))->y;
        $this->assertSame((string)$expectedAge, $result['rows'][0][3]);
    }

    public function testGeburtstagslisteWithMonthsParam(): void {
        $this->memberMapper->method('findByBirthdayMonth')->willReturn([]);

        $result = $this->service->generateGeburtstagsliste(6);

        $this->assertStringContainsString('6 Monate', $result['title']);
    }

    // ── generateBankverbindungen() ──────────────────────────────────

    public function testBankverbindungenReturnsCorrectStructure(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', null);

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

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

        $this->assertSame('Bankverbindungen', $result['title']);
        $this->assertSame(['Familie', 'Kontoinhaber', 'IBAN', 'BIC', 'Kreditinstitut', 'Mitglieder'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

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

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

        $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->generateBankverbindungen();
        $row = $result['rows'][0];

        $this->assertSame('Mustermann', $row[0]);
        $this->assertSame('Max Mustermann', $row[1]);
        $this->assertSame('DE89370400440532013000', $row[2]);
        $this->assertSame('COBADEFFXXX', $row[3]);
        $this->assertSame('Commerzbank', $row[4]);
        $this->assertSame('Max Mustermann', $row[5]);
    }

    public function testBankverbindungenMultipleMembers(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member1 = $this->createMember(1, 'Max', 'Mustermann');
        $member2 = $this->createMember(2, 'Anna', 'Mustermann');

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->memberMapper->method('findByFamily')->willReturn([$member1, $member2]);
        $this->encryptionService->method('decrypt')->willReturn('decrypted');

        $result = $this->service->generateBankverbindungen();

        // Members should be joined with comma
        $this->assertStringContainsString('Max Mustermann', $result['rows'][0][5]);
        $this->assertStringContainsString('Anna Mustermann', $result['rows'][0][5]);
    }

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

        $result = $this->service->generateBankverbindungen();

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

    public function testBankverbindungenNullBankingData(): void {
        $family = new Family();
        $family->setId(1);
        $family->setName('Test');
        $family->setCreatedAt('2020-01-01 00:00:00');
        $family->setUpdatedAt('2020-01-01 00:00:00');
        // No banking data set (nulls)

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->memberMapper->method('findByFamily')->willReturn([]);
        $this->encryptionService->method('decrypt')->willReturn(null);

        $result = $this->service->generateBankverbindungen();
        $row = $result['rows'][0];

        $this->assertSame('–', $row[1]); // kontoinhaber
        $this->assertSame('–', $row[2]); // iban
        $this->assertSame('–', $row[3]); // bic
        $this->assertSame('–', $row[4]); // kreditinstitut
    }

    // ── generateFamilienliste() ─────────────────────────────────────

    public function testFamilienlisteReturnsCorrectStructure(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied');

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

        $result = $this->service->generateFamilienliste();

        $this->assertSame('Familienliste', $result['title']);
        $this->assertSame(['Familie', 'Mitglied', 'Rolle', 'Anzahl'], $result['headers']);
    }

    public function testFamilienlisteRowContainsCorrectData(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied');

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

        $result = $this->service->generateFamilienliste();
        $row = $result['rows'][0];

        $this->assertSame('Mustermann', $row[0]);
        $this->assertSame('Max Mustermann', $row[1]);
        $this->assertSame('Mitglied', $row[2]);
        $this->assertSame('1', $row[3]);
    }

    public function testFamilienlisteEmptyFamily(): void {
        $family = $this->createFamily(1, 'Leer');

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

        $result = $this->service->generateFamilienliste();
        $row = $result['rows'][0];

        $this->assertSame('Leer', $row[0]);
        $this->assertSame('–', $row[1]);
        $this->assertSame('–', $row[2]);
        $this->assertSame('0', $row[3]);
    }

    public function testFamilienlisteMultipleMembers(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member1 = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied');
        $member2 = $this->createMember(2, 'Hans', 'Mustermann', '1980-01-15', 'aktiv', 'erziehungsberechtigter');

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->memberMapper->method('findByFamily')->willReturn([$member1, $member2]);

        $result = $this->service->generateFamilienliste();

        // First row has family name and count
        $this->assertSame('Mustermann', $result['rows'][0][0]);
        $this->assertSame('2', $result['rows'][0][3]);
        // Second row has empty family name
        $this->assertSame('', $result['rows'][1][0]);
        $this->assertSame('', $result['rows'][1][3]);
    }

    public function testFamilienlisteFormatsRollen(): void {
        $family = $this->createFamily(1, 'Test');
        $mitglied = $this->createMember(1, 'Max', 'Test', '2010-01-15', 'aktiv', 'mitglied');
        $parent = $this->createMember(2, 'Hans', 'Test', '1980-01-15', 'aktiv', 'erziehungsberechtigter');

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->memberMapper->method('findByFamily')->willReturn([$mitglied, $parent]);

        $result = $this->service->generateFamilienliste();

        $this->assertSame('Mitglied', $result['rows'][0][2]);
        $this->assertSame('Erziehungsberechtigter', $result['rows'][1][2]);
    }

    // ── generateAllergieliste() ─────────────────────────────────────

    public function testAllergiePlisteReturnsCorrectStructure(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann');
        $member->setAllergienEncrypted('ENC:Nussallergie');
        $member->setKvTyp('gesetzlich');
        $member->setKvName('AOK');

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->encryptionService->method('decrypt')
            ->with('ENC:Nussallergie')
            ->willReturn('Nussallergie');

        $result = $this->service->generateAllergieliste();

        $this->assertSame('Allergieliste', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Allergien', 'KV-Typ', 'KV-Name'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

    public function testAllergielisteSkipsMembersWithoutAllergyOrKv(): void {
        $memberWithAllergy = $this->createMember(1, 'Max', 'Mustermann');
        $memberWithAllergy->setAllergienEncrypted('ENC:Nussallergie');

        $memberWithout = $this->createMember(2, 'Anna', 'Schmidt');

        $this->memberMapper->method('findAll')->willReturn([$memberWithAllergy, $memberWithout]);
        $this->encryptionService->method('decrypt')
            ->willReturnCallback(function (?string $value) {
                if ($value === 'ENC:Nussallergie') return 'Nussallergie';
                return null;
            });

        $result = $this->service->generateAllergieliste();

        $this->assertCount(1, $result['rows']);
        $this->assertSame('Max', $result['rows'][0][1]);
    }

    public function testAllergielisteMemberWithOnlyKv(): void {
        $member = $this->createMember(1, 'Anna', 'Schmidt');
        $member->setKvTyp('privat');
        $member->setKvName('Allianz');

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

        $result = $this->service->generateAllergieliste();

        $this->assertCount(1, $result['rows']);
        $this->assertSame('–', $result['rows'][0][2]); // No allergies
        $this->assertSame('privat', $result['rows'][0][3]);
        $this->assertSame('Allianz', $result['rows'][0][4]);
    }

    // ── generateStufenliste() ───────────────────────────────────────

    public function testStufenlisteReturnsCorrectStructure(): void {
        $stufe = $this->createStufe(1, 'Woelflinge');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', 1);

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

        $result = $this->service->generateStufenliste();

        $this->assertSame('Stufenliste', $result['title']);
        $this->assertSame(['Stufe', 'Nachname', 'Vorname', 'Geburtsdatum', 'Anzahl'], $result['headers']);
    }

    public function testStufenlisteEmptyStufe(): void {
        $stufe = $this->createStufe(1, 'Woelflinge');

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

        $result = $this->service->generateStufenliste();

        $this->assertCount(1, $result['rows']);
        $row = $result['rows'][0];
        $this->assertSame('Woelflinge', $row[0]);
        $this->assertSame('–', $row[1]);
        $this->assertSame('0', $row[4]);
    }

    public function testStufenlisteIncludesMembersWithoutStufe(): void {
        $memberNoStufe = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', null);

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

        $result = $this->service->generateStufenliste();

        $this->assertCount(1, $result['rows']);
        $this->assertSame('Keine Stufe', $result['rows'][0][0]);
    }

    // ── generateKontaktliste() ──────────────────────────────────────

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

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

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

        $result = $this->service->generateKontaktliste();

        $this->assertSame('Kontaktliste', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Strasse', 'PLZ/Ort', 'Telefon', 'E-Mail'], $result['headers']);
        $this->assertCount(1, $result['rows']);

        $row = $result['rows'][0];
        $this->assertSame('Hauptstr. 1', $row[2]);
        $this->assertSame('12345 Berlin', $row[3]);
        $this->assertSame('+4917612345678', $row[4]);
        $this->assertSame('max@example.com', $row[5]);
    }

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

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

        $result = $this->service->generateKontaktliste();
        $row = $result['rows'][0];

        $this->assertSame('–', $row[2]);
        $this->assertSame('–', $row[3]);
        $this->assertSame('–', $row[4]);
        $this->assertSame('–', $row[5]);
    }

    public function testKontaktlisteFilteredByStufe(): void {
        $stufe = $this->createStufe(1, 'Woelflinge');
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv', 'mitglied', 1);

        $this->memberMapper->expects($this->once())
            ->method('findByStufe')
            ->with(1)
            ->willReturn([$member]);
        $this->stufeMapper->method('findById')->with(1)->willReturn($stufe);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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

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

    // ── generateLagerhistorie() ─────────────────────────────────────

    public function testLagerhistorieReturnsCorrectStructure(): void {
        $lager = $this->createLager(1, 'Sommerlager 2026');
        $teilnehmer = $this->createTeilnehmer(1, 1, 'Teilnehmer');
        $member = $this->createMember(1, 'Max', 'Mustermann');

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

        $result = $this->service->generateLagerhistorie();

        $this->assertSame('Lagerhistorie', $result['title']);
        $this->assertSame(['Nachname', 'Vorname', 'Lager', 'Von', 'Bis', 'Ort', 'Rolle'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

    public function testLagerhistorieFilteredByMember(): void {
        $lager = $this->createLager(1, 'Sommerlager');
        $teilnehmer1 = $this->createTeilnehmer(1, 1, 'Teilnehmer');
        $teilnehmer2 = $this->createTeilnehmer(1, 2, 'Leiter');
        $member1 = $this->createMember(1, 'Max', 'Mustermann');

        $this->lagerMapper->method('findAll')->willReturn([$lager]);
        $this->teilnehmerMapper->method('findByLagerId')->willReturn([$teilnehmer1, $teilnehmer2]);
        $this->memberMapper->method('findById')
            ->willReturnCallback(function (int $id) use ($member1) {
                if ($id === 1) return $member1;
                return $this->createMember(2, 'Anna', 'Schmidt');
            });

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

        // Only member 1's entries
        $this->assertCount(1, $result['rows']);
        $this->assertStringContainsString('Max Mustermann', $result['title']);
    }

    public function testLagerhistorieDeletedMember(): void {
        $lager = $this->createLager(1);
        $teilnehmer = $this->createTeilnehmer(1, 999);

        $this->lagerMapper->method('findAll')->willReturn([$lager]);
        $this->teilnehmerMapper->method('findByLagerId')->willReturn([$teilnehmer]);
        $this->memberMapper->method('findById')
            ->willThrowException(new \Exception('Deleted'));

        $result = $this->service->generateLagerhistorie();

        $this->assertSame('Unbekannt', $result['rows'][0][0]);
        $this->assertSame('', $result['rows'][0][1]);
    }

    // ── generateVerletzungsprotokoll() ──────────────────────────────

    public function testVerletzungsprotokollReturnsCorrectStructure(): void {
        $injury = $this->createInjury(1, 1, 1);
        $member = $this->createMember(1, 'Max', 'Mustermann');
        $lager = $this->createLager(1, 'Sommerlager');

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

        $result = $this->service->generateVerletzungsprotokoll();

        $this->assertSame('Verletzungsprotokoll', $result['title']);
        $this->assertSame(['Datum', 'Nachname', 'Vorname', 'Beschreibung', 'Lager', 'Aktivitaet'], $result['headers']);
        $this->assertCount(1, $result['rows']);
    }

    public function testVerletzungsprotokollRowContainsCorrectData(): void {
        $injury = $this->createInjury(1, 1, 1);
        $member = $this->createMember(1, 'Max', 'Mustermann');
        $lager = $this->createLager(1, 'Sommerlager');

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

        $result = $this->service->generateVerletzungsprotokoll();
        $row = $result['rows'][0];

        $this->assertSame('18.07.2026', $row[0]);
        $this->assertSame('Mustermann', $row[1]);
        $this->assertSame('Max', $row[2]);
        $this->assertSame('Knieverletzung', $row[3]);
        $this->assertSame('Sommerlager', $row[4]);
        $this->assertSame('Wanderung', $row[5]);
    }

    public function testVerletzungsprotokollFilteredByDateRange(): void {
        $this->injuryMapper->expects($this->once())
            ->method('findByDateRange')
            ->with('2026-01-01', '2026-06-30')
            ->willReturn([]);

        $result = $this->service->generateVerletzungsprotokoll('2026-01-01', '2026-06-30');

        $this->assertStringContainsString('01.01.2026', $result['title']);
        $this->assertStringContainsString('30.06.2026', $result['title']);
    }

    public function testVerletzungsprotokollFilteredByMember(): void {
        $injury = $this->createInjury(1, 5, null);
        $member = $this->createMember(5, 'Anna', 'Schmidt');

        $this->injuryMapper->expects($this->once())
            ->method('findByMemberId')
            ->with(5)
            ->willReturn([$injury]);
        $this->memberMapper->method('findById')
            ->willReturnCallback(function (int $id) use ($member) {
                if ($id === 5) return $member;
                throw new \Exception('Not found');
            });

        $result = $this->service->generateVerletzungsprotokoll(null, null, 5);

        $this->assertStringContainsString('Anna Schmidt', $result['title']);
    }

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

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

        $result = $this->service->generateVerletzungsprotokoll();
        $row = $result['rows'][0];

        $this->assertSame('–', $row[4]); // No lager
        $this->assertSame('–', $row[5]); // No activity
    }

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

        $this->injuryMapper->method('findAll')->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);
        $this->lagerMapper->method('findById')
            ->willThrowException(new \Exception('Deleted'));

        $result = $this->service->generateVerletzungsprotokoll();

        $this->assertSame('–', $result['rows'][0][4]);
    }

    // ── Helper method tests (formatDate, formatStatus, formatRolle) ─

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

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

        $result = $this->service->generateMitgliederliste();

        $this->assertSame('15.03.2010', $result['rows'][0][2]);
    }

    public function testFormatDateNullShowsDash(): void {
        // Testing via beitragsliste where paymentDate can be null
        $record = $this->createFeeRecord(1, 1, 2026, '100.00', false, null);
        $member = $this->createMember(1);

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

        $result = $this->service->generateBeitragsliste(2026);

        $this->assertSame('–', $result['rows'][0][4]);
    }

    public function testFormatStatusViaReport(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'aktiv');

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

        $result = $this->service->generateMitgliederliste();

        $this->assertSame('Aktiv', $result['rows'][0][4]);
    }

    public function testFormatStatusInaktiv(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'inaktiv');

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

        $result = $this->service->generateMitgliederliste('inaktiv');

        $this->assertSame('Inaktiv', $result['rows'][0][4]);
    }

    public function testFormatStatusGeloescht(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'geloescht');

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

        $result = $this->service->generateMitgliederliste('geloescht');

        $this->assertSame('Geloescht', $result['rows'][0][4]);
    }

    public function testFormatStatusUnknownPassesThrough(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', 'custom_status');

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

        $result = $this->service->generateMitgliederliste('custom_status');

        $this->assertSame('custom_status', $result['rows'][0][4]);
    }

    public function testFormatRolleViaFamilienliste(): void {
        $family = $this->createFamily(1, 'Test');
        $member = $this->createMember(1, 'Max', 'Test', '2010-01-15', 'aktiv', 'mitglied');

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

        $result = $this->service->generateFamilienliste();

        $this->assertSame('Mitglied', $result['rows'][0][2]);
    }

    public function testFormatRolleUnknownPassesThrough(): void {
        $family = $this->createFamily(1, 'Test');
        $member = $this->createMember(1, 'Max', 'Test', '2010-01-15', 'aktiv', 'custom_rolle');

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

        $result = $this->service->generateFamilienliste();

        $this->assertSame('custom_rolle', $result['rows'][0][2]);
    }
}
