<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\AuditLog;
use OCA\Mitgliederverwaltung\Db\AuditLogMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class AuditServiceTest extends TestCase {

    private AuditService $service;
    private AuditLogMapper&MockObject $mapper;
    private IUserSession&MockObject $userSession;

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

        $this->mapper = $this->createMock(AuditLogMapper::class);
        $this->userSession = $this->createMock(IUserSession::class);

        $user = $this->createMock(IUser::class);
        $user->method('getUID')->willReturn('testuser');
        $this->userSession->method('getUser')->willReturn($user);

        $this->service = new AuditService(
            $this->mapper,
            $this->userSession
        );
    }

    // ── logCreate() ─────────────────────────────────────────────────

    public function testLogCreateInsertsOneEntryPerNonNullField(): void {
        $entityData = [
            'id' => 1,
            'vorname' => 'Max',
            'nachname' => 'Mustermann',
            'notizen' => null,
        ];

        // id is skipped, null is skipped => 2 inserts: vorname, nachname
        $this->mapper->expects($this->exactly(2))->method('insert');

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateSetsCorrectEntityTypeAndId(): void {
        $entityData = ['vorname' => 'Max'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('member', $entry->getEntitaet());
                $this->assertSame(42, $entry->getEntitaetId());
                $this->assertSame('create', $entry->getAktion());
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 42);
    }

    public function testLogCreateSetsCorrectFieldValues(): void {
        $entityData = ['vorname' => 'Max'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('vorname', $entry->getFeld());
                $this->assertNull($entry->getAlterWert());
                $this->assertSame('Max', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateSkipsIdField(): void {
        $entityData = ['id' => 1];

        $this->mapper->expects($this->never())->method('insert');

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateSkipsNullValues(): void {
        $entityData = ['vorname' => null];

        $this->mapper->expects($this->never())->method('insert');

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateMasksEncryptedFields(): void {
        $entityData = ['ibanEncrypted' => 'DE89370400440532013000'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('[verschluesselt]', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logCreate($entityData, 'family', 1);
    }

    public function testLogCreateMasksKontoinhaberEncrypted(): void {
        $entityData = ['kontoinhaberEncrypted' => 'Max Mustermann'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('[verschluesselt]', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logCreate($entityData, 'family', 1);
    }

    public function testLogCreateMasksAllergienEncrypted(): void {
        $entityData = ['allergienEncrypted' => 'Nussallergie'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('[verschluesselt]', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateMasksSnakeCaseEncryptedFields(): void {
        $entityData = [
            'iban_encrypted' => 'DE89370400440532013000',
            'kontoinhaber_encrypted' => 'Max',
            'allergien_encrypted' => 'Nuss',
        ];

        $insertedEntries = [];
        $this->mapper->expects($this->exactly(3))
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) use (&$insertedEntries) {
                $insertedEntries[] = $entry;
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 1);

        foreach ($insertedEntries as $entry) {
            $this->assertSame('[verschluesselt]', $entry->getNeuerWert());
        }
    }

    public function testLogCreateSetsUserId(): void {
        $entityData = ['vorname' => 'Max'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('testuser', $entry->getNcUserId());
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateSetsTimestamp(): void {
        $entityData = ['vorname' => 'Max'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertNotEmpty($entry->getZeitpunkt());
                $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $entry->getZeitpunkt());
                return $entry;
            });

        $this->service->logCreate($entityData, 'member', 1);
    }

    public function testLogCreateUsesSystemUserWhenNoSession(): void {
        $this->userSession = $this->createMock(IUserSession::class);
        $this->userSession->method('getUser')->willReturn(null);

        $service = new AuditService($this->mapper, $this->userSession);

        $entityData = ['vorname' => 'Max'];
        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('system', $entry->getNcUserId());
                return $entry;
            });

        $service->logCreate($entityData, 'member', 1);
    }

    // ── logUpdate() ─────────────────────────────────────────────────

    public function testLogUpdateInsertsEntryOnlyForChangedFields(): void {
        $oldData = ['vorname' => 'Max', 'nachname' => 'Mustermann'];
        $newData = ['vorname' => 'Moritz', 'nachname' => 'Mustermann'];

        // Only vorname changed
        $this->mapper->expects($this->once())->method('insert');

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateStoresOldAndNewValues(): void {
        $oldData = ['vorname' => 'Max'];
        $newData = ['vorname' => 'Moritz'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('update', $entry->getAktion());
                $this->assertSame('vorname', $entry->getFeld());
                $this->assertSame('Max', $entry->getAlterWert());
                $this->assertSame('Moritz', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateSkipsUnchangedFields(): void {
        $oldData = ['vorname' => 'Max', 'nachname' => 'Mustermann'];
        $newData = ['vorname' => 'Max', 'nachname' => 'Mustermann'];

        $this->mapper->expects($this->never())->method('insert');

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateSkipsIdField(): void {
        $oldData = ['id' => 1, 'vorname' => 'Max'];
        $newData = ['id' => 1, 'vorname' => 'Max'];

        $this->mapper->expects($this->never())->method('insert');

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateMasksEncryptedFields(): void {
        $oldData = ['ibanEncrypted' => 'OLD_IBAN'];
        $newData = ['ibanEncrypted' => 'NEW_IBAN'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('[verschluesselt]', $entry->getAlterWert());
                $this->assertSame('[verschluesselt]', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateHandlesNullToValueTransition(): void {
        $oldData = ['notizen' => null];
        $newData = ['notizen' => 'Some notes'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertNull($entry->getAlterWert());
                $this->assertSame('Some notes', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateHandlesValueToNullTransition(): void {
        $oldData = ['notizen' => 'Some notes'];
        $newData = ['notizen' => null];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('Some notes', $entry->getAlterWert());
                $this->assertNull($entry->getNeuerWert());
                return $entry;
            });

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateHandlesNewFieldNotInOldData(): void {
        $oldData = [];
        $newData = ['vorname' => 'Max'];

        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertNull($entry->getAlterWert());
                $this->assertSame('Max', $entry->getNeuerWert());
                return $entry;
            });

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    public function testLogUpdateNormalizesIntegerAndStringComparison(): void {
        // Both represent the same value as strings
        $oldData = ['stufeId' => 1];
        $newData = ['stufeId' => 1];

        $this->mapper->expects($this->never())->method('insert');

        $this->service->logUpdate($oldData, $newData, 'member', 1);
    }

    // ── logDelete() ──────────────────────────────────────────────────

    public function testLogDeleteInsertsCorrectEntry(): void {
        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('delete', $entry->getAktion());
                $this->assertSame('member', $entry->getEntitaet());
                $this->assertSame(5, $entry->getEntitaetId());
                $this->assertSame('testuser', $entry->getNcUserId());
                return $entry;
            });

        $this->service->logDelete('member', 5);
    }

    public function testLogDeleteDoesNotSetFieldOrValues(): void {
        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                // logDelete does not set feld, alterWert, neuerWert
                $this->assertNull($entry->getFeld());
                $this->assertNull($entry->getAlterWert());
                $this->assertNull($entry->getNeuerWert());
                return $entry;
            });

        $this->service->logDelete('family', 3);
    }

    // ── logSoftDelete() ──────────────────────────────────────────────

    public function testLogSoftDeleteInsertsCorrectEntry(): void {
        $this->mapper->expects($this->once())
            ->method('insert')
            ->willReturnCallback(function (AuditLog $entry) {
                $this->assertSame('soft-delete', $entry->getAktion());
                $this->assertSame('member', $entry->getEntitaet());
                $this->assertSame(7, $entry->getEntitaetId());
                return $entry;
            });

        $this->service->logSoftDelete('member', 7);
    }

    // ── getLog() ─────────────────────────────────────────────────────

    public function testGetLogReturnsDataAndTotal(): void {
        $entry = new AuditLog();
        $entry->setZeitpunkt('2026-01-01 12:00:00');
        $entry->setNcUserId('testuser');
        $entry->setAktion('create');
        $entry->setEntitaet('member');
        $entry->setEntitaetId(1);

        $this->mapper->method('findFiltered')
            ->willReturn([$entry]);
        $this->mapper->method('countFiltered')
            ->willReturn(1);

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

        $this->assertArrayHasKey('data', $result);
        $this->assertArrayHasKey('total', $result);
        $this->assertCount(1, $result['data']);
        $this->assertSame(1, $result['total']);
    }

    public function testGetLogFiltersEntityType(): void {
        $this->mapper->expects($this->once())
            ->method('findFiltered')
            ->with('member', null, null, null, null, null, null)
            ->willReturn([]);

        $this->mapper->expects($this->once())
            ->method('countFiltered')
            ->with('member', null, null)
            ->willReturn(0);

        $result = $this->service->getLog('member');

        $this->assertSame([], $result['data']);
        $this->assertSame(0, $result['total']);
    }

    public function testGetLogFiltersEntityId(): void {
        $this->mapper->expects($this->once())
            ->method('findFiltered')
            ->with('member', 42, null, null, null, null, null)
            ->willReturn([]);

        $this->mapper->expects($this->once())
            ->method('countFiltered')
            ->with('member', 42, null)
            ->willReturn(0);

        $this->service->getLog('member', 42);
    }

    public function testGetLogFiltersUser(): void {
        $this->mapper->expects($this->once())
            ->method('findFiltered')
            ->with(null, null, 'admin', null, null, null, null)
            ->willReturn([]);

        $this->mapper->expects($this->once())
            ->method('countFiltered')
            ->with(null, null, 'admin')
            ->willReturn(0);

        $this->service->getLog(null, null, 'admin');
    }

    public function testGetLogFiltersDateRange(): void {
        $this->mapper->expects($this->once())
            ->method('findFiltered')
            ->with(null, null, null, '2026-01-01', '2026-12-31', null, null)
            ->willReturn([]);

        $this->service->getLog(null, null, null, '2026-01-01', '2026-12-31');
    }

    public function testGetLogPagination(): void {
        $this->mapper->expects($this->once())
            ->method('findFiltered')
            ->with(null, null, null, null, null, 10, 20)
            ->willReturn([]);

        $this->service->getLog(null, null, null, null, null, 10, 20);
    }

    public function testGetLogReturnsSerializedEntries(): void {
        $entry = new AuditLog();
        $entry->setZeitpunkt('2026-03-15 10:30:00');
        $entry->setNcUserId('admin');
        $entry->setAktion('update');
        $entry->setEntitaet('member');
        $entry->setEntitaetId(5);
        $entry->setFeld('vorname');
        $entry->setAlterWert('Max');
        $entry->setNeuerWert('Moritz');

        $this->mapper->method('findFiltered')->willReturn([$entry]);
        $this->mapper->method('countFiltered')->willReturn(1);

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

        $data = $result['data'][0];
        $this->assertSame('2026-03-15 10:30:00', $data['zeitpunkt']);
        $this->assertSame('admin', $data['ncUserId']);
        $this->assertSame('update', $data['aktion']);
        $this->assertSame('member', $data['entitaet']);
        $this->assertSame(5, $data['entitaetId']);
        $this->assertSame('vorname', $data['feld']);
        $this->assertSame('Max', $data['alterWert']);
        $this->assertSame('Moritz', $data['neuerWert']);
    }

    public function testGetLogEmptyResult(): void {
        $this->mapper->method('findFiltered')->willReturn([]);
        $this->mapper->method('countFiltered')->willReturn(0);

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

        $this->assertSame([], $result['data']);
        $this->assertSame(0, $result['total']);
    }
}
