<?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\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\DuplicateMemberException;
use OCA\Mitgliederverwaltung\Service\MemberService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\IDBConnection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Comprehensive validation tests for MemberService.
 *
 * Covers date plausibility, required fields, and duplicate detection
 * per Issue #66.
 */
class MemberValidationTest extends TestCase {

    private MemberService $service;
    private MemberMapper&MockObject $memberMapper;
    private AddressMapper&MockObject $addressMapper;
    private PhoneMapper&MockObject $phoneMapper;
    private EmailMapper&MockObject $emailMapper;
    private FamilyMapper&MockObject $familyMapper;
    private StufeHistoryMapper&MockObject $stufeHistoryMapper;
    private AuditService&MockObject $auditService;
    private LoggerInterface&MockObject $logger;
    private IDBConnection&MockObject $db;

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

        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->addressMapper = $this->createMock(AddressMapper::class);
        $this->phoneMapper = $this->createMock(PhoneMapper::class);
        $this->emailMapper = $this->createMock(EmailMapper::class);
        $this->familyMapper = $this->createMock(FamilyMapper::class);
        $this->stufeHistoryMapper = $this->createMock(StufeHistoryMapper::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->logger = $this->createMock(LoggerInterface::class);
        $this->db = $this->createMock(IDBConnection::class);

        $this->service = new MemberService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->stufeHistoryMapper,
            $this->auditService,
            $this->logger,
            null,
            $this->db
        );

        // Default: no duplicates found
        $this->memberMapper->method('search')->willReturn([]);
    }

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

    private function validData(): array {
        return [
            'vorname' => 'Max',
            'nachname' => 'Mustermann',
            'geburtsdatum' => '2010-06-15',
            'eintritt' => '2020-09-01',
            'rolle' => 'mitglied',
        ];
    }

    // ── Required fields ─────────────────────────────────────────────

    public function testMissingVornameThrowsValidationException(): void {
        $data = $this->validData();
        unset($data['vorname']);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/vorname/');

        $this->service->create($data);
    }

    public function testMissingNachnameThrowsValidationException(): void {
        $data = $this->validData();
        unset($data['nachname']);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/nachname/');

        $this->service->create($data);
    }

    public function testMissingGeburtsdatumIsAllowed(): void {
        $data = $this->validData();
        unset($data['geburtsdatum']);

        // geburtsdatum is optional since Issue #162 — no exception expected
        $this->memberMapper->method('search')->willReturn([]);
        $this->memberMapper->method('insert')->willReturnCallback(function (Member $m) {
            $m->setId(1);
            return $m;
        });
        $this->addressMapper->method('findByMemberId')->willReturn([]);
        $this->phoneMapper->method('findByMemberId')->willReturn([]);
        $this->emailMapper->method('findByMemberId')->willReturn([]);

        $result = $this->service->create($data);
        $this->assertSame(1, $result['id']);
    }

    public function testMissingEintrittThrowsValidationException(): void {
        $data = $this->validData();
        unset($data['eintritt']);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/eintritt/');

        $this->service->create($data);
    }

    public function testEmptyVornameThrowsValidationException(): void {
        $data = $this->validData();
        $data['vorname'] = '';

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/vorname/');

        $this->service->create($data);
    }

    public function testWhitespaceOnlyVornameThrowsValidationException(): void {
        $data = $this->validData();
        $data['vorname'] = '   ';

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/vorname/');

        $this->service->create($data);
    }

    public function testMultipleMissingFieldsListedInError(): void {
        $data = [];

        try {
            $this->service->create($data);
            $this->fail('Expected ValidationException');
        } catch (ValidationException $e) {
            $message = $e->getMessage();
            $this->assertStringContainsString('vorname', $message);
            $this->assertStringContainsString('nachname', $message);
            $this->assertStringContainsString('eintritt', $message);
            // geburtsdatum is optional since Issue #162
            $this->assertStringNotContainsString('geburtsdatum', $message);
        }
    }

    // ── Date plausibility: birthday in future ───────────────────────

    public function testBirthdayInFutureThrowsValidationException(): void {
        $data = $this->validData();
        $data['geburtsdatum'] = '2030-01-01';
        $data['eintritt'] = '2030-09-01';

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/Zukunft/');

        $this->service->create($data);
    }

    // ── Date plausibility: birthday > 120 years ago ─────────────────

    public function testBirthdayMoreThan120YearsAgoThrowsValidationException(): void {
        $data = $this->validData();
        $data['geburtsdatum'] = '1850-01-01';
        $data['eintritt'] = '1870-01-01';

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/120/');

        $this->service->create($data);
    }

    // ── Date plausibility: eintritt before birthday ─────────────────

    public function testEintrittBeforeBirthdayThrowsValidationException(): void {
        $data = $this->validData();
        $data['geburtsdatum'] = '2010-06-15';
        $data['eintritt'] = '2005-09-01';

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessageMatches('/Eintrittsdatum.*Geburtsdatum/');

        $this->service->create($data);
    }

    // ── Date plausibility: valid dates pass ─────────────────────────

    public function testValidDatesPassValidation(): void {
        $data = $this->validData();

        $insertedMember = new Member();
        $insertedMember->setId(1);
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mustermann');
        $insertedMember->setGeburtsdatum('2010-06-15');
        $insertedMember->setEintritt('2020-09-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2026-01-01 00:00:00');
        $insertedMember->setUpdatedAt('2026-01-01 00:00:00');

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

        $result = $this->service->create($data);

        $this->assertSame('Max', $result['vorname']);
        $this->assertSame('Mustermann', $result['nachname']);
    }

    // ── Duplicate detection ─────────────────────────────────────────

    public function testDuplicateDetectionByNameAndBirthdayThrows(): void {
        $data = $this->validData();

        $existingMember = new Member();
        $existingMember->setId(42);
        $existingMember->setVorname('Max');
        $existingMember->setNachname('Mustermann');
        $existingMember->setGeburtsdatum('2010-06-15');
        $existingMember->setEintritt('2020-09-01');
        $existingMember->setRolle('mitglied');
        $existingMember->setStatus('aktiv');
        $existingMember->setCreatedAt('2026-01-01 00:00:00');
        $existingMember->setUpdatedAt('2026-01-01 00:00:00');

        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->memberMapper->method('search')->willReturn([$existingMember]);

        // Rebuild service with new mapper
        $this->service = new MemberService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->stufeHistoryMapper,
            $this->auditService,
            $this->logger,
            null,
            $this->db
        );

        $this->expectException(DuplicateMemberException::class);
        $this->expectExceptionMessageMatches('/ID: 42/');

        $this->service->create($data);
    }

    public function testDuplicateDetectionCaseInsensitive(): void {
        $data = $this->validData();
        $data['vorname'] = 'MAX';
        $data['nachname'] = 'mustermann';

        $existingMember = new Member();
        $existingMember->setId(42);
        $existingMember->setVorname('max');
        $existingMember->setNachname('Mustermann');
        $existingMember->setGeburtsdatum('2010-06-15');
        $existingMember->setEintritt('2020-09-01');
        $existingMember->setRolle('mitglied');
        $existingMember->setStatus('aktiv');
        $existingMember->setCreatedAt('2026-01-01 00:00:00');
        $existingMember->setUpdatedAt('2026-01-01 00:00:00');

        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->memberMapper->method('search')->willReturn([$existingMember]);

        $this->service = new MemberService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->stufeHistoryMapper,
            $this->auditService,
            $this->logger,
            null,
            $this->db
        );

        $this->expectException(DuplicateMemberException::class);

        $this->service->create($data);
    }

    public function testSameNameDifferentBirthdayNoDuplicate(): void {
        $data = $this->validData();

        $existingMember = new Member();
        $existingMember->setId(42);
        $existingMember->setVorname('Max');
        $existingMember->setNachname('Mustermann');
        $existingMember->setGeburtsdatum('2012-03-20'); // Different birthday
        $existingMember->setEintritt('2020-09-01');
        $existingMember->setRolle('mitglied');
        $existingMember->setStatus('aktiv');
        $existingMember->setCreatedAt('2026-01-01 00:00:00');
        $existingMember->setUpdatedAt('2026-01-01 00:00:00');

        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->memberMapper->method('search')->willReturn([$existingMember]);

        $insertedMember = new Member();
        $insertedMember->setId(43);
        $insertedMember->setVorname('Max');
        $insertedMember->setNachname('Mustermann');
        $insertedMember->setGeburtsdatum('2010-06-15');
        $insertedMember->setEintritt('2020-09-01');
        $insertedMember->setRolle('mitglied');
        $insertedMember->setStatus('aktiv');
        $insertedMember->setCreatedAt('2026-01-01 00:00:00');
        $insertedMember->setUpdatedAt('2026-01-01 00:00:00');

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

        $this->service = new MemberService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->stufeHistoryMapper,
            $this->auditService,
            $this->logger,
            null,
            $this->db
        );

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

        // Should not throw — different birthday means not a duplicate
        $result = $this->service->create($data);
        $this->assertSame(43, $result['id']);
    }

    public function testDuplicateDetectionTrimsWhitespace(): void {
        $data = $this->validData();
        $data['vorname'] = '  Max  ';
        $data['nachname'] = '  Mustermann  ';

        $existingMember = new Member();
        $existingMember->setId(42);
        $existingMember->setVorname('Max');
        $existingMember->setNachname('Mustermann');
        $existingMember->setGeburtsdatum('2010-06-15');
        $existingMember->setEintritt('2020-09-01');
        $existingMember->setRolle('mitglied');
        $existingMember->setStatus('aktiv');
        $existingMember->setCreatedAt('2026-01-01 00:00:00');
        $existingMember->setUpdatedAt('2026-01-01 00:00:00');

        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->memberMapper->method('search')->willReturn([$existingMember]);

        $this->service = new MemberService(
            $this->memberMapper,
            $this->addressMapper,
            $this->phoneMapper,
            $this->emailMapper,
            $this->familyMapper,
            $this->stufeHistoryMapper,
            $this->auditService,
            $this->logger,
            null,
            $this->db
        );

        $this->expectException(DuplicateMemberException::class);

        $this->service->create($data);
    }
}
