<?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\Stufe;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\Family;
use OCA\Mitgliederverwaltung\Db\Lager;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\EncryptionService;
use OCA\Mitgliederverwaltung\Service\EntityExportService;
use OCA\Mitgliederverwaltung\Service\EntityImportService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class EntityImportServiceTest extends TestCase {

    private EntityImportService $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 EntityExportService&MockObject $exportService;
    private AuditService&MockObject $auditService;
    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->exportService = $this->createMock(EntityExportService::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->encryptionService = $this->createMock(EncryptionService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new EntityImportService(
            $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->exportService,
            $this->auditService,
            $this->encryptionService,
            $this->logger
        );
    }

    // ── detectEntityType() ─────────────────────────────────────────

    public function testDetectEntityTypeMitglieder(): void {
        $this->exportService->method('getColumnHeaders')
            ->willReturnCallback(function (string $type) {
                // Use real column headers
                $svc = $this->createRealExportService();
                return $svc->getColumnHeaders($type);
            });

        $headers = ['ID', 'Vorname', 'Nachname', 'Geburtsdatum', 'Geschlecht', 'Rolle', 'Stufe-ID', 'Stufenname', 'Eintritt', 'Austritt', 'Status', 'Allergien', 'Notizen', 'Zusaetzliche Notizen', 'KV-Typ', 'KV-Name', 'Familien-ID', 'Familienname', 'Eingefrorener Beitragssatz', 'Einwilligungsdatum'];

        $result = $this->service->detectEntityType($headers);

        $this->assertEquals('mitglieder', $result['type']);
        $this->assertGreaterThanOrEqual(0.4, $result['confidence']);
    }

    public function testDetectEntityTypeFamilien(): void {
        $this->exportService->method('getColumnHeaders')
            ->willReturnCallback(function (string $type) {
                $svc = $this->createRealExportService();
                return $svc->getColumnHeaders($type);
            });

        $headers = ['ID', 'Name', 'Kontoinhaber', 'IBAN', 'BIC', 'Kreditinstitut'];

        $result = $this->service->detectEntityType($headers);

        $this->assertEquals('familien', $result['type']);
    }

    public function testDetectEntityTypeReturnsNullForUnknown(): void {
        $this->exportService->method('getColumnHeaders')
            ->willReturnCallback(function (string $type) {
                $svc = $this->createRealExportService();
                return $svc->getColumnHeaders($type);
            });

        $headers = ['Foo', 'Bar', 'Baz'];

        $result = $this->service->detectEntityType($headers);

        $this->assertNull($result['type']);
        $this->assertLessThan(0.4, $result['confidence']);
    }

    public function testDetectEntityTypeReturnsAlternatives(): void {
        $this->exportService->method('getColumnHeaders')
            ->willReturnCallback(function (string $type) {
                $svc = $this->createRealExportService();
                return $svc->getColumnHeaders($type);
            });

        $headers = ['ID', 'Vorname', 'Nachname', 'Geburtsdatum'];

        $result = $this->service->detectEntityType($headers);

        $this->assertIsArray($result['alternatives']);
    }

    // ── getImportSchema() ──────────────────────────────────────────

    public function testGetImportSchemaStufen(): void {
        $this->exportService->method('getColumnHeaders')
            ->with('stufen')
            ->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);

        $schema = $this->service->getImportSchema('stufen');

        $this->assertArrayHasKey('headers', $schema);
        $this->assertArrayHasKey('targetFields', $schema);
        $this->assertNotEmpty($schema['targetFields']);

        $keys = array_column($schema['targetFields'], 'key');
        $this->assertContains('name', $keys);
    }

    public function testGetImportSchemaMitglieder(): void {
        $this->exportService->method('getColumnHeaders')
            ->with('mitglieder')
            ->willReturn(['ID', 'Vorname', 'Nachname', 'Geburtsdatum']);

        $schema = $this->service->getImportSchema('mitglieder');

        $targetFields = $schema['targetFields'];
        $requiredFields = array_filter($targetFields, fn($f) => $f['required'] ?? false);
        $this->assertGreaterThan(0, count($requiredFields));
    }

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

    // ── autoMapHeaders() ───────────────────────────────────────────

    public function testAutoMapHeadersStufen(): void {
        $this->exportService->method('getColumnHeaders')
            ->with('stufen')
            ->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);

        $csvHeaders = ['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe'];
        $mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);

        $this->assertContains('name', $mapping);
        $this->assertContains('sort_order', $mapping);
    }

    public function testAutoMapHeadersCaseInsensitive(): void {
        $this->exportService->method('getColumnHeaders')
            ->with('stufen')
            ->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);

        $csvHeaders = ['id', 'name', 'sortierung', 'mindestalter', 'hoechstalter', 'farbe'];
        $mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);

        $this->assertContains('name', $mapping);
    }

    public function testAutoMapHeadersUnmappedColumnsEmptyString(): void {
        $this->exportService->method('getColumnHeaders')
            ->with('stufen')
            ->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);

        $csvHeaders = ['ID', 'Name', 'UnknownColumn'];
        $mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);

        $this->assertEquals('', $mapping[2]);
    }

    // ── parseFile() ────────────────────────────────────────────────

    public function testParseFileSemicolon(): void {
        $csv = "Name;Sortierung\nWoelflinge;1\nJungpfadfinder;2";

        $result = $this->service->parseFile($csv, ';', 'UTF-8');

        $this->assertEquals(['Name', 'Sortierung'], $result['columns']);
        $this->assertCount(2, $result['rows']);
        $this->assertEquals(2, $result['totalRows']);
    }

    public function testParseFileRemovesBom(): void {
        $csv = "\xEF\xBB\xBFName;Sortierung\nWoelflinge;1";

        $result = $this->service->parseFile($csv, ';', 'UTF-8');

        $this->assertEquals('Name', $result['columns'][0]);
    }

    public function testParseFileThrowsOnEmpty(): void {
        $this->expectException(\Throwable::class);
        $this->service->parseFile('', ';', 'UTF-8');
    }

    public function testParseFileThrowsOnHeaderOnly(): void {
        $this->expectException(ValidationException::class);
        $this->service->parseFile("Name;Sortierung", ';', 'UTF-8');
    }

    public function testParseFileLimitsPreviewTo50Rows(): void {
        $lines = ["Name;Sortierung"];
        for ($i = 1; $i <= 100; $i++) {
            $lines[] = "Stufe$i;$i";
        }
        $csv = implode("\n", $lines);

        $result = $this->service->parseFile($csv, ';', 'UTF-8');

        $this->assertCount(50, $result['rows']);
        $this->assertEquals(100, $result['totalRows']);
    }

    // ── preview() ──────────────────────────────────────────────────

    public function testPreviewValidRows(): void {
        $this->exportService->method('getColumnHeaders')
            ->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
        $this->stufeMapper->method('findAll')->willReturn([]);

        $csv = "Name;Sortierung\nWoelflinge;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertArrayHasKey('valid', $result);
        $this->assertArrayHasKey('errors', $result);
        $this->assertArrayHasKey('duplicates', $result);
        $this->assertArrayHasKey('summary', $result);
        $this->assertEquals(1, $result['summary']['total']);
        $this->assertEquals(1, $result['summary']['toCreate']);
    }

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

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

        $csv = "Name;Sortierung\nWoelflinge;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['summary']['duplicates']);
    }

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

        // Name is required for stufen
        $csv = "Name;Sortierung\n;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertGreaterThan(0, $result['summary']['errors']);
    }

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

        $csv = "Name;Sortierung\n;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];
        // Correct the missing name in row 2
        $corrections = [2 => ['name' => 'CorrectedName']];

        $result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8', false, $corrections);

        $this->assertEquals(1, $result['summary']['toCreate']);
        $this->assertEquals(0, $result['summary']['errors']);
    }

    // ── execute() ──────────────────────────────────────────────────

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

        $insertedStufe = new Stufe();
        $insertedStufe->setName('Woelflinge');
        $ref = new \ReflectionProperty($insertedStufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedStufe, 99);

        $this->stufeMapper->method('insert')->willReturn($insertedStufe);

        $csv = "Name;Sortierung\nWoelflinge;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
        $this->assertEquals(0, $result['skipped']);
        $this->assertEquals(0, $result['errors']);
    }

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

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

        $csv = "Name;Sortierung\nWoelflinge;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(0, $result['created']);
        $this->assertEquals(1, $result['skipped']);
    }

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

        // Missing required 'name' field
        $csv = "Name;Sortierung\n;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(0, $result['created']);
        $this->assertEquals(1, $result['errors']);
    }

    // ── getFieldDefinitions() ──────────────────────────────────────

    public function testGetFieldDefinitionsMitglieder(): void {
        $defs = $this->service->getFieldDefinitions('mitglieder');

        $this->assertNotEmpty($defs);
        $keys = array_column($defs, 'key');
        $this->assertContains('vorname', $keys);
        $this->assertContains('nachname', $keys);
        $this->assertContains('geburtsdatum', $keys);

        $requiredKeys = array_column(
            array_filter($defs, fn($d) => ($d['required'] ?? false)),
            'key'
        );
        $this->assertContains('vorname', $requiredKeys);
        $this->assertContains('nachname', $requiredKeys);
    }

    public function testGetFieldDefinitionsAllTypes(): void {
        $types = ['mitglieder', 'familien', 'adressen', 'telefonnummern', 'emails',
            'stufen', 'stufenverlauf', 'beitragsregeln', 'beitraege', 'lager',
            'lagerteilnehmer', 'verletzungen', 'gespeicherte_abfragen'];

        foreach ($types as $type) {
            $defs = $this->service->getFieldDefinitions($type);
            $this->assertNotEmpty($defs, "Field definitions for $type should not be empty");
        }
    }

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

    // ── Date validation ────────────────────────────────────────────

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;2010-01-01;2020-01-01";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');

        // Should not have any errors for valid ISO dates
        $this->assertEquals(0, $result['summary']['errors'], 'Valid ISO dates should not cause errors');
    }

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;not-a-date;2020-01-01";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');

        $this->assertGreaterThan(0, $result['summary']['errors']);
    }

    // ── preview() with migration mode ────────────────────────────

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

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertEquals(1, $result['summary']['toCreate']);
        $this->assertEmpty($result['fkWarnings']);
    }

    public function testPreviewMigrationModeDetectsUnknownStufe(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->lagerMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Unknown";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertNotEmpty($result['fkWarnings']);
    }

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

        $member2 = new Member();
        $member2->setVorname('Max');
        $member2->setNachname('Mueller');
        $member2->setGeburtsdatum('2012-05-01');
        $member2->setEintritt('2021-01-01');
        $member2->setRolle('mitglied');
        $member2->setStatus('aktiv');
        $ref2 = new \ReflectionProperty($member2, 'id');
        $ref2->setAccessible(true);
        $ref2->setValue($member2, 2);

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

        $csv = "Mitglied-ID;Mitgliedername;Strasse;PLZ;Ort\n;Max Mueller;Test 1;12345;Berlin";
        $mapping = [0 => 'member_id', 1 => 'mitgliedername', 2 => 'strasse', 3 => 'plz', 4 => 'ort'];

        $result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertNotEmpty($result['ambiguous']);
    }

    // ── preview() FK resolution (non-migration) ───────────────────

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

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', false);

        $this->assertEmpty($result['fkWarnings']);
    }

    public function testPreviewWarnsOnUnknownFamily(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Familienname\nMax;Mueller;2010-01-01;2020-01-01;UnknownFamily";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'familienname'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', false);

        $this->assertNotEmpty($result['fkWarnings']);
        $this->assertStringContainsString('UnknownFamily', $result['fkWarnings'][0]['message']);
    }

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

        $csv = "Mitgliedername;Strasse;PLZ;Ort\nUnknown Person;Test 1;12345;Berlin";
        $mapping = [0 => 'mitgliedername', 1 => 'strasse', 2 => 'plz', 3 => 'ort'];

        $result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8', false);

        $this->assertNotEmpty($result['fkWarnings']);
    }

    public function testPreviewWarnsOnUnknownLagerFK(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->lagerMapper->method('findAll')->willReturn([]);

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

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

        $csv = "Mitgliedername;Lagername;Rolle\nMax Mueller;UnknownLager;Teilnehmer";
        $mapping = [0 => 'mitgliedername', 1 => 'lagername', 2 => 'rolle'];

        $result = $this->service->preview('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8', false);

        $this->assertNotEmpty($result['fkWarnings']);
    }

    // ── preview() validation for FK-required entity types ─────────

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

        $csv = "Strasse;PLZ;Ort\nTest 1;12345;Berlin";
        $mapping = [0 => 'strasse', 1 => 'plz', 2 => 'ort'];

        $result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8');

        $this->assertGreaterThan(0, $result['summary']['errors']);
    }

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

        $csv = "Nummer\n+4917612345678";
        $mapping = [0 => 'nummer'];

        $result = $this->service->preview('telefonnummern', $csv, $mapping, ';', 'UTF-8');

        $this->assertGreaterThan(0, $result['summary']['errors']);
    }

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

        $csv = "E-Mail-Adresse\ntest@test.de";
        $mapping = [0 => 'email'];

        $result = $this->service->preview('emails', $csv, $mapping, ';', 'UTF-8');

        $this->assertGreaterThan(0, $result['summary']['errors']);
    }

    // ── preview() duplicate detection for various types ────────────

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

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;2010-01-01;2020-01-01";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['summary']['duplicates']);
        $this->assertNotEmpty($result['duplicates'][0]['_existingId']);
    }

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

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

        $csv = "Name\nMueller";
        $mapping = [0 => 'name'];

        $result = $this->service->preview('familien', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['summary']['duplicates']);
    }

    public function testPreviewDetectsLagerDuplicate(): void {
        $lager = new Lager();
        $lager->setName('Sommerlager');
        $ref = new \ReflectionProperty($lager, 'id');
        $ref->setAccessible(true);
        $ref->setValue($lager, 1);

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

        $csv = "Name;Startdatum;Enddatum\nSommerlager;2024-07-01;2024-07-14";
        $mapping = [0 => 'name', 1 => 'startdatum', 2 => 'enddatum'];

        $result = $this->service->preview('lager', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['summary']['duplicates']);
    }

    public function testPreviewDetectsSavedQueryDuplicate(): void {
        $query = new \OCA\Mitgliederverwaltung\Db\SavedQuery();
        $query->setName('TestQuery');
        $query->setQueryJson('{}');
        $query->setCreatedBy('admin');
        $query->setCreatedAt('2020-01-01');
        $ref = new \ReflectionProperty($query, 'id');
        $ref->setAccessible(true);
        $ref->setValue($query, 1);

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

        $csv = "Name;Abfrage\nTestQuery;{}";
        $mapping = [0 => 'name', 1 => 'query_json'];

        $result = $this->service->preview('gespeicherte_abfragen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['summary']['duplicates']);
    }

    // ── execute() with different entity types ─────────────────────

    public function testExecuteCreatesMember(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $insertedMember = new Member();
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mueller');
        $ref = new \ReflectionProperty($insertedMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedMember, 1);

        $this->memberMapper->method('insert')->willReturn($insertedMember);
        $this->encryptionService->method('encrypt')->willReturnArgument(0);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Geschlecht;Rolle;Status;Notizen;KV-Typ;KV-Name;Allergien\nMax;Mueller;2010-01-01;2020-01-01;m;mitglied;aktiv;TestNote;GKV;AOK;Erdnuesse";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'geschlecht', 5 => 'rolle', 6 => 'status', 7 => 'notizen', 8 => 'kv_typ', 9 => 'kv_name', 10 => 'allergien'];

        $result = $this->service->execute('mitglieder', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedFamily = new Family();
        $insertedFamily->setName('Mueller');
        $ref = new \ReflectionProperty($insertedFamily, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedFamily, 1);

        $this->familyMapper->method('insert')->willReturn($insertedFamily);
        $this->encryptionService->method('encrypt')->willReturnArgument(0);

        $csv = "Name;Kontoinhaber;IBAN;BIC;Kreditinstitut\nMueller;Hans Mueller;DE89370400440532013000;DEUTDEDB;Deutsche Bank";
        $mapping = [0 => 'name', 1 => 'kontoinhaber', 2 => 'iban', 3 => 'bic', 4 => 'kreditinstitut'];

        $result = $this->service->execute('familien', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedAddress = new \OCA\Mitgliederverwaltung\Db\Address();
        $ref = new \ReflectionProperty($insertedAddress, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedAddress, 1);

        $this->addressMapper->method('insert')->willReturn($insertedAddress);

        $csv = "Mitglied-ID;Strasse;PLZ;Ort;Land;Primaer;Label\n1;Test 1;12345;Berlin;Deutschland;Ja;Home";
        $mapping = [0 => 'member_id', 1 => 'strasse', 2 => 'plz', 3 => 'ort', 4 => 'land', 5 => 'primaer', 6 => 'label'];

        $result = $this->service->execute('adressen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedPhone = new \OCA\Mitgliederverwaltung\Db\Phone();
        $ref = new \ReflectionProperty($insertedPhone, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedPhone, 1);

        $this->phoneMapper->method('insert')->willReturn($insertedPhone);

        $csv = "Mitglied-ID;Nummer;Label\n1;+4917612345678;Mobil";
        $mapping = [0 => 'member_id', 1 => 'nummer', 2 => 'label'];

        $result = $this->service->execute('telefonnummern', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedEmail = new \OCA\Mitgliederverwaltung\Db\Email();
        $ref = new \ReflectionProperty($insertedEmail, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedEmail, 1);

        $this->emailMapper->method('insert')->willReturn($insertedEmail);

        $csv = "Mitglied-ID;E-Mail-Adresse;Label\n1;test@test.de;Privat";
        $mapping = [0 => 'member_id', 1 => 'email', 2 => 'label'];

        $result = $this->service->execute('emails', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedHistory = new \OCA\Mitgliederverwaltung\Db\StufeHistory();
        $ref = new \ReflectionProperty($insertedHistory, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedHistory, 1);

        $this->stufeHistoryMapper->method('insert')->willReturn($insertedHistory);

        $csv = "Mitglied-ID;Stufe-ID;Datum;Geaendert von\n1;2;2024-01-01;admin";
        $mapping = [0 => 'member_id', 1 => 'stufe_id', 2 => 'datum', 3 => 'changed_by'];

        $result = $this->service->execute('stufenverlauf', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedRule = new \OCA\Mitgliederverwaltung\Db\FeeRule();
        $ref = new \ReflectionProperty($insertedRule, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedRule, 1);

        $this->feeRuleMapper->method('insert')->willReturn($insertedRule);

        $csv = "Gueltig ab Jahr;Grundbeitrag;Familienregeln;Inaktiven-Regel\n2026;120.00;{};freeze";
        $mapping = [0 => 'year_from', 1 => 'base_rate', 2 => 'family_rules_json', 3 => 'inactive_rule'];

        $result = $this->service->execute('beitragsregeln', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedRecord = new \OCA\Mitgliederverwaltung\Db\FeeRecord();
        $ref = new \ReflectionProperty($insertedRecord, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedRecord, 1);

        $this->feeRecordMapper->method('insert')->willReturn($insertedRecord);

        $csv = "Mitglied-ID;Jahr;Betrag;Bezahlt;Zahlungsdatum;Manuell angepasst;Notizen\n1;2026;120.00;Ja;2026-03-01;Nein;Testnotiz";
        $mapping = [0 => 'member_id', 1 => 'year', 2 => 'amount', 3 => 'paid', 4 => 'payment_date', 5 => 'manuell_angepasst', 6 => 'notes'];

        $result = $this->service->execute('beitraege', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $insertedLager = new Lager();
        $insertedLager->setName('Sommerlager');
        $ref = new \ReflectionProperty($insertedLager, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedLager, 1);

        $this->lagerMapper->method('insert')->willReturn($insertedLager);

        $csv = "Name;Startdatum;Enddatum;Ort;Beschreibung\nSommerlager;2024-07-01;2024-07-14;Wald;Tolles Lager";
        $mapping = [0 => 'name', 1 => 'startdatum', 2 => 'enddatum', 3 => 'ort', 4 => 'beschreibung'];

        $result = $this->service->execute('lager', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $inserted = new \OCA\Mitgliederverwaltung\Db\LagerTeilnehmer();
        $ref = new \ReflectionProperty($inserted, 'id');
        $ref->setAccessible(true);
        $ref->setValue($inserted, 1);

        $this->lagerTeilnehmerMapper->method('insert')->willReturn($inserted);

        $csv = "Lager-ID;Mitglied-ID;Rolle\n1;2;Teilnehmer";
        $mapping = [0 => 'lager_id', 1 => 'member_id', 2 => 'rolle'];

        $result = $this->service->execute('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $inserted = new \OCA\Mitgliederverwaltung\Db\Injury();
        $ref = new \ReflectionProperty($inserted, 'id');
        $ref->setAccessible(true);
        $ref->setValue($inserted, 1);

        $this->injuryMapper->method('insert')->willReturn($inserted);

        $csv = "Datum;Mitglied-ID;Lager-ID;Aktivitaet;Beschreibung;Erstellt von\n2024-07-05;1;2;Wandern;Knie verletzt;admin";
        $mapping = [0 => 'datum', 1 => 'member_id', 2 => 'lager_id', 3 => 'aktivitaet', 4 => 'beschreibung', 5 => 'created_by'];

        $result = $this->service->execute('verletzungen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

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

        $inserted = new \OCA\Mitgliederverwaltung\Db\SavedQuery();
        $inserted->setName('TestQuery');
        $inserted->setQueryJson('{}');
        $inserted->setCreatedBy('import');
        $inserted->setCreatedAt('2024-01-01');
        $ref = new \ReflectionProperty($inserted, 'id');
        $ref->setAccessible(true);
        $ref->setValue($inserted, 1);

        $this->savedQueryMapper->method('insert')->willReturn($inserted);

        $csv = "Name;Abfrage;Sortierung;Erstellt von\nMyQuery;{};{};admin";
        $mapping = [0 => 'name', 1 => 'query_json', 2 => 'sort_json', 3 => 'created_by'];

        $result = $this->service->execute('gespeicherte_abfragen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

    // ── execute() error handling ───────────────────────────────────

    public function testExecuteHandlesInsertException(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->stufeMapper->method('insert')
            ->willThrowException(new \RuntimeException('DB error'));

        $csv = "Name;Sortierung\nWoelflinge;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];

        $result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(0, $result['created']);
        $this->assertEquals(1, $result['errors']);
    }

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

        $insertedStufe = new Stufe();
        $insertedStufe->setName('CorrectedName');
        $ref = new \ReflectionProperty($insertedStufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedStufe, 99);

        $this->stufeMapper->method('insert')->willReturn($insertedStufe);

        $csv = "Name;Sortierung\n;1";
        $mapping = [0 => 'name', 1 => 'sort_order'];
        $corrections = [2 => ['name' => 'CorrectedName']];

        $result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8', false, $corrections);

        $this->assertEquals(1, $result['created']);
    }

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

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

        $insertedMember = new Member();
        $insertedMember->setVorname('Max');
        $ref2 = new \ReflectionProperty($insertedMember, 'id');
        $ref2->setAccessible(true);
        $ref2->setValue($insertedMember, 1);

        $this->memberMapper->method('insert')->willReturn($insertedMember);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];

        $result = $this->service->execute('mitglieder', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertEquals(1, $result['created']);
    }

    public function testExecuteThrowsOnUnknownType(): void {
        $this->expectException(\InvalidArgumentException::class);

        $csv = "Name\nTest";
        $mapping = [0 => 'name'];

        $this->service->execute('unknown', $csv, $mapping, ';', 'UTF-8');
    }

    // ── mergeConflicts() ──────────────────────────────────────────

    /**
     * Tests that mergeConflicts calls memberMapper->update with correct field values.
     * Note: The source code has a bug where logUpdate is called with wrong argument count/types.
     * The updateEntity method itself works correctly up to the logUpdate call.
     */
    public function testMergeConflictsUpdatesMitglieder(): void {
        $existingMember = new Member();
        $existingMember->setVorname('Max');
        $existingMember->setNachname('Mueller');
        $existingMember->setGeburtsdatum('2010-01-01');
        $existingMember->setEintritt('2020-01-01');
        $existingMember->setRolle('mitglied');
        $existingMember->setStatus('aktiv');
        $existingMember->setCreatedAt('2020-01-01 00:00:00');
        $existingMember->setUpdatedAt('2020-01-01 00:00:00');
        $ref = new \ReflectionProperty($existingMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($existingMember, 1);

        $this->memberMapper->method('findById')->with(1)->willReturn($existingMember);
        $this->memberMapper->expects($this->once())->method('update');
        $this->encryptionService->method('encrypt')->willReturnArgument(0);

        $resolutions = [
            [
                'existingId' => 1,
                'mergedFields' => [
                    'vorname' => 'Maximilian',
                    'nachname' => 'Mueller-Schmidt',
                    'geschlecht' => 'm',
                    'rolle' => 'leiter',
                    'status' => 'aktiv',
                    'notizen' => 'Updated',
                    'zusatz_notizen' => 'Extra',
                    'kv_typ' => 'GKV',
                    'kv_name' => 'AOK',
                    'allergien' => 'Nuss',
                    'geburtsdatum' => '2010-05-01',
                    'eintritt' => '2019-01-01',
                    'austritt' => '2025-01-01',
                ],
            ],
        ];

        // logUpdate in source has a bug (wrong arg types), so TypeError is expected
        try {
            $this->service->mergeConflicts('mitglieder', $resolutions);
        } catch (\TypeError $e) {
            // Expected: source calls logUpdate(array, string, int) instead of (array, array, string, int)
            $this->assertStringContainsString('logUpdate', $e->getMessage());
        }
    }

    public function testMergeConflictsUpdatesFamilien(): void {
        $existingFamily = new Family();
        $existingFamily->setName('Mueller');
        $existingFamily->setCreatedAt('2020-01-01');
        $existingFamily->setUpdatedAt('2020-01-01');
        $ref = new \ReflectionProperty($existingFamily, 'id');
        $ref->setAccessible(true);
        $ref->setValue($existingFamily, 1);

        $this->familyMapper->method('findById')->with(1)->willReturn($existingFamily);
        $this->familyMapper->expects($this->once())->method('update');
        $this->encryptionService->method('encrypt')->willReturnArgument(0);

        $resolutions = [
            [
                'existingId' => 1,
                'mergedFields' => [
                    'name' => 'Mueller-Schmidt',
                    'bic' => 'DEUTDEDB',
                    'kreditinstitut' => 'Deutsche Bank',
                    'kontoinhaber' => 'Hans Mueller',
                    'iban' => 'DE89370400440532013000',
                ],
            ],
        ];

        try {
            $this->service->mergeConflicts('familien', $resolutions);
        } catch (\TypeError $e) {
            $this->assertStringContainsString('logUpdate', $e->getMessage());
        }
    }

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

        $this->stufeMapper->method('findById')->with(1)->willReturn($existingStufe);
        $this->stufeMapper->expects($this->once())->method('update');

        $resolutions = [
            [
                'existingId' => 1,
                'mergedFields' => [
                    'name' => 'Woelflinge Updated',
                    'sort_order' => '2',
                    'age_range_min' => '7',
                    'age_range_max' => '10',
                    'color' => '#00ff00',
                ],
            ],
        ];

        try {
            $this->service->mergeConflicts('stufen', $resolutions);
        } catch (\TypeError $e) {
            $this->assertStringContainsString('logUpdate', $e->getMessage());
        }
    }

    public function testMergeConflictsUpdatesLager(): void {
        $existingLager = new Lager();
        $existingLager->setName('Sommerlager');
        $existingLager->setStartdatum('2024-07-01');
        $existingLager->setEnddatum('2024-07-14');
        $existingLager->setCreatedAt('2020-01-01');
        $existingLager->setUpdatedAt('2020-01-01');
        $ref = new \ReflectionProperty($existingLager, 'id');
        $ref->setAccessible(true);
        $ref->setValue($existingLager, 1);

        $this->lagerMapper->method('findById')->with(1)->willReturn($existingLager);
        $this->lagerMapper->expects($this->once())->method('update');

        $resolutions = [
            [
                'existingId' => 1,
                'mergedFields' => [
                    'name' => 'Sommerlager Updated',
                    'startdatum' => '2024-08-01',
                    'enddatum' => '2024-08-14',
                    'ort' => 'Berge',
                    'beschreibung' => 'Updated description',
                ],
            ],
        ];

        try {
            $this->service->mergeConflicts('lager', $resolutions);
        } catch (\TypeError $e) {
            $this->assertStringContainsString('logUpdate', $e->getMessage());
        }
    }

    public function testMergeConflictsRejectsInvalidResolution(): void {
        $resolutions = [
            ['existingId' => 0, 'mergedFields' => []],
        ];

        $result = $this->service->mergeConflicts('mitglieder', $resolutions);

        $this->assertEquals(0, $result['updated']);
        $this->assertEquals(1, $result['errors']);
    }

    public function testMergeConflictsRejectsUnsupportedType(): void {
        $existingMember = new Member();
        $this->memberMapper->method('findById')->willReturn($existingMember);

        $resolutions = [
            ['existingId' => 1, 'mergedFields' => ['name' => 'Test']],
        ];

        $result = $this->service->mergeConflicts('adressen', $resolutions);

        $this->assertEquals(0, $result['updated']);
        $this->assertEquals(1, $result['errors']);
    }

    public function testMergeConflictsHandlesUpdateException(): void {
        $this->stufeMapper->method('findById')
            ->willThrowException(new \RuntimeException('Not found'));

        $resolutions = [
            ['existingId' => 999, 'mergedFields' => ['name' => 'Test']],
        ];

        $result = $this->service->mergeConflicts('stufen', $resolutions);

        $this->assertEquals(0, $result['updated']);
        $this->assertEquals(1, $result['errors']);
    }

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

        $this->memberMapper->method('findById')->with(1)->willReturn($existingMember);
        $this->memberMapper->expects($this->once())->method('update');

        $resolutions = [
            [
                'existingId' => 1,
                'mergedFields' => [
                    'allergien' => '[verschluesselt]',
                ],
            ],
        ];

        // Encrypt should NOT be called because the value is the placeholder
        $this->encryptionService->expects($this->never())->method('encrypt');

        try {
            $this->service->mergeConflicts('mitglieder', $resolutions);
        } catch (\TypeError $e) {
            // Expected: source bug in logUpdate call
            $this->assertStringContainsString('logUpdate', $e->getMessage());
        }
    }

    // ── parseFile() with encoding ─────────────────────────────────

    public function testParseFileWithNonUtf8Encoding(): void {
        // Latin-1 encoded content
        $csv = mb_convert_encoding("Name;Sortierung\nWoelflinge;1", 'ISO-8859-1', 'UTF-8');

        $result = $this->service->parseFile($csv, ';', 'ISO-8859-1');

        $this->assertEquals(['Name', 'Sortierung'], $result['columns']);
        $this->assertCount(1, $result['rows']);
    }

    // ── Date format tests ─────────────────────────────────────────

    public function testPreviewAcceptsGermanDate(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;01.01.2010;01.01.2020";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(0, $result['summary']['errors']);
    }

    public function testPreviewAcceptsSlashDate(): void {
        $this->stufeMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->familyMapper->method('findAll')->willReturn([]);
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;01/01/2010;01/01/2020";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(0, $result['summary']['errors']);
    }

    // ── Encrypted fields handling ─────────────────────────────────

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

        $insertedFamily = new Family();
        $insertedFamily->setName('Mueller');
        $ref = new \ReflectionProperty($insertedFamily, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedFamily, 1);

        $this->familyMapper->method('insert')->willReturn($insertedFamily);
        $this->encryptionService->expects($this->never())->method('encrypt');

        $csv = "Name;Kontoinhaber;IBAN\nMueller;[verschluesselt];[verschluesselt]";
        $mapping = [0 => 'name', 1 => 'kontoinhaber', 2 => 'iban'];

        $result = $this->service->execute('familien', $csv, $mapping, ';', 'UTF-8');

        $this->assertEquals(1, $result['created']);
    }

    // ── Migration mode FK resolution for family/lager ──────────────

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

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

        $csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Familienname\nMax;Mueller;2010-01-01;2020-01-01;Mueller";
        $mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'familienname'];

        $result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertEmpty($result['fkWarnings']);
    }

    public function testMigrationModeResolvesLagerByName(): void {
        $lager = new Lager();
        $lager->setName('Sommerlager');
        $ref = new \ReflectionProperty($lager, 'id');
        $ref->setAccessible(true);
        $ref->setValue($lager, 5);

        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $ref2 = new \ReflectionProperty($member, 'id');
        $ref2->setAccessible(true);
        $ref2->setValue($member, 1);

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

        $csv = "Mitgliedername;Lagername;Rolle\nMax Mueller;Sommerlager;Teilnehmer";
        $mapping = [0 => 'mitgliedername', 1 => 'lagername', 2 => 'rolle'];

        $result = $this->service->preview('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8', true);

        $this->assertEmpty($result['fkWarnings']);
    }

    // ── Helper ─────────────────────────────────────────────────────

    private function createRealExportService(): EntityExportService {
        return new EntityExportService(
            $this->createMock(MemberMapper::class),
            $this->createMock(FamilyMapper::class),
            $this->createMock(AddressMapper::class),
            $this->createMock(PhoneMapper::class),
            $this->createMock(EmailMapper::class),
            $this->createMock(StufeMapper::class),
            $this->createMock(StufeHistoryMapper::class),
            $this->createMock(FeeRuleMapper::class),
            $this->createMock(FeeRecordMapper::class),
            $this->createMock(LagerMapper::class),
            $this->createMock(LagerTeilnehmerMapper::class),
            $this->createMock(InjuryMapper::class),
            $this->createMock(SavedQueryMapper::class),
            $this->createMock(EncryptionService::class),
            $this->createMock(LoggerInterface::class)
        );
    }
}
