<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Address;
use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\Email;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\Family;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\FeeRecord;
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Service\CsvExportService;
use OCA\Mitgliederverwaltung\Service\EncryptionService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class CsvExportServiceTest extends TestCase {

    private CsvExportService $service;
    private MemberMapper&MockObject $memberMapper;
    private AddressMapper&MockObject $addressMapper;
    private FamilyMapper&MockObject $familyMapper;
    private PhoneMapper&MockObject $phoneMapper;
    private EmailMapper&MockObject $emailMapper;
    private FeeRecordMapper&MockObject $feeRecordMapper;
    private EncryptionService&MockObject $encryptionService;
    private LoggerInterface&MockObject $logger;

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

        $this->service = new CsvExportService(
            $this->memberMapper,
            $this->addressMapper,
            $this->familyMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->feeRecordMapper,
            $this->encryptionService,
            $this->logger
        );
    }

    private function createMember(int $id, string $vorname, string $nachname): Member {
        $member = new Member();
        $member->setVorname($vorname);
        $member->setNachname($nachname);
        $member->setGeburtsdatum('2000-01-15');
        $member->setGeschlecht('m');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, $id);
        return $member;
    }

    // ── exportMemberList() ─────────────────────────────────────────

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

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

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

        $this->assertArrayHasKey('content', $result);
        $this->assertArrayHasKey('filename', $result);
        $this->assertStringStartsWith("\xEF\xBB\xBF", $result['content']);
        $this->assertStringContainsString('Nachname;Vorname;Geburtsdatum', $result['content']);
        $this->assertStringContainsString('Mueller;Max', $result['content']);
    }

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

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

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

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

    public function testExportMemberListWithoutFilter(): void {
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

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

        $this->assertStringContainsString('Mitgliederliste_', $result['filename']);
        $this->assertStringEndsWith('.csv', $result['filename']);
    }

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

        $address = new Address();
        $address->setMemberId(1);
        $address->setStrasse('Musterstr. 1');
        $address->setPlz('12345');
        $address->setOrt('Berlin');
        $address->setIsPrimary(true);

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

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

        $this->assertStringContainsString('Musterstr. 1', $result['content']);
        $this->assertStringContainsString('12345', $result['content']);
        $this->assertStringContainsString('Berlin', $result['content']);
    }

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

        $phone = new Phone();
        $phone->setMemberId(1);
        $phone->setNumberE164('+4917612345678');

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

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

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

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

        $email = new Email();
        $email->setMemberId(1);
        $email->setEmail('max@example.com');

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

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

        $this->assertStringContainsString('max@example.com', $result['content']);
    }

    public function testExportMemberListFormatsGeschlecht(): void {
        $member = $this->createMember(1, 'Max', 'Mueller');
        $member->setGeschlecht('m');

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

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

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

    public function testExportMemberListFormatsRolle(): void {
        $member = $this->createMember(1, 'Max', 'Mueller');
        $member->setRolle('mitglied');

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

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

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

    public function testExportMemberListFormatsStatus(): void {
        $member = $this->createMember(1, 'Max', 'Mueller');
        $member->setStatus('aktiv');

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

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

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

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

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

        $content = $result['content'];
        $lines = explode("\r\n", $content);
        $headerLine = str_replace("\xEF\xBB\xBF", '', $lines[0]);
        $headers = explode(';', $headerLine);

        $this->assertContains('Nachname', $headers);
        $this->assertContains('Vorname', $headers);
        $this->assertContains('Geburtsdatum', $headers);
        $this->assertContains('Geschlecht', $headers);
        $this->assertContains('Rolle', $headers);
        $this->assertContains('Status', $headers);
        $this->assertContains('Adresse', $headers);
        $this->assertContains('PLZ', $headers);
        $this->assertContains('Ort', $headers);
        $this->assertContains('Telefon', $headers);
        $this->assertContains('E-Mail', $headers);
    }

    // ── exportFeeRecords() ─────────────────────────────────────────

    public function testExportFeeRecords(): void {
        $record = new FeeRecord();
        $record->setMemberId(1);
        $record->setYear(2024);
        $record->setAmount('100.00');
        $record->setPaid(true);
        $record->setPaymentDate('2024-03-15');
        $record->setManuellAngepasst(false);
        $ref = new \ReflectionProperty($record, 'id');
        $ref->setAccessible(true);
        $ref->setValue($record, 1);

        $member = $this->createMember(1, 'Max', 'Mueller');

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

        $result = $this->service->exportFeeRecords(2024);

        $this->assertStringContainsString('Mueller', $result['content']);
        $this->assertStringContainsString('100.00', $result['content']);
        $this->assertStringContainsString('Ja', $result['content']); // paid
        $this->assertEquals('Beitraege_2024.csv', $result['filename']);
    }

    public function testExportFeeRecordsHandlesDeletedMember(): void {
        $record = new FeeRecord();
        $record->setMemberId(999);
        $record->setYear(2024);
        $record->setAmount('50.00');
        $record->setPaid(false);
        $record->setManuellAngepasst(false);
        $ref = new \ReflectionProperty($record, 'id');
        $ref->setAccessible(true);
        $ref->setValue($record, 1);

        $this->feeRecordMapper->method('findByYear')->willReturn([$record]);
        $this->memberMapper->method('findById')
            ->willThrowException(new \OCP\AppFramework\Db\DoesNotExistException(''));

        $result = $this->service->exportFeeRecords(2024);

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

    // ── exportBirthdayList() ───────────────────────────────────────

    public function testExportBirthdayList(): void {
        $member = $this->createMember(1, 'Max', 'Mueller');
        $member->setGeburtsdatum('2000-03-15');
        $member->setStatus('aktiv');

        $this->memberMapper->method('findByBirthdayMonth')->with(3)->willReturn([$member]);

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

        $this->assertStringContainsString('Mueller', $result['content']);
        $this->assertStringContainsString('2000-03-15', $result['content']);
        $this->assertStringContainsString('Geburtstage_Maerz.csv', $result['filename']);
    }

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

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

        $this->assertStringContainsString('Nachname;Vorname;Geburtsdatum;Alter;Status', $result['content']);
        $this->assertStringContainsString('Geburtstage_Juni.csv', $result['filename']);
    }

    // ── exportBankingList() ────────────────────────────────────────

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

        $member = $this->createMember(1, 'Max', 'Mueller');
        $member->setFamilyId(1);

        $this->familyMapper->method('findAll')->willReturn([$family]);
        $this->encryptionService->method('decrypt')
            ->willReturnCallback(fn($v) => match ($v) {
                'ENC:encrypted_name' => 'Hans Mueller',
                'ENC:encrypted_iban' => 'DE89370400440532013000',
                default => $v,
            });
        $this->memberMapper->method('findByFamily')->willReturn([$member]);

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

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

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

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

        $this->assertStringContainsString('Familie;Kontoinhaber;IBAN;BIC;Kreditinstitut;Mitglieder', $result['content']);
    }

    // ── CSV format ─────────────────────────────────────────────────

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

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

        $this->assertStringStartsWith("\xEF\xBB\xBF", $result['content']);
    }

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

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

        $lines = explode("\r\n", $result['content']);
        $headerLine = str_replace("\xEF\xBB\xBF", '', $lines[0]);
        $this->assertGreaterThan(0, substr_count($headerLine, ';'));
    }

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

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

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

    // -- CSV injection prevention (Issue #174) --

    public function testSanitizeCsvFieldPrefixesEquals(): void {
        $this->assertSame("'=SUM(A1:A10)", CsvExportService::sanitizeCsvField('=SUM(A1:A10)'));
    }

    public function testSanitizeCsvFieldPrefixesPlus(): void {
        $this->assertSame("'+cmd|' /C calc'!A0", CsvExportService::sanitizeCsvField("+cmd|' /C calc'!A0"));
    }

    public function testSanitizeCsvFieldPrefixesMinus(): void {
        $this->assertSame("'-1+1", CsvExportService::sanitizeCsvField('-1+1'));
    }

    public function testSanitizeCsvFieldPrefixesAt(): void {
        $this->assertSame("'@SUM(A1)", CsvExportService::sanitizeCsvField('@SUM(A1)'));
    }

    public function testSanitizeCsvFieldPreservesNormalText(): void {
        $this->assertSame('Normal text', CsvExportService::sanitizeCsvField('Normal text'));
    }

    public function testSanitizeCsvFieldPreservesEmpty(): void {
        $this->assertSame('', CsvExportService::sanitizeCsvField(''));
    }

    public function testSanitizeCsvFieldPreservesNumeric(): void {
        $this->assertSame('12345', CsvExportService::sanitizeCsvField('12345'));
    }
}
