<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\AuditLogMapper;
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\PermissionMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\SavedQueryMapper;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Db\StufeMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\ICompositeExpression;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * Comprehensive tests for all mapper classes.
 *
 * These tests exercise the actual query-building methods by mocking
 * the IDBConnection/IQueryBuilder/IResult chain and verifying that
 * mapper methods execute without errors and return expected entity types.
 */
class MapperTest extends TestCase {

    private IDBConnection&MockObject $db;
    private IQueryBuilder&MockObject $qb;
    private IExpressionBuilder&MockObject $expr;
    private IResult&MockObject $result;

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

        $this->db = $this->createMock(IDBConnection::class);
        $this->qb = $this->createMock(IQueryBuilder::class);
        $this->expr = $this->createMock(IExpressionBuilder::class);
        $this->result = $this->createMock(IResult::class);

        // Configure the query builder to chain fluently
        $this->qb->method('expr')->willReturn($this->expr);
        $this->qb->method('select')->willReturnSelf();
        $this->qb->method('selectDistinct')->willReturnSelf();
        $this->qb->method('from')->willReturnSelf();
        $this->qb->method('where')->willReturnSelf();
        $this->qb->method('andWhere')->willReturnSelf();
        $this->qb->method('orWhere')->willReturnSelf();
        $this->qb->method('orderBy')->willReturnSelf();
        $this->qb->method('addOrderBy')->willReturnSelf();
        $this->qb->method('setMaxResults')->willReturnSelf();
        $this->qb->method('setFirstResult')->willReturnSelf();
        $this->qb->method('innerJoin')->willReturnSelf();
        $this->qb->method('leftJoin')->willReturnSelf();
        $this->qb->method('insert')->willReturnSelf();
        $this->qb->method('delete')->willReturnSelf();
        $this->qb->method('values')->willReturnSelf();
        $this->qb->method('createNamedParameter')->willReturn(':param');
        $this->qb->method('createFunction')->willReturnCallback(fn($call) => $call);
        $this->qb->method('getSQL')->willReturn('SELECT * FROM test');

        // Expression builder returns string predicates
        $compositeExpr = $this->createMock(ICompositeExpression::class);
        $this->expr->method('eq')->willReturn('col = :param');
        $this->expr->method('neq')->willReturn('col != :param');
        $this->expr->method('like')->willReturn('col LIKE :param');
        $this->expr->method('iLike')->willReturn('col ILIKE :param');
        $this->expr->method('isNull')->willReturn('col IS NULL');
        $this->expr->method('gte')->willReturn('col >= :param');
        $this->expr->method('lte')->willReturn('col <= :param');
        $this->expr->method('orX')->willReturn($compositeExpr);

        // DB connection returns query builder and provides escapeLikeParameter
        $this->db->method('getQueryBuilder')->willReturn($this->qb);
        $this->db->method('escapeLikeParameter')->willReturnCallback(fn($s) => $s);
    }

    /**
     * Configure the IResult mock to return rows and then false (end-of-results).
     *
     * @param array[] $rows Rows to return from fetch()
     */
    private function configureResultRows(array $rows): void {
        $fetchCalls = array_merge($rows, [false]);
        $this->result->method('fetch')
            ->willReturnOnConsecutiveCalls(...$fetchCalls);
        $this->result->method('closeCursor')->willReturn(true);
        $this->qb->method('executeQuery')->willReturn($this->result);
    }

    /**
     * Configure the IResult mock to return a single row (for findEntity).
     * findEntity calls fetch() twice: once for the row, once to verify no second row.
     */
    private function configureResultSingleRow(array $row): void {
        $this->result->method('fetch')
            ->willReturnOnConsecutiveCalls($row, false);
        $this->result->method('closeCursor')->willReturn(true);
        $this->qb->method('executeQuery')->willReturn($this->result);
    }

    /**
     * Configure the IResult mock to return no rows (for DoesNotExistException).
     */
    private function configureResultEmpty(): void {
        $this->result->method('fetch')->willReturn(false);
        $this->result->method('closeCursor')->willReturn(true);
        $this->qb->method('executeQuery')->willReturn($this->result);
    }

    /**
     * Configure the IResult mock for count queries (fetchOne returns a number).
     */
    private function configureResultCount(int $count): void {
        $this->result->method('fetchOne')->willReturn((string)$count);
        $this->result->method('closeCursor')->willReturn(true);
        $this->qb->method('executeQuery')->willReturn($this->result);
    }

    /**
     * Configure the query builder for executeStatement (insert/delete/update).
     */
    private function configureExecuteStatement(int $affected = 1): void {
        $this->qb->method('executeStatement')->willReturn($affected);
    }

    // ── Helper: sample row data ─────────────────────────────────────

    private function memberRow(int $id = 1): array {
        return [
            'id' => $id,
            'vorname' => 'Max',
            'nachname' => 'Mustermann',
            'geburtsdatum' => '1990-06-15',
            'geschlecht' => 'Maennlich',
            'rolle' => 'mitglied',
            'stufe_id' => 1,
            'eintritt' => '2020-01-01',
            'austritt' => null,
            'status' => 'aktiv',
            'allergien_encrypted' => null,
            'notizen' => null,
            'zusatz_notizen' => null,
            'kv_typ' => null,
            'kv_name' => null,
            'family_id' => 1,
            'frozen_fee_rate' => null,
            'calendar_event_uri' => null,
            'contact_vcard_uri' => null,
            'created_at' => '2025-01-01 00:00:00',
            'updated_at' => '2025-01-01 00:00:00',
            'deleted_at' => null,
            'einwilligung_datum' => null,
        ];
    }

    private function familyRow(int $id = 1): array {
        return [
            'id' => $id,
            'name' => 'Familie Mustermann',
            'kontoinhaber_encrypted' => null,
            'iban_encrypted' => null,
            'bic' => null,
            'kreditinstitut' => null,
            'created_at' => '2025-01-01',
            'updated_at' => '2025-01-01',
        ];
    }

    private function addressRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'label' => null,
            'strasse' => 'Hauptstr. 1',
            'plz' => '12345',
            'ort' => 'Berlin',
            'land' => 'Deutschland',
            'is_primary' => 1,
        ];
    }

    private function phoneRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'label' => 'Mobil',
            'number_e164' => '+491701234567',
        ];
    }

    private function emailRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'label' => 'Privat',
            'email' => 'max@example.com',
        ];
    }

    private function feeRecordRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'year' => 2026,
            'amount' => '120.00',
            'rule_snapshot_json' => null,
            'manuell_angepasst' => 0,
            'paid' => 0,
            'payment_date' => null,
            'notes' => null,
            'created_at' => '2025-01-01',
            'updated_at' => '2025-01-01',
        ];
    }

    private function feeRuleRow(int $id = 1): array {
        return [
            'id' => $id,
            'year_from' => 2025,
            'base_rate' => '120.00',
            'family_rules_json' => '{}',
            'inactive_rule' => 'freeze_rate_at_exit',
            'created_at' => '2025-01-01',
            'created_by' => 'admin',
        ];
    }

    private function injuryRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'lager_id' => null,
            'activity_name' => null,
            'datum' => '2026-03-15',
            'beschreibung' => 'Sturz beim Wandern',
            'created_by' => 'admin',
            'created_at' => '2025-01-01',
        ];
    }

    private function lagerRow(int $id = 1): array {
        return [
            'id' => $id,
            'name' => 'Sommerlager 2026',
            'startdatum' => '2026-07-15',
            'enddatum' => '2026-07-25',
            'ort' => 'Allgaeu',
            'beschreibung' => null,
            'files_folder_path' => null,
            'created_at' => '2025-01-01',
            'updated_at' => '2025-01-01',
        ];
    }

    private function lagerTeilnehmerRow(int $id = 1): array {
        return [
            'id' => $id,
            'lager_id' => 1,
            'member_id' => 1,
            'rolle' => 'Teilnehmer',
        ];
    }

    private function auditLogRow(int $id = 1): array {
        return [
            'id' => $id,
            'zeitpunkt' => '2025-01-01 12:00:00',
            'nc_user_id' => 'admin',
            'aktion' => 'create',
            'entitaet' => 'member',
            'entitaet_id' => 1,
            'feld' => null,
            'alter_wert' => null,
            'neuer_wert' => null,
        ];
    }

    private function permissionRow(int $id = 1): array {
        return [
            'id' => $id,
            'nc_user_id' => 'admin',
            'level' => 'admin',
            'allowed_stufen_json' => null,
            'can_see_banking' => 0,
        ];
    }

    private function savedQueryRow(int $id = 1): array {
        return [
            'id' => $id,
            'name' => 'Test Query',
            'query_json' => '{"filters":[]}',
            'sort_json' => null,
            'created_by' => 'admin',
            'created_at' => '2025-01-01',
        ];
    }

    private function stufeRow(int $id = 1): array {
        return [
            'id' => $id,
            'name' => 'Woelflinge',
            'sort_order' => 1,
            'age_range_min' => 7,
            'age_range_max' => 10,
            'color' => '#FF9900',
        ];
    }

    private function stufeHistoryRow(int $id = 1): array {
        return [
            'id' => $id,
            'member_id' => 1,
            'stufe_id' => 1,
            'changed_at' => '2025-01-01',
            'changed_by' => 'admin',
        ];
    }

    // ══════════════════════════════════════════════════════════════════
    // ── MemberMapper ─────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testMemberMapperInstantiation(): void {
        $mapper = new MemberMapper($this->db);
        $this->assertInstanceOf(MemberMapper::class, $mapper);
    }

    public function testMemberMapperFindById(): void {
        $this->configureResultSingleRow($this->memberRow(42));
        $mapper = new MemberMapper($this->db);
        $member = $mapper->findById(42);
        $this->assertSame(42, $member->getId());
        $this->assertSame('Max', $member->getVorname());
        $this->assertSame('Mustermann', $member->getNachname());
    }

    public function testMemberMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new MemberMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testMemberMapperFindAllNoParams(): void {
        $this->configureResultRows([$this->memberRow(1), $this->memberRow(2)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findAll();
        $this->assertCount(2, $members);
    }

    public function testMemberMapperFindAllWithPagination(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findAll(10, 0);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindAllIncludeDeleted(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findAll(null, null, true);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindByFamily(): void {
        $this->configureResultRows([$this->memberRow(1), $this->memberRow(2)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findByFamily(1);
        $this->assertCount(2, $members);
    }

    public function testMemberMapperFindByStatus(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findByStatus('aktiv');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindByStufe(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findByStufe(1);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperSearch(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->search('Max');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperSearchEmptyResult(): void {
        $this->configureResultRows([]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->search('Nonexistent');
        $this->assertCount(0, $members);
    }

    public function testMemberMapperFindByBirthdayMonth(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findByBirthdayMonth(6);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindByBirthdayMonthPadsSingleDigit(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findByBirthdayMonth(1);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindWithUnpaidFees(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findWithUnpaidFees(2026);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFullTextSearch(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->fullTextSearch('Max');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFullTextSearchWithCustomLimit(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->fullTextSearch('Max', 5);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindByNameAndBirthdate(): void {
        $this->configureResultSingleRow($this->memberRow(1));
        $mapper = new MemberMapper($this->db);
        $member = $mapper->findByNameAndBirthdate('Max', 'Mustermann', '1990-06-15');
        $this->assertNotNull($member);
        $this->assertSame(1, $member->getId());
    }

    public function testMemberMapperFindByNameAndBirthdateReturnsNullWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new MemberMapper($this->db);
        $member = $mapper->findByNameAndBirthdate('Nonexistent', 'Person', '2000-01-01');
        $this->assertNull($member);
    }

    public function testMemberMapperFindFilteredNoFilters(): void {
        $this->configureResultRows([$this->memberRow(1), $this->memberRow(2)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered();
        $this->assertCount(2, $members);
    }

    public function testMemberMapperFindFilteredByStatus(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(status: 'aktiv');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredByRolle(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(rolle: 'mitglied');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredByBirthdayThisMonth(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(birthdayThisMonth: true);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredByUnpaidFees(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(unpaidFees: true);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredCombinedStatusAndRolle(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(status: 'aktiv', rolle: 'mitglied');
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredCombinedAll(): void {
        $this->configureResultRows([]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(
            status: 'aktiv',
            rolle: 'erziehungsberechtigter',
            birthdayThisMonth: true,
            unpaidFees: true
        );
        $this->assertCount(0, $members);
    }

    public function testMemberMapperFindFilteredCombinedStatusAndBirthday(): void {
        $this->configureResultRows([$this->memberRow(1)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(status: 'inaktiv', birthdayThisMonth: true);
        $this->assertCount(1, $members);
    }

    public function testMemberMapperFindFilteredCombinedRolleAndUnpaid(): void {
        $this->configureResultRows([$this->memberRow(1), $this->memberRow(2)]);
        $mapper = new MemberMapper($this->db);
        $members = $mapper->findFiltered(rolle: 'mitglied', unpaidFees: true);
        $this->assertCount(2, $members);
    }

    public function testMemberMapperCountAll(): void {
        $this->configureResultCount(42);
        $mapper = new MemberMapper($this->db);
        $count = $mapper->countAll();
        $this->assertSame(42, $count);
    }

    public function testMemberMapperCountAllZero(): void {
        $this->configureResultCount(0);
        $mapper = new MemberMapper($this->db);
        $count = $mapper->countAll();
        $this->assertSame(0, $count);
    }

    public function testMemberMapperCountArchived(): void {
        $this->configureResultCount(7);
        $mapper = new MemberMapper($this->db);
        $count = $mapper->countArchived();
        $this->assertSame(7, $count);
    }

    public function testMemberMapperCountArchivedZero(): void {
        $this->configureResultCount(0);
        $mapper = new MemberMapper($this->db);
        $count = $mapper->countArchived();
        $this->assertSame(0, $count);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── FamilyMapper ─────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testFamilyMapperInstantiation(): void {
        $mapper = new FamilyMapper($this->db);
        $this->assertInstanceOf(FamilyMapper::class, $mapper);
    }

    public function testFamilyMapperFindById(): void {
        $this->configureResultSingleRow($this->familyRow(1));
        $mapper = new FamilyMapper($this->db);
        $family = $mapper->findById(1);
        $this->assertSame(1, $family->getId());
    }

    public function testFamilyMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new FamilyMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testFamilyMapperFindAll(): void {
        $this->configureResultRows([$this->familyRow(1), $this->familyRow(2)]);
        $mapper = new FamilyMapper($this->db);
        $families = $mapper->findAll();
        $this->assertCount(2, $families);
    }

    public function testFamilyMapperFindAllWithPagination(): void {
        $this->configureResultRows([$this->familyRow(1)]);
        $mapper = new FamilyMapper($this->db);
        $families = $mapper->findAll(10, 0);
        $this->assertCount(1, $families);
    }

    public function testFamilyMapperFindByName(): void {
        $this->configureResultRows([$this->familyRow(1)]);
        $mapper = new FamilyMapper($this->db);
        $families = $mapper->findByName('Mustermann');
        $this->assertCount(1, $families);
    }

    public function testFamilyMapperFindByNameEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new FamilyMapper($this->db);
        $families = $mapper->findByName('Nonexistent');
        $this->assertCount(0, $families);
    }

    public function testFamilyMapperCountAll(): void {
        $this->configureResultCount(5);
        $mapper = new FamilyMapper($this->db);
        $count = $mapper->countAll();
        $this->assertSame(5, $count);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── AddressMapper ────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testAddressMapperInstantiation(): void {
        $mapper = new AddressMapper($this->db);
        $this->assertInstanceOf(AddressMapper::class, $mapper);
    }

    public function testAddressMapperFindAll(): void {
        $this->configureResultRows([$this->addressRow(1), $this->addressRow(2)]);
        $mapper = new AddressMapper($this->db);
        $addresses = $mapper->findAll();
        $this->assertCount(2, $addresses);
    }

    public function testAddressMapperFindByMemberId(): void {
        $this->configureResultRows([$this->addressRow(1)]);
        $mapper = new AddressMapper($this->db);
        $addresses = $mapper->findByMemberId(1);
        $this->assertCount(1, $addresses);
    }

    public function testAddressMapperFindByMemberIdEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new AddressMapper($this->db);
        $addresses = $mapper->findByMemberId(999);
        $this->assertCount(0, $addresses);
    }

    public function testAddressMapperDeleteByMemberId(): void {
        $this->configureExecuteStatement(2);
        $mapper = new AddressMapper($this->db);
        $mapper->deleteByMemberId(1);
        // No exception means success
        $this->assertTrue(true);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── PhoneMapper ──────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testPhoneMapperInstantiation(): void {
        $mapper = new PhoneMapper($this->db);
        $this->assertInstanceOf(PhoneMapper::class, $mapper);
    }

    public function testPhoneMapperFindAll(): void {
        $this->configureResultRows([$this->phoneRow(1), $this->phoneRow(2)]);
        $mapper = new PhoneMapper($this->db);
        $phones = $mapper->findAll();
        $this->assertCount(2, $phones);
    }

    public function testPhoneMapperFindByMemberId(): void {
        $this->configureResultRows([$this->phoneRow(1)]);
        $mapper = new PhoneMapper($this->db);
        $phones = $mapper->findByMemberId(1);
        $this->assertCount(1, $phones);
    }

    public function testPhoneMapperFindByMemberIdEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new PhoneMapper($this->db);
        $phones = $mapper->findByMemberId(999);
        $this->assertCount(0, $phones);
    }

    public function testPhoneMapperDeleteByMemberId(): void {
        $this->configureExecuteStatement(1);
        $mapper = new PhoneMapper($this->db);
        $mapper->deleteByMemberId(1);
        $this->assertTrue(true);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── EmailMapper ──────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testEmailMapperInstantiation(): void {
        $mapper = new EmailMapper($this->db);
        $this->assertInstanceOf(EmailMapper::class, $mapper);
    }

    public function testEmailMapperFindAll(): void {
        $this->configureResultRows([$this->emailRow(1), $this->emailRow(2)]);
        $mapper = new EmailMapper($this->db);
        $emails = $mapper->findAll();
        $this->assertCount(2, $emails);
    }

    public function testEmailMapperFindByMemberId(): void {
        $this->configureResultRows([$this->emailRow(1)]);
        $mapper = new EmailMapper($this->db);
        $emails = $mapper->findByMemberId(1);
        $this->assertCount(1, $emails);
    }

    public function testEmailMapperFindByMemberIdEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new EmailMapper($this->db);
        $emails = $mapper->findByMemberId(999);
        $this->assertCount(0, $emails);
    }

    public function testEmailMapperDeleteByMemberId(): void {
        $this->configureExecuteStatement(1);
        $mapper = new EmailMapper($this->db);
        $mapper->deleteByMemberId(1);
        $this->assertTrue(true);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── FeeRecordMapper ──────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testFeeRecordMapperInstantiation(): void {
        $mapper = new FeeRecordMapper($this->db);
        $this->assertInstanceOf(FeeRecordMapper::class, $mapper);
    }

    public function testFeeRecordMapperFindById(): void {
        $this->configureResultSingleRow($this->feeRecordRow(1));
        $mapper = new FeeRecordMapper($this->db);
        $record = $mapper->findById(1);
        $this->assertSame(1, $record->getId());
    }

    public function testFeeRecordMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new FeeRecordMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testFeeRecordMapperFindAll(): void {
        $this->configureResultRows([$this->feeRecordRow(1), $this->feeRecordRow(2)]);
        $mapper = new FeeRecordMapper($this->db);
        $records = $mapper->findAll();
        $this->assertCount(2, $records);
    }

    public function testFeeRecordMapperFindByYear(): void {
        $this->configureResultRows([$this->feeRecordRow(1)]);
        $mapper = new FeeRecordMapper($this->db);
        $records = $mapper->findByYear(2026);
        $this->assertCount(1, $records);
    }

    public function testFeeRecordMapperFindByMemberId(): void {
        $this->configureResultRows([$this->feeRecordRow(1)]);
        $mapper = new FeeRecordMapper($this->db);
        $records = $mapper->findByMemberId(1);
        $this->assertCount(1, $records);
    }

    public function testFeeRecordMapperFindByMemberAndYear(): void {
        $this->configureResultSingleRow($this->feeRecordRow(1));
        $mapper = new FeeRecordMapper($this->db);
        $record = $mapper->findByMemberAndYear(1, 2026);
        $this->assertSame(1, $record->getId());
    }

    public function testFeeRecordMapperFindByMemberAndYearThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new FeeRecordMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findByMemberAndYear(999, 2026);
    }

    public function testFeeRecordMapperCountByYear(): void {
        $this->configureResultCount(15);
        $mapper = new FeeRecordMapper($this->db);
        $count = $mapper->countByYear(2026);
        $this->assertSame(15, $count);
    }

    public function testFeeRecordMapperCountByYearZero(): void {
        $this->configureResultCount(0);
        $mapper = new FeeRecordMapper($this->db);
        $count = $mapper->countByYear(2099);
        $this->assertSame(0, $count);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── FeeRuleMapper ────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testFeeRuleMapperInstantiation(): void {
        $mapper = new FeeRuleMapper($this->db);
        $this->assertInstanceOf(FeeRuleMapper::class, $mapper);
    }

    public function testFeeRuleMapperFindById(): void {
        $this->configureResultSingleRow($this->feeRuleRow(1));
        $mapper = new FeeRuleMapper($this->db);
        $rule = $mapper->findById(1);
        $this->assertSame(1, $rule->getId());
    }

    public function testFeeRuleMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new FeeRuleMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testFeeRuleMapperFindByYear(): void {
        $this->configureResultSingleRow($this->feeRuleRow(1));
        $mapper = new FeeRuleMapper($this->db);
        $rule = $mapper->findByYear(2026);
        $this->assertSame(1, $rule->getId());
    }

    public function testFeeRuleMapperFindByYearThrowsWhenNoneApplies(): void {
        $this->configureResultEmpty();
        $mapper = new FeeRuleMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findByYear(1900);
    }

    public function testFeeRuleMapperFindAll(): void {
        $this->configureResultRows([$this->feeRuleRow(1), $this->feeRuleRow(2)]);
        $mapper = new FeeRuleMapper($this->db);
        $rules = $mapper->findAll();
        $this->assertCount(2, $rules);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── InjuryMapper ─────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testInjuryMapperInstantiation(): void {
        $mapper = new InjuryMapper($this->db);
        $this->assertInstanceOf(InjuryMapper::class, $mapper);
    }

    public function testInjuryMapperFindById(): void {
        $this->configureResultSingleRow($this->injuryRow(1));
        $mapper = new InjuryMapper($this->db);
        $injury = $mapper->findById(1);
        $this->assertSame(1, $injury->getId());
    }

    public function testInjuryMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new InjuryMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testInjuryMapperFindAllNoParams(): void {
        $this->configureResultRows([$this->injuryRow(1), $this->injuryRow(2)]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findAll();
        $this->assertCount(2, $injuries);
    }

    public function testInjuryMapperFindAllWithPagination(): void {
        $this->configureResultRows([$this->injuryRow(1)]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findAll(10, 0);
        $this->assertCount(1, $injuries);
    }

    public function testInjuryMapperFindByMemberId(): void {
        $this->configureResultRows([$this->injuryRow(1)]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findByMemberId(1);
        $this->assertCount(1, $injuries);
    }

    public function testInjuryMapperFindByLagerId(): void {
        $this->configureResultRows([$this->injuryRow(1)]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findByLagerId(1);
        $this->assertCount(1, $injuries);
    }

    public function testInjuryMapperFindByDateRange(): void {
        $this->configureResultRows([$this->injuryRow(1)]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findByDateRange('2026-01-01', '2026-12-31');
        $this->assertCount(1, $injuries);
    }

    public function testInjuryMapperFindByDateRangeEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new InjuryMapper($this->db);
        $injuries = $mapper->findByDateRange('2000-01-01', '2000-12-31');
        $this->assertCount(0, $injuries);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── LagerMapper ──────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testLagerMapperInstantiation(): void {
        $mapper = new LagerMapper($this->db);
        $this->assertInstanceOf(LagerMapper::class, $mapper);
    }

    public function testLagerMapperFindById(): void {
        $this->configureResultSingleRow($this->lagerRow(1));
        $mapper = new LagerMapper($this->db);
        $lager = $mapper->findById(1);
        $this->assertSame(1, $lager->getId());
    }

    public function testLagerMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new LagerMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testLagerMapperFindAllNoParams(): void {
        $this->configureResultRows([$this->lagerRow(1), $this->lagerRow(2)]);
        $mapper = new LagerMapper($this->db);
        $camps = $mapper->findAll();
        $this->assertCount(2, $camps);
    }

    public function testLagerMapperFindAllWithPagination(): void {
        $this->configureResultRows([$this->lagerRow(1)]);
        $mapper = new LagerMapper($this->db);
        $camps = $mapper->findAll(10, 0);
        $this->assertCount(1, $camps);
    }

    public function testLagerMapperFindByYear(): void {
        $this->configureResultRows([$this->lagerRow(1)]);
        $mapper = new LagerMapper($this->db);
        $camps = $mapper->findByYear(2026);
        $this->assertCount(1, $camps);
    }

    public function testLagerMapperFindByYearEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new LagerMapper($this->db);
        $camps = $mapper->findByYear(1999);
        $this->assertCount(0, $camps);
    }

    public function testLagerMapperFindByStufe(): void {
        $this->configureResultRows([$this->lagerRow(1)]);
        $mapper = new LagerMapper($this->db);
        $camps = $mapper->findByStufe(1);
        $this->assertCount(1, $camps);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── LagerTeilnehmerMapper ────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testLagerTeilnehmerMapperInstantiation(): void {
        $mapper = new LagerTeilnehmerMapper($this->db);
        $this->assertInstanceOf(LagerTeilnehmerMapper::class, $mapper);
    }

    public function testLagerTeilnehmerMapperFindAll(): void {
        $this->configureResultRows([$this->lagerTeilnehmerRow(1), $this->lagerTeilnehmerRow(2)]);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $participants = $mapper->findAll();
        $this->assertCount(2, $participants);
    }

    public function testLagerTeilnehmerMapperFindByLagerId(): void {
        $this->configureResultRows([$this->lagerTeilnehmerRow(1)]);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $participants = $mapper->findByLagerId(1);
        $this->assertCount(1, $participants);
    }

    public function testLagerTeilnehmerMapperFindByMemberId(): void {
        $this->configureResultRows([$this->lagerTeilnehmerRow(1)]);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $participants = $mapper->findByMemberId(1);
        $this->assertCount(1, $participants);
    }

    public function testLagerTeilnehmerMapperAddParticipant(): void {
        $this->configureExecuteStatement(1);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $mapper->addParticipant(1, 1);
        $this->assertTrue(true);
    }

    public function testLagerTeilnehmerMapperAddParticipantWithCustomRole(): void {
        $this->configureExecuteStatement(1);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $mapper->addParticipant(1, 1, 'Leiter');
        $this->assertTrue(true);
    }

    public function testLagerTeilnehmerMapperRemoveParticipant(): void {
        $this->configureExecuteStatement(1);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $mapper->removeParticipant(1, 1);
        $this->assertTrue(true);
    }

    public function testLagerTeilnehmerMapperDeleteByLagerId(): void {
        $this->configureExecuteStatement(5);
        $mapper = new LagerTeilnehmerMapper($this->db);
        $mapper->deleteByLagerId(1);
        $this->assertTrue(true);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── AuditLogMapper ───────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testAuditLogMapperInstantiation(): void {
        $mapper = new AuditLogMapper($this->db);
        $this->assertInstanceOf(AuditLogMapper::class, $mapper);
    }

    public function testAuditLogMapperFindFilteredNoParams(): void {
        $this->configureResultRows([$this->auditLogRow(1), $this->auditLogRow(2)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered();
        $this->assertCount(2, $logs);
    }

    public function testAuditLogMapperFindFilteredByEntitaet(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered(entitaet: 'member');
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperFindFilteredByEntitaetId(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered(entitaetId: 1);
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperFindFilteredByNcUserId(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered(ncUserId: 'admin');
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperFindFilteredByDateRange(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered(fromDate: '2025-01-01', toDate: '2025-12-31');
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperFindFilteredWithPagination(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered(limit: 10, offset: 0);
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperFindFilteredAllParams(): void {
        $this->configureResultRows([$this->auditLogRow(1)]);
        $mapper = new AuditLogMapper($this->db);
        $logs = $mapper->findFiltered('member', 1, 'admin', '2025-01-01', '2025-12-31', 10, 0);
        $this->assertCount(1, $logs);
    }

    public function testAuditLogMapperCountFilteredNoParams(): void {
        $this->configureResultCount(100);
        $mapper = new AuditLogMapper($this->db);
        $count = $mapper->countFiltered();
        $this->assertSame(100, $count);
    }

    public function testAuditLogMapperCountFilteredByEntitaet(): void {
        $this->configureResultCount(10);
        $mapper = new AuditLogMapper($this->db);
        $count = $mapper->countFiltered(entitaet: 'member');
        $this->assertSame(10, $count);
    }

    public function testAuditLogMapperCountFilteredByEntitaetId(): void {
        $this->configureResultCount(3);
        $mapper = new AuditLogMapper($this->db);
        $count = $mapper->countFiltered(entitaetId: 1);
        $this->assertSame(3, $count);
    }

    public function testAuditLogMapperCountFilteredByNcUserId(): void {
        $this->configureResultCount(5);
        $mapper = new AuditLogMapper($this->db);
        $count = $mapper->countFiltered(ncUserId: 'admin');
        $this->assertSame(5, $count);
    }

    public function testAuditLogMapperCountFilteredAllParams(): void {
        $this->configureResultCount(1);
        $mapper = new AuditLogMapper($this->db);
        $count = $mapper->countFiltered('member', 1, 'admin');
        $this->assertSame(1, $count);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── PermissionMapper ─────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testPermissionMapperInstantiation(): void {
        $mapper = new PermissionMapper($this->db);
        $this->assertInstanceOf(PermissionMapper::class, $mapper);
    }

    public function testPermissionMapperFindByUserId(): void {
        $this->configureResultSingleRow($this->permissionRow(1));
        $mapper = new PermissionMapper($this->db);
        $permission = $mapper->findByUserId('admin');
        $this->assertSame(1, $permission->getId());
    }

    public function testPermissionMapperFindByUserIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new PermissionMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findByUserId('nonexistent');
    }

    public function testPermissionMapperFindAll(): void {
        $this->configureResultRows([$this->permissionRow(1), $this->permissionRow(2)]);
        $mapper = new PermissionMapper($this->db);
        $permissions = $mapper->findAll();
        $this->assertCount(2, $permissions);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── StufeMapper ──────────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testStufeMapperInstantiation(): void {
        $mapper = new StufeMapper($this->db);
        $this->assertInstanceOf(StufeMapper::class, $mapper);
    }

    public function testStufeMapperFindById(): void {
        $this->configureResultSingleRow($this->stufeRow(1));
        $mapper = new StufeMapper($this->db);
        $stufe = $mapper->findById(1);
        $this->assertSame(1, $stufe->getId());
    }

    public function testStufeMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new StufeMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testStufeMapperFindAll(): void {
        $this->configureResultRows([$this->stufeRow(1), $this->stufeRow(2)]);
        $mapper = new StufeMapper($this->db);
        $stufen = $mapper->findAll();
        $this->assertCount(2, $stufen);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── SavedQueryMapper ─────────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testSavedQueryMapperInstantiation(): void {
        $mapper = new SavedQueryMapper($this->db);
        $this->assertInstanceOf(SavedQueryMapper::class, $mapper);
    }

    public function testSavedQueryMapperFindById(): void {
        $this->configureResultSingleRow($this->savedQueryRow(1));
        $mapper = new SavedQueryMapper($this->db);
        $query = $mapper->findById(1);
        $this->assertSame(1, $query->getId());
    }

    public function testSavedQueryMapperFindByIdThrowsWhenNotFound(): void {
        $this->configureResultEmpty();
        $mapper = new SavedQueryMapper($this->db);
        $this->expectException(DoesNotExistException::class);
        $mapper->findById(999);
    }

    public function testSavedQueryMapperFindAll(): void {
        $this->configureResultRows([$this->savedQueryRow(1), $this->savedQueryRow(2)]);
        $mapper = new SavedQueryMapper($this->db);
        $queries = $mapper->findAll();
        $this->assertCount(2, $queries);
    }

    // ══════════════════════════════════════════════════════════════════
    // ── StufeHistoryMapper ───────────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════

    public function testStufeHistoryMapperInstantiation(): void {
        $mapper = new StufeHistoryMapper($this->db);
        $this->assertInstanceOf(StufeHistoryMapper::class, $mapper);
    }

    public function testStufeHistoryMapperFindByMemberId(): void {
        $this->configureResultRows([$this->stufeHistoryRow(1)]);
        $mapper = new StufeHistoryMapper($this->db);
        $history = $mapper->findByMemberId(1);
        $this->assertCount(1, $history);
    }

    public function testStufeHistoryMapperFindByMemberIdEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new StufeHistoryMapper($this->db);
        $history = $mapper->findByMemberId(999);
        $this->assertCount(0, $history);
    }

    public function testStufeHistoryMapperFindAll(): void {
        $this->configureResultRows([$this->stufeHistoryRow(1), $this->stufeHistoryRow(2)]);
        $mapper = new StufeHistoryMapper($this->db);
        $history = $mapper->findAll();
        $this->assertCount(2, $history);
    }

    public function testStufeHistoryMapperFindByStufeId(): void {
        $this->configureResultRows([$this->stufeHistoryRow(1)]);
        $mapper = new StufeHistoryMapper($this->db);
        $history = $mapper->findByStufeId(1);
        $this->assertCount(1, $history);
    }

    public function testStufeHistoryMapperFindByStufeIdEmpty(): void {
        $this->configureResultRows([]);
        $mapper = new StufeHistoryMapper($this->db);
        $history = $mapper->findByStufeId(999);
        $this->assertCount(0, $history);
    }
}
