<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Integration;

use OCA\Mitgliederverwaltung\Controller\MemberController;
use OCA\Mitgliederverwaltung\Db\Address;
use OCA\Mitgliederverwaltung\Db\Email;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Service\DuplicateMemberException;
use OCA\Mitgliederverwaltung\Service\MemberService;
use OCA\Mitgliederverwaltung\Service\PermissionService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCA\Mitgliederverwaltung\Tests\TestInputStream;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Integration-level tests for MemberController API endpoints.
 *
 * Tests HTTP status codes, response structure, permission enforcement,
 * and error handling for all member CRUD operations.
 *
 * Part of Issue #65.
 */
class MemberControllerTest extends TestCase {

    private MemberController $controller;
    private MemberService&MockObject $memberService;
    private PermissionService&MockObject $permissionService;
    private IRequest&MockObject $request;
    private IUserSession&MockObject $userSession;
    private LoggerInterface&MockObject $logger;

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

        $this->memberService = $this->createMock(MemberService::class);
        $this->permissionService = $this->createMock(PermissionService::class);
        $this->request = $this->createMock(IRequest::class);
        $this->userSession = $this->createMock(IUserSession::class);
        $this->logger = $this->createMock(LoggerInterface::class);

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

        $this->controller = new MemberController(
            'mitgliederverwaltung',
            $this->request,
            $this->memberService,
            $this->permissionService,
            $this->userSession,
            $this->logger
        );
    }

    protected function tearDown(): void {
        TestInputStream::unregister();
        parent::tearDown();
    }

    private function setRequestBody(array $data): void {
        TestInputStream::register();
        TestInputStream::setData(json_encode($data));
    }

    private function makeAddress(): Address {
        $addr = new Address();
        $addr->setId(1);
        $addr->setMemberId(1);
        $addr->setStrasse('Hauptstr. 1');
        $addr->setPlz('12345');
        $addr->setOrt('Berlin');
        return $addr;
    }

    private function makePhone(): Phone {
        $phone = new Phone();
        $phone->setId(1);
        $phone->setMemberId(1);
        $phone->setNumberE164('+491701234567');
        return $phone;
    }

    private function makeEmail(): Email {
        $email = new Email();
        $email->setId(1);
        $email->setMemberId(1);
        $email->setEmail('max@example.com');
        return $email;
    }

    // ── GET /api/v1/members/search ──────────────────────────────────

    public function testSearchReturnsEmptyForBlankQuery(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'q' => '',
                    'limit' => '20',
                    default => $default,
                };
            });

        $response = $this->controller->search();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertEmpty($response->getData()['data']);
    }

    public function testSearchReturnsResultsForValidQuery(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'q' => 'Max',
                    'limit' => '20',
                    default => $default,
                };
            });
        $this->memberService->method('fullTextSearch')
            ->willReturn([['id' => 1, 'vorname' => 'Max']]);

        $response = $this->controller->search();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testSearchReturns500OnException(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'q' => 'Max',
                    'limit' => '20',
                    default => $default,
                };
            });
        $this->memberService->method('fullTextSearch')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->search();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── GET /api/v1/members (index) ─────────────────────────────────

    public function testIndexReturns200WithMemberList(): void {
        $members = [
            ['id' => 1, 'vorname' => 'Max', 'nachname' => 'Mustermann'],
            ['id' => 2, 'vorname' => 'Anna', 'nachname' => 'Schmidt'],
        ];

        $this->request->method('getParam')->willReturn(null);
        $this->memberService->method('findAll')->willReturn($members);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $data = $response->getData();
        $this->assertArrayHasKey('data', $data);
        $this->assertArrayHasKey('total', $data);
        $this->assertCount(2, $data['data']);
        $this->assertSame(2, $data['total']);
    }

    public function testIndexSearchReturnsFilteredResults(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'search' => 'Mueller',
                    default => $default,
                };
            });

        $this->memberService->method('search')
            ->with('Mueller')
            ->willReturn([['id' => 1, 'nachname' => 'Mueller']]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $data = $response->getData();
        $this->assertCount(1, $data['data']);
    }

    public function testIndexFilterBirthdayThisMonthLegacy(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'filter' => 'birthday_this_month',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, null, true, false)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testIndexFilterBirthdayThisMonthNewParam(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'birthday_this_month' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, null, true, false)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testIndexFilterUnpaidFeesLegacy(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'filter' => 'unpaid_fees',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, null, false, true)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testIndexFilterUnpaidFeesNewParam(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'unpaid_fees' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, null, false, true)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testIndexFilterByStatus(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'status' => 'aktiv',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with('aktiv', null, false, false)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testIndexFilterByRolle(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'rolle' => 'mitglied',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, 'mitglied', false, false)
            ->willReturn([['id' => 1], ['id' => 2]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(2, $response->getData()['data']);
    }

    public function testIndexFilterByRolleErziehungsberechtigter(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'rolle' => 'erziehungsberechtigter',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with(null, 'erziehungsberechtigter', false, false)
            ->willReturn([['id' => 3]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testIndexCombinedStatusAndBirthday(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'status' => 'aktiv',
                    'birthday_this_month' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with('aktiv', null, true, false)
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testIndexCombinedStatusRolleAndUnpaid(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'status' => 'aktiv',
                    'rolle' => 'mitglied',
                    'unpaid_fees' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with('aktiv', 'mitglied', false, true)
            ->willReturn([['id' => 5]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(1, $response->getData()['data']);
    }

    public function testIndexCombinedAllFilters(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'status' => 'inaktiv',
                    'rolle' => 'erziehungsberechtigter',
                    'birthday_this_month' => '1',
                    'unpaid_fees' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->with('inaktiv', 'erziehungsberechtigter', true, true)
            ->willReturn([]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertCount(0, $response->getData()['data']);
        $this->assertSame(0, $response->getData()['total']);
    }

    public function testIndexTotalReflectsFilteredCount(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'status' => 'aktiv',
                    default => $default,
                };
            });
        $this->memberService->method('findFiltered')
            ->willReturn([['id' => 1], ['id' => 2], ['id' => 3]]);

        $response = $this->controller->index();

        $this->assertSame(3, $response->getData()['total']);
    }

    public function testIndexFilterByFamilyId(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'familyId' => '1',
                    default => $default,
                };
            });
        $this->memberService->method('findByFamily')
            ->willReturn([['id' => 1]]);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testIndexReturns500OnException(): void {
        $this->request->method('getParam')->willReturn(null);
        $this->memberService->method('findAll')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── GET /api/v1/members/{id} (show) ─────────────────────────────

    public function testShowReturns200ForExistingMember(): void {
        $member = ['id' => 1, 'vorname' => 'Max', 'nachname' => 'Mustermann'];
        $this->memberService->method('find')->with(1)->willReturn($member);

        $response = $this->controller->show(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertSame(1, $response->getData()['id']);
    }

    public function testShowReturns404ForMissingMember(): void {
        $this->memberService->method('find')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->show(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
        $this->assertArrayHasKey('error', $response->getData());
    }

    public function testShowReturns500OnException(): void {
        $this->memberService->method('find')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->show(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── POST /api/v1/members (create) ───────────────────────────────

    public function testCreateReturns201OnSuccess(): void {
        $memberData = ['vorname' => 'Max', 'nachname' => 'Mustermann', 'geburtsdatum' => '1990-06-15'];
        $this->setRequestBody($memberData);
        $this->memberService->method('create')
            ->willReturn(['id' => 1, ...$memberData]);

        $response = $this->controller->create();

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreateWithSubEntities(): void {
        $memberData = [
            'vorname' => 'Max',
            'nachname' => 'Mustermann',
            'addresses' => [['strasse' => 'Hauptstr. 1']],
            'phones' => [['numberE164' => '+491701234567']],
            'emails' => [['email' => 'max@example.com']],
        ];
        $this->setRequestBody($memberData);
        $this->memberService->method('create')
            ->willReturn(['id' => 1, 'vorname' => 'Max']);

        $response = $this->controller->create();

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreateReturns400OnValidationError(): void {
        $this->setRequestBody(['vorname' => '']);
        $this->memberService->method('create')
            ->willThrowException(new ValidationException('Missing required fields: vorname'));

        $response = $this->controller->create();

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
        $this->assertStringContainsString('vorname', $response->getData()['error']);
    }

    public function testCreateReturns409OnDuplicate(): void {
        $this->setRequestBody(['vorname' => 'Max', 'nachname' => 'Mustermann']);
        $this->memberService->method('create')
            ->willThrowException(new DuplicateMemberException('Duplicate member ID: 42'));

        $response = $this->controller->create();

        $this->assertSame(Http::STATUS_CONFLICT, $response->getStatus());
        $this->assertArrayHasKey('error', $response->getData());
    }

    public function testCreateReturns500OnException(): void {
        $this->setRequestBody(['vorname' => 'Max']);
        $this->memberService->method('create')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->create();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── PUT /api/v1/members/{id} (update) ───────────────────────────

    public function testUpdateReturns200OnSuccess(): void {
        $this->setRequestBody(['vorname' => 'Maximilian']);
        $this->memberService->method('update')
            ->willReturn(['id' => 1, 'vorname' => 'Maximilian']);

        $response = $this->controller->update(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testUpdateReturns404ForMissingMember(): void {
        $this->setRequestBody(['vorname' => 'Max']);
        $this->memberService->method('update')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->update(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testUpdateReturns400OnValidationError(): void {
        $this->setRequestBody(['geburtsdatum' => '2099-01-01']);
        $this->memberService->method('update')
            ->willThrowException(new ValidationException('Geburtsdatum in Zukunft'));

        $response = $this->controller->update(1);

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testUpdateReturns500OnException(): void {
        $this->setRequestBody(['vorname' => 'Max']);
        $this->memberService->method('update')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->update(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── DELETE /api/v1/members/{id} (destroy) ───────────────────────

    public function testDestroyReturns200OnSuccess(): void {
        $this->memberService->expects($this->once())->method('delete')->with(1);

        $response = $this->controller->destroy(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertSame('deleted', $response->getData()['status']);
    }

    public function testDestroyReturns404ForMissingMember(): void {
        $this->memberService->method('delete')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->destroy(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testDestroyReturns500OnException(): void {
        $this->memberService->method('delete')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->destroy(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── Address sub-entity endpoints ────────────────────────────────

    public function testCreateAddressReturns201OnSuccess(): void {
        $this->setRequestBody(['strasse' => 'Hauptstr. 1', 'plz' => '12345', 'ort' => 'Berlin']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addAddress')->willReturn($this->makeAddress());

        $response = $this->controller->createAddress(1);

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreateAddressReturns404ForMissingMember(): void {
        $this->memberService->method('find')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->createAddress(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testCreateAddressReturns500OnException(): void {
        $this->setRequestBody(['strasse' => 'Hauptstr. 1']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addAddress')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->createAddress(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testUpdateAddressReturns200OnSuccess(): void {
        $this->setRequestBody(['strasse' => 'Neue Str. 2']);
        $this->memberService->method('updateAddress')->willReturn($this->makeAddress());

        $response = $this->controller->updateAddress(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testUpdateAddressReturns404WhenNotFound(): void {
        $this->setRequestBody(['strasse' => 'Neue Str. 2']);
        $this->memberService->method('updateAddress')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->updateAddress(1, 999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testUpdateAddressReturns500OnException(): void {
        $this->setRequestBody(['strasse' => 'Neue Str. 2']);
        $this->memberService->method('updateAddress')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->updateAddress(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testDestroyAddressReturns200OnSuccess(): void {
        $this->memberService->expects($this->once())->method('deleteAddress')->with(1);

        $response = $this->controller->destroyAddress(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testDestroyAddressReturns404ForMissingAddress(): void {
        $this->memberService->method('deleteAddress')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->destroyAddress(1, 9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testDestroyAddressReturns500OnException(): void {
        $this->memberService->method('deleteAddress')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->destroyAddress(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── Phone sub-entity endpoints ──────────────────────────────────

    public function testCreatePhoneReturns201OnSuccess(): void {
        $this->setRequestBody(['numberE164' => '+491701234567']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addPhone')->willReturn($this->makePhone());

        $response = $this->controller->createPhone(1);

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreatePhoneReturns404ForMissingMember(): void {
        $this->memberService->method('find')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->createPhone(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testCreatePhoneReturns500OnException(): void {
        $this->setRequestBody(['numberE164' => '+491701234567']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addPhone')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->createPhone(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testUpdatePhoneReturns200OnSuccess(): void {
        $this->setRequestBody(['numberE164' => '+491709999999']);
        $this->memberService->method('updatePhone')->willReturn($this->makePhone());

        $response = $this->controller->updatePhone(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testUpdatePhoneReturns404WhenNotFound(): void {
        $this->setRequestBody(['numberE164' => '+491709999999']);
        $this->memberService->method('updatePhone')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->updatePhone(1, 999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testUpdatePhoneReturns500OnException(): void {
        $this->setRequestBody(['numberE164' => '+491709999999']);
        $this->memberService->method('updatePhone')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->updatePhone(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testDestroyPhoneReturns200OnSuccess(): void {
        $this->memberService->expects($this->once())->method('deletePhone')->with(1);

        $response = $this->controller->destroyPhone(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testDestroyPhoneReturns404WhenNotFound(): void {
        $this->memberService->method('deletePhone')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->destroyPhone(1, 999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testDestroyPhoneReturns500OnException(): void {
        $this->memberService->method('deletePhone')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->destroyPhone(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── Email sub-entity endpoints ──────────────────────────────────

    public function testCreateEmailReturns201OnSuccess(): void {
        $this->setRequestBody(['email' => 'max@example.com']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addEmail')->willReturn($this->makeEmail());

        $response = $this->controller->createEmail(1);

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreateEmailReturns404ForMissingMember(): void {
        $this->memberService->method('find')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->createEmail(9999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testCreateEmailReturns500OnException(): void {
        $this->setRequestBody(['email' => 'max@example.com']);
        $this->memberService->method('find')->willReturn(['id' => 1]);
        $this->memberService->method('addEmail')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->createEmail(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testUpdateEmailReturns200OnSuccess(): void {
        $this->setRequestBody(['email' => 'new@example.com']);
        $this->memberService->method('updateEmail')->willReturn($this->makeEmail());

        $response = $this->controller->updateEmail(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testUpdateEmailReturns404WhenNotFound(): void {
        $this->setRequestBody(['email' => 'new@example.com']);
        $this->memberService->method('updateEmail')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->updateEmail(1, 999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testUpdateEmailReturns500OnException(): void {
        $this->setRequestBody(['email' => 'new@example.com']);
        $this->memberService->method('updateEmail')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->updateEmail(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testDestroyEmailReturns200OnSuccess(): void {
        $this->memberService->expects($this->once())->method('deleteEmail')->with(1);

        $response = $this->controller->destroyEmail(1, 1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testDestroyEmailReturns404WhenNotFound(): void {
        $this->memberService->method('deleteEmail')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->destroyEmail(1, 999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testDestroyEmailReturns500OnException(): void {
        $this->memberService->method('deleteEmail')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->destroyEmail(1, 1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── Archive (admin-only) ────────────────────────────────────────

    public function testArchiveReturns403ForNonAdmin(): void {
        $this->permissionService->method('isAdmin')->willReturn(false);
        $this->request->method('getParam')->willReturn(null);

        $response = $this->controller->archive();

        $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus());
    }

    public function testArchiveReturns200ForAdmin(): void {
        $this->permissionService->method('isAdmin')->willReturn(true);
        $this->request->method('getParam')->willReturn(null);
        $this->memberService->method('findArchived')->willReturn([]);
        $this->memberService->method('countArchived')->willReturn(0);

        $response = $this->controller->archive();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $this->assertArrayHasKey('data', $response->getData());
    }

    public function testArchiveReturns500OnException(): void {
        $this->permissionService->method('isAdmin')->willReturn(true);
        $this->request->method('getParam')->willReturn(null);
        $this->memberService->method('findArchived')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->archive();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testArchiveWithNoUser(): void {
        // Reconfigure with null user
        $this->userSession = $this->createMock(IUserSession::class);
        $this->userSession->method('getUser')->willReturn(null);
        $this->controller = new MemberController(
            'mitgliederverwaltung',
            $this->request,
            $this->memberService,
            $this->permissionService,
            $this->userSession,
            $this->logger
        );
        $this->permissionService->method('isAdmin')->willReturn(false);
        $this->request->method('getParam')->willReturn(null);

        $response = $this->controller->archive();

        $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus());
    }

    // ── Security: SQL injection attempts ────────────────────────────

    public function testSearchWithSqlInjectionPayload(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'search' => "'; DROP TABLE oc_mv_members; --",
                    default => $default,
                };
            });

        // Service receives the raw string
        $this->memberService->method('search')->willReturn([]);
        $this->memberService->method('countAll')->willReturn(0);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    // ── Security: XSS payloads ──────────────────────────────────────

    public function testSearchWithXssPayload(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name, $default = null) {
                return match ($name) {
                    'search' => '<script>alert("xss")</script>',
                    default => $default,
                };
            });

        $this->memberService->method('search')->willReturn([]);
        $this->memberService->method('countAll')->willReturn(0);

        $response = $this->controller->index();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }
}
