<?php

namespace Tests\E2E\Services\Project;

use Tests\E2E\Client;
use Utopia\Database\Query;

trait PoliciesBase
{
    // =========================================================================
    // Get Policy
    // =========================================================================

    public function testGetPolicy(): void
    {
        $expectedFields = [
            'password-dictionary' => ['enabled'],
            'password-history' => ['total'],
            'password-personal-data' => ['enabled'],
            'session-alert' => ['enabled'],
            'session-duration' => ['duration'],
            'session-invalidation' => ['enabled'],
            'session-limit' => ['total'],
            'user-limit' => ['total'],
            'membership-privacy' => ['userId', 'userEmail', 'userPhone', 'userName', 'userMFA'],
        ];

        foreach ($expectedFields as $policyId => $fields) {
            $response = $this->getPolicy($policyId);

            $this->assertSame(200, $response['headers']['status-code']);
            $this->assertSame($policyId, $response['body']['$id']);

            foreach ($fields as $field) {
                $this->assertArrayHasKey($field, $response['body']);
            }
        }
    }

    public function testGetPolicyMatchesListPolicies(): void
    {
        $list = $this->listPolicies();

        $this->assertSame(200, $list['headers']['status-code']);

        $byId = [];
        foreach ($list['body']['policies'] as $policy) {
            $byId[$policy['$id']] = $policy;
        }

        foreach (\array_keys($byId) as $policyId) {
            $response = $this->getPolicy($policyId);

            $this->assertSame(200, $response['headers']['status-code']);
            $this->assertSame($byId[$policyId], $response['body']);
        }
    }

    public function testGetPolicyReflectsUpdates(): void
    {
        $this->updatePasswordDictionaryPolicy(true);
        $this->updatePasswordHistoryPolicy(5);
        $this->updateSessionDurationPolicy(3600);
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => false,
            'userName' => true,
            'userMFA' => true,
        ]);

        $passwordDictionary = $this->getPolicy('password-dictionary');
        $passwordHistory = $this->getPolicy('password-history');
        $sessionDuration = $this->getPolicy('session-duration');
        $membershipPrivacy = $this->getPolicy('membership-privacy');

        $this->assertSame(200, $passwordDictionary['headers']['status-code']);
        $this->assertSame(true, $passwordDictionary['body']['enabled']);

        $this->assertSame(200, $passwordHistory['headers']['status-code']);
        $this->assertSame(5, $passwordHistory['body']['total']);

        $this->assertSame(200, $sessionDuration['headers']['status-code']);
        $this->assertSame(3600, $sessionDuration['body']['duration']);

        $this->assertSame(200, $membershipPrivacy['headers']['status-code']);
        $this->assertSame(true, $membershipPrivacy['body']['userId']);
        $this->assertSame(true, $membershipPrivacy['body']['userEmail']);
        $this->assertSame(false, $membershipPrivacy['body']['userPhone']);
        $this->assertSame(true, $membershipPrivacy['body']['userName']);
        $this->assertSame(true, $membershipPrivacy['body']['userMFA']);

        // Cleanup
        $this->updatePasswordDictionaryPolicy(false);
        $this->updatePasswordHistoryPolicy(null);
        $this->updateSessionDurationPolicy(31536000);
        $this->updateMembershipPrivacyPolicy([
            'userId' => false,
            'userEmail' => false,
            'userPhone' => false,
            'userName' => false,
            'userMFA' => false,
        ]);
    }

    public function testGetPolicyWithoutAuthentication(): void
    {
        $response = $this->getPolicy('password-dictionary', authenticated: false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    public function testGetPolicyInvalidPolicyId(): void
    {
        $response = $this->getPolicy('invalid-policy');

        $this->assertSame(400, $response['headers']['status-code']);
    }

    // =========================================================================
    // List Policies
    // =========================================================================

    public function testListPolicies(): void
    {
        $response = $this->listPolicies();

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertArrayHasKey('policies', $response['body']);
        $this->assertArrayHasKey('total', $response['body']);
        $this->assertIsArray($response['body']['policies']);
        $this->assertIsInt($response['body']['total']);
        $this->assertSame(9, $response['body']['total']);
        $this->assertCount(9, $response['body']['policies']);

        $policyIds = \array_column($response['body']['policies'], '$id');

        $this->assertContains('password-dictionary', $policyIds);
        $this->assertContains('password-history', $policyIds);
        $this->assertContains('password-personal-data', $policyIds);
        $this->assertContains('session-alert', $policyIds);
        $this->assertContains('session-duration', $policyIds);
        $this->assertContains('session-invalidation', $policyIds);
        $this->assertContains('session-limit', $policyIds);
        $this->assertContains('user-limit', $policyIds);
        $this->assertContains('membership-privacy', $policyIds);
    }

    public function testListPoliciesResponseModel(): void
    {
        $response = $this->listPolicies();

        $this->assertSame(200, $response['headers']['status-code']);

        foreach ($response['body']['policies'] as $policy) {
            $this->assertArrayHasKey('$id', $policy);
        }

        $byId = [];
        foreach ($response['body']['policies'] as $policy) {
            $byId[$policy['$id']] = $policy;
        }

        $this->assertArrayHasKey('enabled', $byId['password-dictionary']);
        $this->assertArrayHasKey('total', $byId['password-history']);
        $this->assertArrayHasKey('enabled', $byId['password-personal-data']);
        $this->assertArrayHasKey('enabled', $byId['session-alert']);
        $this->assertArrayHasKey('duration', $byId['session-duration']);
        $this->assertArrayHasKey('enabled', $byId['session-invalidation']);
        $this->assertArrayHasKey('total', $byId['session-limit']);
        $this->assertArrayHasKey('total', $byId['user-limit']);
        $this->assertArrayHasKey('userId', $byId['membership-privacy']);
        $this->assertArrayHasKey('userEmail', $byId['membership-privacy']);
        $this->assertArrayHasKey('userPhone', $byId['membership-privacy']);
        $this->assertArrayHasKey('userName', $byId['membership-privacy']);
        $this->assertArrayHasKey('userMFA', $byId['membership-privacy']);
    }

    public function testListPoliciesReflectsUpdates(): void
    {
        $this->updatePasswordDictionaryPolicy(true);
        $this->updatePasswordHistoryPolicy(5);
        $this->updateSessionDurationPolicy(3600);
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => false,
            'userName' => true,
            'userMFA' => true,
        ]);

        $response = $this->listPolicies();

        $this->assertSame(200, $response['headers']['status-code']);

        $byId = [];
        foreach ($response['body']['policies'] as $policy) {
            $byId[$policy['$id']] = $policy;
        }

        $this->assertSame(true, $byId['password-dictionary']['enabled']);
        $this->assertSame(5, $byId['password-history']['total']);
        $this->assertSame(3600, $byId['session-duration']['duration']);
        $this->assertSame(true, $byId['membership-privacy']['userId']);
        $this->assertSame(true, $byId['membership-privacy']['userEmail']);
        $this->assertSame(false, $byId['membership-privacy']['userPhone']);
        $this->assertSame(true, $byId['membership-privacy']['userName']);
        $this->assertSame(true, $byId['membership-privacy']['userMFA']);

        // Cleanup
        $this->updatePasswordDictionaryPolicy(false);
        $this->updatePasswordHistoryPolicy(null);
        $this->updateSessionDurationPolicy(31536000);
        $this->updateMembershipPrivacyPolicy([
            'userId' => false,
            'userEmail' => false,
            'userPhone' => false,
            'userName' => false,
            'userMFA' => false,
        ]);
    }

    public function testListPoliciesTotalFalse(): void
    {
        $response = $this->listPolicies(total: false);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(0, $response['body']['total']);
        $this->assertCount(9, $response['body']['policies']);
    }

    public function testListPoliciesWithLimit(): void
    {
        $response = $this->listPolicies([
            Query::limit(1)->toString(),
        ]);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertCount(1, $response['body']['policies']);
        $this->assertSame(9, $response['body']['total']);
    }

    public function testListPoliciesWithOffset(): void
    {
        $listAll = $this->listPolicies();
        $this->assertSame(200, $listAll['headers']['status-code']);

        $listOffset = $this->listPolicies([
            Query::offset(1)->toString(),
        ]);

        $this->assertSame(200, $listOffset['headers']['status-code']);
        $this->assertCount(\count($listAll['body']['policies']) - 1, $listOffset['body']['policies']);
        $this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
    }

    public function testListPoliciesWithoutAuthentication(): void
    {
        $response = $this->listPolicies(authenticated: false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Password Dictionary Policy
    // =========================================================================

    public function testUpdatePasswordDictionaryPolicyEnable(): void
    {
        $response = $this->updatePasswordDictionaryPolicy(true);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(true, $response['body']['authPasswordDictionary']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(true, $project['body']['authPasswordDictionary']);

        // Cleanup
        $this->updatePasswordDictionaryPolicy(false);
    }

    public function testUpdatePasswordDictionaryPolicyDisable(): void
    {
        $this->updatePasswordDictionaryPolicy(true);

        $response = $this->updatePasswordDictionaryPolicy(false);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authPasswordDictionary']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(false, $project['body']['authPasswordDictionary']);
    }

    public function testUpdatePasswordDictionaryPolicyIdempotent(): void
    {
        $first = $this->updatePasswordDictionaryPolicy(true);
        $this->assertSame(200, $first['headers']['status-code']);
        $this->assertSame(true, $first['body']['authPasswordDictionary']);

        $second = $this->updatePasswordDictionaryPolicy(true);
        $this->assertSame(200, $second['headers']['status-code']);
        $this->assertSame(true, $second['body']['authPasswordDictionary']);

        // Cleanup
        $this->updatePasswordDictionaryPolicy(false);
    }

    public function testUpdatePasswordDictionaryPolicyWithoutAuth(): void
    {
        $response = $this->updatePasswordDictionaryPolicy(true, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    public function testUpdatePasswordDictionaryPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', $this->buildHeaders(), [
            'enabled' => 'not-a-boolean',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordDictionaryPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    // =========================================================================
    // Password History Policy
    // =========================================================================

    public function testUpdatePasswordHistoryPolicyEnable(): void
    {
        $response = $this->updatePasswordHistoryPolicy(5);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(5, $response['body']['authPasswordHistory']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(5, $project['body']['authPasswordHistory']);

        // Cleanup (disable by setting total to null which maps to 0)
        $this->updatePasswordHistoryPolicy(null);
    }

    public function testUpdatePasswordHistoryPolicyMin(): void
    {
        $response = $this->updatePasswordHistoryPolicy(1);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(1, $response['body']['authPasswordHistory']);

        // Cleanup
        $this->updatePasswordHistoryPolicy(null);
    }

    public function testUpdatePasswordHistoryPolicyMax(): void
    {
        $response = $this->updatePasswordHistoryPolicy(5000);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(5000, $response['body']['authPasswordHistory']);

        // Cleanup
        $this->updatePasswordHistoryPolicy(null);
    }

    public function testUpdatePasswordHistoryPolicyDisable(): void
    {
        $this->updatePasswordHistoryPolicy(5);

        $response = $this->updatePasswordHistoryPolicy(null);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(0, $response['body']['authPasswordHistory']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(0, $project['body']['authPasswordHistory']);
    }

    public function testUpdatePasswordHistoryPolicyBelowMin(): void
    {
        $response = $this->updatePasswordHistoryPolicy(0);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordHistoryPolicyAboveMax(): void
    {
        $response = $this->updatePasswordHistoryPolicy(5001);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordHistoryPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-history', $this->buildHeaders(), [
            'total' => 'not-a-number',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordHistoryPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-history', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordHistoryPolicyWithoutAuth(): void
    {
        $response = $this->updatePasswordHistoryPolicy(5, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Password Personal Data Policy
    // =========================================================================

    public function testUpdatePasswordPersonalDataPolicyEnable(): void
    {
        $response = $this->updatePasswordPersonalDataPolicy(true);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(true, $response['body']['authPersonalDataCheck']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(true, $project['body']['authPersonalDataCheck']);

        // Cleanup
        $this->updatePasswordPersonalDataPolicy(false);
    }

    public function testUpdatePasswordPersonalDataPolicyDisable(): void
    {
        $this->updatePasswordPersonalDataPolicy(true);

        $response = $this->updatePasswordPersonalDataPolicy(false);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authPersonalDataCheck']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(false, $project['body']['authPersonalDataCheck']);
    }

    public function testUpdatePasswordPersonalDataPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-personal-data', $this->buildHeaders(), [
            'enabled' => 'not-a-boolean',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordPersonalDataPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-personal-data', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdatePasswordPersonalDataPolicyWithoutAuth(): void
    {
        $response = $this->updatePasswordPersonalDataPolicy(true, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Session Alert Policy
    // =========================================================================

    public function testUpdateSessionAlertPolicyEnable(): void
    {
        $response = $this->updateSessionAlertPolicy(true);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(true, $response['body']['authSessionAlerts']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(true, $project['body']['authSessionAlerts']);

        // Cleanup
        $this->updateSessionAlertPolicy(false);
    }

    public function testUpdateSessionAlertPolicyDisable(): void
    {
        $this->updateSessionAlertPolicy(true);

        $response = $this->updateSessionAlertPolicy(false);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authSessionAlerts']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(false, $project['body']['authSessionAlerts']);
    }

    public function testUpdateSessionAlertPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-alert', $this->buildHeaders(), [
            'enabled' => 'not-a-boolean',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionAlertPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-alert', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionAlertPolicyWithoutAuth(): void
    {
        $response = $this->updateSessionAlertPolicy(true, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Session Duration Policy
    // =========================================================================

    public function testUpdateSessionDurationPolicy(): void
    {
        $response = $this->updateSessionDurationPolicy(3600);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(3600, $response['body']['authDuration']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(3600, $project['body']['authDuration']);

        // Cleanup (reset to default 1 year)
        $this->updateSessionDurationPolicy(31536000);
    }

    public function testUpdateSessionDurationPolicyMin(): void
    {
        $response = $this->updateSessionDurationPolicy(5);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(5, $response['body']['authDuration']);

        // Cleanup
        $this->updateSessionDurationPolicy(31536000);
    }

    public function testUpdateSessionDurationPolicyMax(): void
    {
        $response = $this->updateSessionDurationPolicy(31536000);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(31536000, $response['body']['authDuration']);
    }

    public function testUpdateSessionDurationPolicyBelowMin(): void
    {
        $response = $this->updateSessionDurationPolicy(4);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionDurationPolicyAboveMax(): void
    {
        $response = $this->updateSessionDurationPolicy(31536001);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionDurationPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-duration', $this->buildHeaders(), [
            'duration' => 'not-a-number',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionDurationPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-duration', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionDurationPolicyWithoutAuth(): void
    {
        $response = $this->updateSessionDurationPolicy(3600, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Session Invalidation Policy
    // =========================================================================

    public function testUpdateSessionInvalidationPolicyEnable(): void
    {
        $response = $this->updateSessionInvalidationPolicy(true);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(true, $response['body']['authInvalidateSessions']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(true, $project['body']['authInvalidateSessions']);

        // Cleanup
        $this->updateSessionInvalidationPolicy(false);
    }

    public function testUpdateSessionInvalidationPolicyDisable(): void
    {
        $this->updateSessionInvalidationPolicy(true);

        $response = $this->updateSessionInvalidationPolicy(false);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authInvalidateSessions']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(false, $project['body']['authInvalidateSessions']);
    }

    public function testUpdateSessionInvalidationPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-invalidation', $this->buildHeaders(), [
            'enabled' => 'not-a-boolean',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionInvalidationPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-invalidation', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionInvalidationPolicyWithoutAuth(): void
    {
        $response = $this->updateSessionInvalidationPolicy(true, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Session Limit Policy
    // =========================================================================

    public function testUpdateSessionLimitPolicy(): void
    {
        $response = $this->updateSessionLimitPolicy(5);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(5, $response['body']['authSessionsLimit']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(5, $project['body']['authSessionsLimit']);

        // Cleanup (reset to default)
        $this->updateSessionLimitPolicy(10);
    }

    public function testUpdateSessionLimitPolicyMin(): void
    {
        $response = $this->updateSessionLimitPolicy(1);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(1, $response['body']['authSessionsLimit']);

        // Cleanup
        $this->updateSessionLimitPolicy(10);
    }

    public function testUpdateSessionLimitPolicyMax(): void
    {
        $response = $this->updateSessionLimitPolicy(5000);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(5000, $response['body']['authSessionsLimit']);

        // Cleanup
        $this->updateSessionLimitPolicy(10);
    }

    public function testUpdateSessionLimitPolicyDisable(): void
    {
        $response = $this->updateSessionLimitPolicy(null);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(0, $response['body']['authSessionsLimit']);

        // Cleanup
        $this->updateSessionLimitPolicy(10);
    }

    public function testUpdateSessionLimitPolicyBelowMin(): void
    {
        $response = $this->updateSessionLimitPolicy(0);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionLimitPolicyAboveMax(): void
    {
        $response = $this->updateSessionLimitPolicy(5001);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionLimitPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-limit', $this->buildHeaders(), [
            'total' => 'not-a-number',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionLimitPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-limit', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateSessionLimitPolicyWithoutAuth(): void
    {
        $response = $this->updateSessionLimitPolicy(5, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // User Limit Policy
    // =========================================================================

    public function testUpdateUserLimitPolicy(): void
    {
        $response = $this->updateUserLimitPolicy(100);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(100, $response['body']['authLimit']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(100, $project['body']['authLimit']);

        // Cleanup
        $this->updateUserLimitPolicy(null);
    }

    public function testUpdateUserLimitPolicyMin(): void
    {
        $response = $this->updateUserLimitPolicy(1);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(1, $response['body']['authLimit']);

        // Cleanup
        $this->updateUserLimitPolicy(null);
    }

    public function testUpdateUserLimitPolicyMax(): void
    {
        $response = $this->updateUserLimitPolicy(5000);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(5000, $response['body']['authLimit']);

        // Cleanup
        $this->updateUserLimitPolicy(null);
    }

    public function testUpdateUserLimitPolicyDisable(): void
    {
        $this->updateUserLimitPolicy(100);

        $response = $this->updateUserLimitPolicy(null);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(0, $response['body']['authLimit']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(0, $project['body']['authLimit']);
    }

    public function testUpdateUserLimitPolicyBelowMin(): void
    {
        $response = $this->updateUserLimitPolicy(0);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateUserLimitPolicyAboveMax(): void
    {
        $response = $this->updateUserLimitPolicy(5001);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateUserLimitPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/user-limit', $this->buildHeaders(), [
            'total' => 'not-a-number',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateUserLimitPolicyMissingParam(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/user-limit', $this->buildHeaders(), []);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateUserLimitPolicyWithoutAuth(): void
    {
        $response = $this->updateUserLimitPolicy(100, false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Membership Privacy Policy
    // =========================================================================

    public function testUpdateMembershipPrivacyPolicyAllEnabled(): void
    {
        $response = $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertNotEmpty($response['body']['$id']);
        $this->assertSame(true, $response['body']['authMembershipsUserId']);
        $this->assertSame(true, $response['body']['authMembershipsUserEmail']);
        $this->assertSame(true, $response['body']['authMembershipsUserPhone']);
        $this->assertSame(true, $response['body']['authMembershipsUserName']);
        $this->assertSame(true, $response['body']['authMembershipsMfa']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(true, $project['body']['authMembershipsUserId']);
        $this->assertSame(true, $project['body']['authMembershipsUserEmail']);
        $this->assertSame(true, $project['body']['authMembershipsUserPhone']);
        $this->assertSame(true, $project['body']['authMembershipsUserName']);
        $this->assertSame(true, $project['body']['authMembershipsMfa']);
    }

    public function testUpdateMembershipPrivacyPolicyAllDisabled(): void
    {
        $response = $this->updateMembershipPrivacyPolicy([
            'userId' => false,
            'userEmail' => false,
            'userPhone' => false,
            'userName' => false,
            'userMFA' => false,
        ]);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authMembershipsUserId']);
        $this->assertSame(false, $response['body']['authMembershipsUserEmail']);
        $this->assertSame(false, $response['body']['authMembershipsUserPhone']);
        $this->assertSame(false, $response['body']['authMembershipsUserName']);
        $this->assertSame(false, $response['body']['authMembershipsMfa']);

        $project = $this->getProjectDocument();
        $this->assertSame(200, $project['headers']['status-code']);
        $this->assertSame(false, $project['body']['authMembershipsUserId']);
        $this->assertSame(false, $project['body']['authMembershipsUserEmail']);
        $this->assertSame(false, $project['body']['authMembershipsUserPhone']);
        $this->assertSame(false, $project['body']['authMembershipsUserName']);
        $this->assertSame(false, $project['body']['authMembershipsMfa']);

        // Cleanup (restore defaults)
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);
    }

    public function testUpdateMembershipPrivacyPolicyMixed(): void
    {
        $response = $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => false,
            'userPhone' => true,
            'userName' => false,
            'userMFA' => true,
        ]);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(true, $response['body']['authMembershipsUserId']);
        $this->assertSame(false, $response['body']['authMembershipsUserEmail']);
        $this->assertSame(true, $response['body']['authMembershipsUserPhone']);
        $this->assertSame(false, $response['body']['authMembershipsUserName']);
        $this->assertSame(true, $response['body']['authMembershipsMfa']);

        // Cleanup
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);
    }

    public function testUpdateMembershipPrivacyPolicyIndividualFields(): void
    {
        // Start from a known baseline where every field is enabled
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);

        $fields = [
            'userId' => 'authMembershipsUserId',
            'userEmail' => 'authMembershipsUserEmail',
            'userPhone' => 'authMembershipsUserPhone',
            'userName' => 'authMembershipsUserName',
            'userMFA' => 'authMembershipsMfa',
        ];

        // Each field can be toggled individually without clobbering the others
        foreach ($fields as $param => $attribute) {
            $response = $this->updateMembershipPrivacyPolicy([$param => false]);
            $this->assertSame(200, $response['headers']['status-code']);
            $this->assertSame(false, $response['body'][$attribute]);

            foreach ($fields as $otherParam => $otherAttribute) {
                if ($otherParam === $param) {
                    continue;
                }
                $this->assertSame(true, $response['body'][$otherAttribute], $otherAttribute . ' should be untouched while only ' . $param . ' was updated');
            }

            // Restore the field before the next iteration
            $restore = $this->updateMembershipPrivacyPolicy([$param => true]);
            $this->assertSame(200, $restore['headers']['status-code']);
            $this->assertSame(true, $restore['body'][$attribute]);
        }
    }

    public function testUpdateMembershipPrivacyPolicyMultipleFields(): void
    {
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);

        $response = $this->updateMembershipPrivacyPolicy([
            'userId' => false,
            'userPhone' => false,
        ]);

        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authMembershipsUserId']);
        $this->assertSame(false, $response['body']['authMembershipsUserPhone']);
        $this->assertSame(true, $response['body']['authMembershipsUserEmail']);
        $this->assertSame(true, $response['body']['authMembershipsUserName']);
        $this->assertSame(true, $response['body']['authMembershipsMfa']);

        // Cleanup
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);
    }

    public function testUpdateMembershipPrivacyPolicyEmptyBody(): void
    {
        // PATCH with no fields should be a no-op, leaving state unchanged
        $this->updateMembershipPrivacyPolicy([
            'userId' => false,
            'userEmail' => false,
            'userPhone' => false,
            'userName' => false,
            'userMFA' => false,
        ]);

        $response = $this->updateMembershipPrivacyPolicy([]);
        $this->assertSame(200, $response['headers']['status-code']);
        $this->assertSame(false, $response['body']['authMembershipsUserId']);
        $this->assertSame(false, $response['body']['authMembershipsUserEmail']);
        $this->assertSame(false, $response['body']['authMembershipsUserPhone']);
        $this->assertSame(false, $response['body']['authMembershipsUserName']);
        $this->assertSame(false, $response['body']['authMembershipsMfa']);

        // Cleanup
        $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ]);
    }

    public function testUpdateMembershipPrivacyPolicyInvalidType(): void
    {
        $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/membership-privacy', $this->buildHeaders(), [
            'userId' => 'not-a-boolean',
        ]);

        $this->assertSame(400, $response['headers']['status-code']);
    }

    public function testUpdateMembershipPrivacyPolicyWithoutAuth(): void
    {
        $response = $this->updateMembershipPrivacyPolicy([
            'userId' => true,
            'userEmail' => true,
            'userPhone' => true,
            'userName' => true,
            'userMFA' => true,
        ], false);

        $this->assertSame(401, $response['headers']['status-code']);
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    protected function buildHeaders(bool $authenticated = true): array
    {
        $headers = [
            'content-type' => 'application/json',
            'x-appwrite-project' => $this->getProject()['$id'],
        ];

        if ($authenticated) {
            $headers = array_merge($headers, $this->getHeaders(), [
                'x-appwrite-response-format' => '1.9.4',
            ]);
        }

        return $headers;
    }

    protected function getProjectDocument(): array
    {
        return $this->client->call(Client::METHOD_GET, '/projects/' . $this->getProject()['$id'], [
            'content-type' => 'application/json',
            'x-appwrite-project' => 'console',
            'cookie' => 'a_session_console=' . $this->getRoot()['session'],
            'x-appwrite-response-format' => '1.9.4',
        ]);
    }

    protected function listPolicies(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
    {
        $params = [];

        if ($queries !== null) {
            $params['queries'] = $queries;
        }

        if ($total !== null) {
            $params['total'] = $total;
        }

        return $this->client->call(Client::METHOD_GET, '/project/policies', $this->buildHeaders($authenticated), $params);
    }

    protected function getPolicy(string $policyId, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_GET, '/project/policies/' . $policyId, $this->buildHeaders($authenticated));
    }

    protected function updatePasswordDictionaryPolicy(bool $enabled, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', $this->buildHeaders($authenticated), [
            'enabled' => $enabled,
        ]);
    }

    protected function updatePasswordHistoryPolicy(?int $total, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/password-history', $this->buildHeaders($authenticated), [
            'total' => $total,
        ]);
    }

    protected function updatePasswordPersonalDataPolicy(bool $enabled, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/password-personal-data', $this->buildHeaders($authenticated), [
            'enabled' => $enabled,
        ]);
    }

    protected function updateSessionAlertPolicy(bool $enabled, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/session-alert', $this->buildHeaders($authenticated), [
            'enabled' => $enabled,
        ]);
    }

    protected function updateSessionDurationPolicy(int $duration, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/session-duration', $this->buildHeaders($authenticated), [
            'duration' => $duration,
        ]);
    }

    protected function updateSessionInvalidationPolicy(bool $enabled, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/session-invalidation', $this->buildHeaders($authenticated), [
            'enabled' => $enabled,
        ]);
    }

    protected function updateSessionLimitPolicy(?int $total, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/session-limit', $this->buildHeaders($authenticated), [
            'total' => $total,
        ]);
    }

    protected function updateUserLimitPolicy(?int $total, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/user-limit', $this->buildHeaders($authenticated), [
            'total' => $total,
        ]);
    }

    /**
     * @param array<string, bool> $params
     */
    protected function updateMembershipPrivacyPolicy(array $params, bool $authenticated = true): mixed
    {
        return $this->client->call(Client::METHOD_PATCH, '/project/policies/membership-privacy', $this->buildHeaders($authenticated), $params);
    }
}
