<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Family;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\FamilyService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class FamilyServiceTest extends TestCase {

    private FamilyService $service;
    private FamilyMapper&MockObject $familyMapper;
    private MemberMapper&MockObject $memberMapper;
    private AuditService&MockObject $auditService;
    private LoggerInterface&MockObject $logger;

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

        $this->familyMapper = $this->createMock(FamilyMapper::class);
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->service = new FamilyService(
            $this->familyMapper,
            $this->memberMapper,
            $this->auditService,
            $this->logger
        );
    }

    // ── Helper factories ────────────────────────────────────────────

    private function createFamily(
        int $id = 1,
        string $name = 'Mustermann',
        ?string $ibanEncrypted = null,
        ?string $kontoinhaberEncrypted = null
    ): Family {
        $family = new Family();
        $family->setId($id);
        $family->setName($name);
        $family->setIbanEncrypted($ibanEncrypted);
        $family->setKontoinhaberEncrypted($kontoinhaberEncrypted);
        $family->setCreatedAt('2020-01-01 00:00:00');
        $family->setUpdatedAt('2020-01-01 00:00:00');
        return $family;
    }

    private function createMember(
        int $id = 1,
        string $vorname = 'Max',
        string $nachname = 'Mustermann',
        string $status = 'aktiv',
        string $rolle = 'mitglied',
        ?int $familyId = null
    ): Member {
        $member = new Member();
        $member->setId($id);
        $member->setVorname($vorname);
        $member->setNachname($nachname);
        $member->setGeburtsdatum('2010-01-15');
        $member->setEintritt('2020-01-01');
        $member->setStatus($status);
        $member->setRolle($rolle);
        $member->setFamilyId($familyId);
        $member->setCreatedAt('2020-01-01 00:00:00');
        $member->setUpdatedAt('2020-01-01 00:00:00');
        return $member;
    }

    // ── find() ──────────────────────────────────────────────────────

    public function testFindReturnsFamilyWithMembers(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member = $this->createMember(1, 'Max', 'Mustermann', 'aktiv', 'mitglied', 1);

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->memberMapper->method('findByFamily')->with(1)->willReturn([$member]);

        $result = $this->service->find(1);

        $this->assertSame('Mustermann', $result['name']);
        $this->assertCount(1, $result['members']);
        $this->assertSame('Max', $result['members'][0]['vorname']);
    }

    public function testFindThrowsDoesNotExistException(): void {
        $this->familyMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

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

        $this->service->find(999);
    }

    public function testFindIncludesActiveChildrenCount(): void {
        $family = $this->createFamily(1);
        $activeMitglied = $this->createMember(1, 'Max', 'Mustermann', 'aktiv', 'mitglied', 1);
        $inactiveMitglied = $this->createMember(2, 'Anna', 'Mustermann', 'inaktiv', 'mitglied', 1);
        $parent = $this->createMember(3, 'Hans', 'Mustermann', 'aktiv', 'erziehungsberechtigter', 1);

        $this->familyMapper->method('findById')->willReturn($family);
        $this->memberMapper->method('findByFamily')
            ->willReturn([$activeMitglied, $inactiveMitglied, $parent]);

        $result = $this->service->find(1);

        $this->assertSame(1, $result['activeChildrenCount']);
    }

    // ── findAll() ───────────────────────────────────────────────────

    public function testFindAllReturnsList(): void {
        $family1 = $this->createFamily(1, 'Mustermann');
        $family2 = $this->createFamily(2, 'Schmidt');

        $this->familyMapper->method('findAll')->willReturn([$family1, $family2]);
        $this->memberMapper->method('findByFamily')->willReturn([]);

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

        $this->assertCount(2, $result);
        $this->assertSame('Mustermann', $result[0]['name']);
        $this->assertSame('Schmidt', $result[1]['name']);
    }

    public function testFindAllWithPagination(): void {
        $this->familyMapper->expects($this->once())
            ->method('findAll')
            ->with(10, 5)
            ->willReturn([]);

        $this->service->findAll(10, 5);
    }

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

        $result = $this->service->findAll();
        $this->assertSame([], $result);
    }

    // ── search() ────────────────────────────────────────────────────

    public function testSearchReturnsMatchingFamilies(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $this->familyMapper->method('findByName')->with('Muster')->willReturn([$family]);
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $result = $this->service->search('Muster');

        $this->assertCount(1, $result);
        $this->assertSame('Mustermann', $result[0]['name']);
    }

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

        $result = $this->service->search('Nonexistent');
        $this->assertSame([], $result);
    }

    // ── create() ────────────────────────────────────────────────────

    public function testCreateValidFamily(): void {
        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) {
                $this->assertSame('Mustermann', $f->getName());
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $this->auditService->expects($this->once())->method('logCreate');

        $result = $this->service->create(['name' => 'Mustermann']);

        $this->assertSame('Mustermann', $result['name']);
    }

    public function testCreateTrimsName(): void {
        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) {
                $this->assertSame('Mustermann', $f->getName());
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $this->service->create(['name' => '  Mustermann  ']);
    }

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

        $this->service->create([]);
    }

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

        $this->service->create(['name' => '   ']);
    }

    public function testCreateWithValidIban(): void {
        $data = [
            'name' => 'Mustermann',
            'ibanEncrypted' => 'DE89370400440532013000',
        ];

        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) {
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

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

    public function testCreateWithInvalidIbanThrowsValidationException(): void {
        $data = [
            'name' => 'Mustermann',
            'ibanEncrypted' => 'INVALID_IBAN',
        ];

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

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

    public function testCreateWithEmptyIbanDoesNotValidate(): void {
        $data = [
            'name' => 'Mustermann',
            'ibanEncrypted' => '',
        ];

        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) {
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

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

    public function testCreateWithNullIbanDoesNotValidate(): void {
        $data = [
            'name' => 'Mustermann',
            'ibanEncrypted' => null,
        ];

        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) {
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

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

    public function testCreateWithAllFields(): void {
        $data = [
            'name' => 'Mustermann',
            'kontoinhaberEncrypted' => 'ENC:Max Mustermann',
            'ibanEncrypted' => 'DE89370400440532013000',
            'bic' => 'COBADEFFXXX',
            'kreditinstitut' => 'Commerzbank',
        ];

        $this->familyMapper->method('insert')
            ->willReturnCallback(function (Family $f) use ($data) {
                $this->assertSame('ENC:Max Mustermann', $f->getKontoinhaberEncrypted());
                $this->assertSame('DE89370400440532013000', $f->getIbanEncrypted());
                $this->assertSame('COBADEFFXXX', $f->getBic());
                $this->assertSame('Commerzbank', $f->getKreditinstitut());
                $f->setId(1);
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

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

    // ── update() ────────────────────────────────────────────────────

    public function testUpdateName(): void {
        $family = $this->createFamily(1, 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->familyMapper->method('update')
            ->willReturnCallback(function (Family $f) {
                $this->assertSame('Schmidt', $f->getName());
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $this->auditService->expects($this->once())->method('logUpdate');

        $result = $this->service->update(1, ['name' => 'Schmidt']);
        $this->assertSame('Schmidt', $result['name']);
    }

    public function testUpdateEmptyNameThrowsValidationException(): void {
        $family = $this->createFamily(1, 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('Familienname darf nicht leer sein');

        $this->service->update(1, ['name' => '   ']);
    }

    public function testUpdateWithInvalidIbanThrowsValidationException(): void {
        $this->expectException(ValidationException::class);

        $this->service->update(1, ['ibanEncrypted' => 'INVALID']);
    }

    public function testUpdateThrowsForMissingFamily(): void {
        $this->familyMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

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

        $this->service->update(999, ['name' => 'Test']);
    }

    public function testUpdateBankingFields(): void {
        $family = $this->createFamily(1, 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->familyMapper->method('update')
            ->willReturnCallback(function (Family $f) {
                $this->assertSame('COBADEFFXXX', $f->getBic());
                $this->assertSame('Commerzbank', $f->getKreditinstitut());
                return $f;
            });
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $this->service->update(1, [
            'bic' => 'COBADEFFXXX',
            'kreditinstitut' => 'Commerzbank',
        ]);
    }

    public function testUpdateWithValidIban(): void {
        $family = $this->createFamily(1, 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->familyMapper->method('update')->willReturnCallback(fn(Family $f) => $f);
        $this->memberMapper->method('findByFamily')->willReturn([]);

        $result = $this->service->update(1, ['ibanEncrypted' => 'DE89370400440532013000']);
        $this->assertSame('Mustermann', $result['name']);
    }

    // ── delete() ────────────────────────────────────────────────────

    public function testDeleteUnlinksMembersFirst(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member1 = $this->createMember(1, 'Max', 'Mustermann', 'aktiv', 'mitglied', 1);
        $member2 = $this->createMember(2, 'Anna', 'Mustermann', 'aktiv', 'mitglied', 1);

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->memberMapper->method('findByFamily')->with(1)->willReturn([$member1, $member2]);

        $this->memberMapper->expects($this->exactly(2))
            ->method('update')
            ->willReturnCallback(function (Member $m) {
                $this->assertNull($m->getFamilyId());
                return $m;
            });

        $this->familyMapper->expects($this->once())->method('delete')->with($family);
        $this->auditService->expects($this->once())->method('logDelete')->with('family', 1);
        $this->logger->expects($this->once())->method('info');

        $this->service->delete(1);
    }

    public function testDeleteWithNoMembers(): void {
        $family = $this->createFamily(1, 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->memberMapper->method('findByFamily')->with(1)->willReturn([]);

        $this->memberMapper->expects($this->never())->method('update');
        $this->familyMapper->expects($this->once())->method('delete');

        $this->service->delete(1);
    }

    public function testDeleteThrowsForMissingFamily(): void {
        $this->familyMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

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

        $this->service->delete(999);
    }

    // ── linkMember() ────────────────────────────────────────────────

    public function testLinkMemberSetsFamilyId(): void {
        $family = $this->createFamily(1, 'Mustermann');
        $member = $this->createMember(1, 'Max', 'Mustermann');

        $this->familyMapper->method('findById')->with(1)->willReturn($family);
        $this->memberMapper->method('findById')->with(10)->willReturn($member);

        $this->memberMapper->expects($this->once())
            ->method('update')
            ->willReturnCallback(function (Member $m) {
                $this->assertSame(1, $m->getFamilyId());
                return $m;
            });

        $this->service->linkMember(1, 10);
    }

    public function testLinkMemberThrowsForMissingFamily(): void {
        $this->familyMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

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

        $this->service->linkMember(999, 1);
    }

    public function testLinkMemberThrowsForMissingMember(): void {
        $family = $this->createFamily(1);
        $this->familyMapper->method('findById')->willReturn($family);
        $this->memberMapper->method('findById')
            ->willThrowException(new DoesNotExistException('Not found'));

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

        $this->service->linkMember(1, 999);
    }

    // ── unlinkMember() ──────────────────────────────────────────────

    public function testUnlinkMemberClearsFamilyId(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', 'aktiv', 'mitglied', 5);

        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        $this->memberMapper->expects($this->once())
            ->method('update')
            ->willReturnCallback(function (Member $m) {
                $this->assertNull($m->getFamilyId());
                return $m;
            });

        $this->service->unlinkMember(5, 1);
    }

    public function testUnlinkMemberThrowsWhenMemberNotInFamily(): void {
        $member = $this->createMember(1, 'Max', 'Mustermann', 'aktiv', 'mitglied', 5);

        $this->memberMapper->method('findById')->with(1)->willReturn($member);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('gehoert nicht');

        $this->service->unlinkMember(99, 1);
    }

    // ── getActiveChildrenCount() ────────────────────────────────────

    public function testGetActiveChildrenCountReturnsCorrectCount(): void {
        $activeMitglied1 = $this->createMember(1, 'Max', 'M', 'aktiv', 'mitglied', 1);
        $activeMitglied2 = $this->createMember(2, 'Anna', 'M', 'aktiv', 'mitglied', 1);
        $inactiveMitglied = $this->createMember(3, 'Peter', 'M', 'inaktiv', 'mitglied', 1);
        $parent = $this->createMember(4, 'Hans', 'M', 'aktiv', 'erziehungsberechtigter', 1);

        $this->memberMapper->method('findByFamily')
            ->with(1)
            ->willReturn([$activeMitglied1, $activeMitglied2, $inactiveMitglied, $parent]);

        $count = $this->service->getActiveChildrenCount(1);

        $this->assertSame(2, $count);
    }

    public function testGetActiveChildrenCountReturnsZeroWhenNoChildren(): void {
        $parent = $this->createMember(1, 'Hans', 'M', 'aktiv', 'erziehungsberechtigter', 1);

        $this->memberMapper->method('findByFamily')->with(1)->willReturn([$parent]);

        $this->assertSame(0, $this->service->getActiveChildrenCount(1));
    }

    public function testGetActiveChildrenCountReturnsZeroForEmptyFamily(): void {
        $this->memberMapper->method('findByFamily')->with(1)->willReturn([]);

        $this->assertSame(0, $this->service->getActiveChildrenCount(1));
    }

    // ── countAll() ──────────────────────────────────────────────────

    public function testCountAllDelegatesToMapper(): void {
        $this->familyMapper->method('countAll')->willReturn(5);

        $this->assertSame(5, $this->service->countAll());
    }
}
