<?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\MemberMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\ImportService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for ImportService CSV parsing, validation, and duplicate detection.
 *
 * Part of Issue #58.
 */
class ImportServiceTest extends TestCase {

    private ImportService $service;
    private MemberMapper|MockObject $memberMapper;
    private AddressMapper|MockObject $addressMapper;
    private PhoneMapper|MockObject $phoneMapper;
    private EmailMapper|MockObject $emailMapper;
    private FamilyMapper|MockObject $familyMapper;
    private AuditService|MockObject $auditService;
    private LoggerInterface|MockObject $logger;

    protected function setUp(): void {
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->addressMapper = $this->createMock(AddressMapper::class);
        $this->phoneMapper = $this->createMock(PhoneMapper::class);
        $this->emailMapper = $this->createMock(EmailMapper::class);
        $this->familyMapper = $this->createMock(FamilyMapper::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new ImportService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->auditService,
            $this->logger
        );
    }

    // ── CSV Parsing ─────────────────────────────────────────────────

    public function testParseFileCommaDelimited(): void {
        $csv = "Vorname,Nachname,Geburtsdatum\nMax,Mueller,2010-03-15\nAnna,Schmidt,2012-07-22";

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

        $this->assertCount(3, $result['columns']);
        $this->assertEquals('Vorname', $result['columns'][0]);
        $this->assertEquals('Nachname', $result['columns'][1]);
        $this->assertEquals('Geburtsdatum', $result['columns'][2]);
        $this->assertCount(2, $result['rows']);
        $this->assertEquals(2, $result['totalRows']);
    }

    public function testParseFileSemicolonDelimited(): void {
        $csv = "Vorname;Nachname;Geburtsdatum\nMax;Mueller;2010-03-15";

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

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

    public function testParseFileThrowsOnEmptyFile(): void {
        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('leer');

        $this->service->parseFile('', ',', 'UTF-8');
    }

    public function testParseFileThrowsOnHeaderOnly(): void {
        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('mindestens');

        $this->service->parseFile("Vorname,Nachname", ',', 'UTF-8');
    }

    public function testParseFileRemovesBom(): void {
        $csv = "\xEF\xBB\xBFVorname,Nachname\nMax,Mueller";

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

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

    // ── Target Fields ───────────────────────────────────────────────

    public function testGetTargetFieldsContainsRequiredFields(): void {
        $fields = $this->service->getTargetFields();

        $required = array_filter($fields, fn($f) => $f['required']);
        $requiredKeys = array_column($required, 'key');

        $this->assertContains('vorname', $requiredKeys);
        $this->assertContains('nachname', $requiredKeys);
        $this->assertContains('eintritt', $requiredKeys);
        // geburtsdatum is optional since Issue #162
        $this->assertNotContains('geburtsdatum', $requiredKeys);
    }

    // ── Preview/Dry-run ─────────────────────────────────────────────

    public function testPreviewDetectsValidRows(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')
            ->willReturn(null); // No duplicate

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewDetectsMissingRequiredFields(): void {
        $csv = "Vorname,Nachname\nMax,Mueller";

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
        ];

        $result = $this->service->preview($csv, $mapping);

        // Missing geburtsdatum and eintritt
        $this->assertEquals(0, $result['summary']['toCreate']);
        $this->assertEquals(1, $result['summary']['errors']);
    }

    public function testPreviewDetectsDuplicates(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        // Member already exists
        $existingMember = $this->createMock(\OCA\Mitgliederverwaltung\Db\Member::class);
        $this->memberMapper->method('findByNameAndBirthdate')
            ->willReturn($existingMember);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewDetectsInvalidDate(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,invalid-date,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')
            ->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewWithGermanDateFormat(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,15.03.2010,01.01.2020";

        $this->memberMapper->method('findByNameAndBirthdate')
            ->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    // ── getTargetFields() ──────────────────────────────────────────

    public function testGetTargetFieldsReturnsAllFields(): void {
        $fields = $this->service->getTargetFields();

        $this->assertNotEmpty($fields);
        $keys = array_column($fields, 'key');
        $this->assertContains('vorname', $keys);
        $this->assertContains('nachname', $keys);
        $this->assertContains('email', $keys);
        $this->assertContains('telefon', $keys);
        $this->assertContains('iban', $keys);
        $this->assertContains('strasse', $keys);
    }

    public function testGetTargetFieldsHasLabelsAndRequired(): void {
        $fields = $this->service->getTargetFields();

        foreach ($fields as $field) {
            $this->assertArrayHasKey('key', $field);
            $this->assertArrayHasKey('label', $field);
            $this->assertArrayHasKey('required', $field);
        }
    }

    // ── Preview edge cases ─────────────────────────────────────────

    public function testPreviewDetectsInvalidPhone(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,Telefon\nMax,Mueller,2010-03-15,2020-01-01,invalid";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
            '4' => 'telefon',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewDetectsInvalidEmail(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,Email\nMax,Mueller,2010-03-15,2020-01-01,not-an-email";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
            '4' => 'email',
        ];

        $result = $this->service->preview($csv, $mapping);

        $this->assertEquals(1, $result['summary']['errors']);
        $this->assertStringContainsString('E-Mail', $result['errors'][0]['_errors'][0]);
    }

    public function testPreviewDetectsInvalidIban(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,IBAN\nMax,Mueller,2010-03-15,2020-01-01,INVALID_IBAN";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
            '4' => 'iban',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewDetectsImplausibleDate(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,1850-01-01,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewWithSlashDateFormat(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,15/03/2010,01/01/2020";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

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

    public function testPreviewDuplicateDetectionHandlesException(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')
            ->willThrowException(new \RuntimeException('DB error'));

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->preview($csv, $mapping);

        // Should not be marked as duplicate when exception occurs
        $this->assertEquals(0, $result['summary']['duplicates']);
        $this->assertEquals(1, $result['summary']['toCreate']);
    }

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

    public function testExecuteCreatesMembers(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $insertedMember = new \OCA\Mitgliederverwaltung\Db\Member();
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mueller');
        $insertedMember->setGeburtsdatum('2010-03-15');
        $insertedMember->setEintritt('2020-01-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2024-01-01');
        $insertedMember->setUpdatedAt('2024-01-01');
        $ref = new \ReflectionProperty($insertedMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedMember, 1);
        $this->memberMapper->method('insert')->willReturn($insertedMember);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->execute($csv, $mapping);

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

    public function testExecuteSkipsDuplicates(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        $existingMember = new \OCA\Mitgliederverwaltung\Db\Member();
        $this->memberMapper->method('findByNameAndBirthdate')->willReturn($existingMember);

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
            '2' => 'geburtsdatum',
            '3' => 'eintritt',
        ];

        $result = $this->service->execute($csv, $mapping);

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

    public function testExecuteSkipsInvalidRows(): void {
        $csv = "Vorname,Nachname\nMax,Mueller";

        $mapping = [
            '0' => 'vorname',
            '1' => 'nachname',
        ];

        $result = $this->service->execute($csv, $mapping);

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

    public function testExecuteCreatesAddressPhoneEmail(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,Strasse,PLZ,Ort,Telefon,Email\nMax,Mueller,2010-03-15,2020-01-01,Test 1,12345,Berlin,+4917612345678,max@test.de";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $insertedMember = new \OCA\Mitgliederverwaltung\Db\Member();
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mueller');
        $insertedMember->setGeburtsdatum('2010-03-15');
        $insertedMember->setEintritt('2020-01-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2024-01-01');
        $insertedMember->setUpdatedAt('2024-01-01');
        $ref = new \ReflectionProperty($insertedMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedMember, 1);
        $this->memberMapper->method('insert')->willReturn($insertedMember);

        $this->addressMapper->expects($this->once())->method('insert');
        $this->phoneMapper->expects($this->once())->method('insert');
        $this->emailMapper->expects($this->once())->method('insert');

        $mapping = [
            '0' => 'vorname', '1' => 'nachname', '2' => 'geburtsdatum', '3' => 'eintritt',
            '4' => 'strasse', '5' => 'plz', '6' => 'ort', '7' => 'telefon', '8' => 'email',
        ];

        $result = $this->service->execute($csv, $mapping);

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

    public function testExecuteWithFamilyGrouping(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,PLZ,Ort\nMax,Mueller,2010-03-15,2020-01-01,12345,Berlin\nAnna,Mueller,2012-05-20,2022-01-01,12345,Berlin";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $insertedMember = new \OCA\Mitgliederverwaltung\Db\Member();
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mueller');
        $insertedMember->setGeburtsdatum('2010-03-15');
        $insertedMember->setEintritt('2020-01-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2024-01-01');
        $insertedMember->setUpdatedAt('2024-01-01');
        $ref = new \ReflectionProperty($insertedMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedMember, 1);
        $this->memberMapper->method('insert')->willReturn($insertedMember);

        $insertedFamily = new \OCA\Mitgliederverwaltung\Db\Family();
        $insertedFamily->setName('Familie Mueller');
        $insertedFamily->setCreatedAt('2024-01-01');
        $insertedFamily->setUpdatedAt('2024-01-01');
        $refF = new \ReflectionProperty($insertedFamily, 'id');
        $refF->setAccessible(true);
        $refF->setValue($insertedFamily, 10);
        // Only one family insert should happen (second member reuses same family)
        $this->familyMapper->expects($this->once())->method('insert')->willReturn($insertedFamily);

        $mapping = [
            '0' => 'vorname', '1' => 'nachname', '2' => 'geburtsdatum', '3' => 'eintritt',
            '4' => 'plz', '5' => 'ort',
        ];

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

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

    public function testExecuteHandlesInsertException(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt\nMax,Mueller,2010-03-15,2020-01-01";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
        $this->memberMapper->method('insert')
            ->willThrowException(new \RuntimeException('DB error'));

        $mapping = [
            '0' => 'vorname', '1' => 'nachname', '2' => 'geburtsdatum', '3' => 'eintritt',
        ];

        $result = $this->service->execute($csv, $mapping);

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

    // ── parseFile with encoding ────────────────────────────────────

    public function testParseFileWithLatin1Encoding(): void {
        $csv = mb_convert_encoding("Name,Value\nTest,1", 'ISO-8859-1', 'UTF-8');

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

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

    public function testParseFileLimitsTo50Rows(): void {
        $lines = ["Name,Value"];
        for ($i = 0; $i < 100; $i++) {
            $lines[] = "Test$i,$i";
        }
        $csv = implode("\n", $lines);

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

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

    public function testParseFileSkipsEmptyRows(): void {
        $csv = "Name,Value\nTest,1\n\n\nTest2,2\n";

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

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

    // ── Execute with optional fields ───────────────────────────────

    public function testExecuteWithOptionalFields(): void {
        $csv = "Vorname,Nachname,Geburtsdatum,Eintritt,Geschlecht,Rolle,Status,Notizen,KV_Typ,KV_Name\nMax,Mueller,2010-03-15,2020-01-01,m,mitglied,aktiv,TestNote,GKV,AOK";

        $this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);

        $insertedMember = new \OCA\Mitgliederverwaltung\Db\Member();
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mueller');
        $insertedMember->setGeburtsdatum('2010-03-15');
        $insertedMember->setEintritt('2020-01-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2024-01-01');
        $insertedMember->setUpdatedAt('2024-01-01');
        $ref = new \ReflectionProperty($insertedMember, 'id');
        $ref->setAccessible(true);
        $ref->setValue($insertedMember, 1);
        $this->memberMapper->method('insert')->willReturn($insertedMember);

        $mapping = [
            '0' => 'vorname', '1' => 'nachname', '2' => 'geburtsdatum', '3' => 'eintritt',
            '4' => 'geschlecht', '5' => 'rolle', '6' => 'status', '7' => 'notizen',
            '8' => 'kv_typ', '9' => 'kv_name',
        ];

        $result = $this->service->execute($csv, $mapping);

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