<?php

declare(strict_types=1);

use classes\limited_backoffice_service;

usesApiSuite();

function limited_backoffice_manager_session(array $departmentIds, array $extraPermissions = []): array
{
    $permissions = [
        limited_backoffice_service::PERMISSION_ACCESS,
        limited_backoffice_service::PERMISSION_MANAGE_PRICES,
        limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING,
        limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING,
        limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES,
    ];
    foreach ($departmentIds as $departmentId) {
        $permissions[] = 'department_access_' . (int)$departmentId;
    }

    return api_fixtures()->createUserSession(array_values(array_unique(array_merge($permissions, $extraPermissions))));
}

function limited_backoffice_all_role_permissions(): array
{
    return [
        'user',
        'permissions_list_own',
        'list_departments',
        'list_orders',
        'fetch_order',
        'add_order',
        'edit_order',
        'delete_order',
        'mark_order_as_completed',
        'list_order_items',
        'add_order_items',
        'edit_order_items',
        'delete_order_items',
        'list_order_attachments',
        'add_order_attachments',
        'download_order_attachments',
        'list_products',
        'list_categories',
        'list_department_categories',
        'list_department_order_recommended',
        'vehicle_product_suggestions',
        'search_customers',
        'get_user_from_customer_number',
        'list_customer_notes',
        'add_customer_note',
        'list_customer_attributes',
        'search_vehicles',
        'view_vehicle_status',
        'list_unknown_customer_vehicles',
        'list_vehicle_customer_suggestions',
        'department_license_plate_lookup',
        'department_vehicle_order_last_five',
        'list_number_plate_scans',
        'list_department_number_plate_scanners',
        'charge_order',
        'get_payment_intent',
        'confirm_payment_intent',
        'modules_stripe_department_terminal_readers_list',
        'modules_stripe_invoice_send',
        'list_bookings',
        'list_own_bookings',
        'edit_bookings',
        'add_booking',
        'add_bookings',
        'complete_bookings',
        'resend_booking_confirmations',
        'department_timebookings_entries_get',
        'department_timebookings_entries_post',
        'department_timebookings_entries_put',
        'list_department_daily_reports',
        'list_notifications',
        'list_own_notifications',
        'statistics_orders_new',
        'statistics_bookings_new',
        'limited_backoffice_customer_pricing_view',
        'limited_backoffice_customer_pricing_manage',
    ];
}

function limited_backoffice_role_preset_permissions(string $roleKey): array
{
    static $rolePresets = null;

    if ($rolePresets === null) {
        $reflection = new ReflectionClass(limited_backoffice_service::class);
        $constant = $reflection->getReflectionConstant('ROLE_PRESETS');

        if (!$constant instanceof ReflectionClassConstant) {
            throw new RuntimeException('Limited backoffice role presets are unavailable.');
        }

        $rolePresets = $constant->getValue();
    }

    return $rolePresets[$roleKey]['permissions'] ?? [];
}

function limited_backoffice_price_insert(int $departmentId, int $productId, int $price): void
{
    $statement = api_test_runtime()->db()->prepare(
        'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`)
         VALUES (?, ?, ?)
         ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)'
    );
    $statement->bind_param('iii', $departmentId, $productId, $price);
    $statement->execute();
    $statement->close();

    api_fixtures()->cleanupDeleteWhere('product_department_prices', [
        'department_id' => $departmentId,
        'product_id' => $productId,
    ]);
}

function limited_backoffice_price_value(int $departmentId, int $productId): ?int
{
    $row = api_test_runtime()->queryOne(
        'SELECT `price` FROM `product_department_prices` WHERE `department_id` = ' . $departmentId .
        ' AND `product_id` = ' . $productId . ' LIMIT 1'
    );

    return $row === null ? null : (int)$row['price'];
}

function limited_backoffice_price_rows(int $departmentId, int $productId): array
{
    return api_test_runtime()->db()->query(
        'SELECT `id`, `price` FROM `product_department_prices` WHERE `department_id` = ' . $departmentId .
        ' AND `product_id` = ' . $productId . ' ORDER BY `id` ASC'
    )->fetch_all(MYSQLI_ASSOC);
}

function limited_backoffice_cleanup_created_employee(int $employeeId): void
{
    $row = api_test_runtime()->queryOne(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
    );
    $groupId = (int)($row['managed_group_id'] ?? 0);

    api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => $employeeId]);
    api_fixtures()->cleanupDeleteWhere('tokens', ['user_id' => $employeeId]);
    api_fixtures()->cleanupDeleteById('users', $employeeId);
    if ($groupId > 0) {
        api_fixtures()->cleanupDeleteWhere('groups_permissions', ['group_id' => $groupId]);
        api_fixtures()->cleanupDeleteById('groups', $groupId);
    }
}

function limited_backoffice_without_users_deleted_at(callable $callback): void
{
    $db = api_test_runtime()->db();
    $column = $db->query("SHOW COLUMNS FROM `users` LIKE 'deleted_at'");
    if ($column === false) {
        throw new RuntimeException('Unable to inspect users.deleted_at test column.');
    }

    $hadColumn = (int)$column->num_rows > 0;
    if ($hadColumn) {
        $db->query('ALTER TABLE `users` DROP COLUMN `deleted_at`');
    }

    try {
        $callback();
    } finally {
        if ($hadColumn) {
            $db->query('ALTER TABLE `users` ADD COLUMN `deleted_at` DATETIME NULL');
        }
    }
}

function limited_backoffice_without_department_prices_updated_at(callable $callback): void
{
    $db = api_test_runtime()->db();
    $column = $db->query("SHOW COLUMNS FROM `product_department_prices` LIKE 'updated_at'");
    if ($column === false) {
        throw new RuntimeException('Unable to inspect product_department_prices.updated_at test column.');
    }

    $hadColumn = (int)$column->num_rows > 0;
    if ($hadColumn) {
        $db->query('ALTER TABLE `product_department_prices` DROP COLUMN `updated_at`');
    }

    try {
        $callback();
    } finally {
        if ($hadColumn) {
            $db->query(
                'ALTER TABLE `product_department_prices`
                 ADD COLUMN `updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
                 AFTER `created_at`'
            );
        }
    }
}

it('lists and updates explicit prices only for assigned departments', function (): void {
    api_test_covers('GET /limited-backoffice/departments', 'happy');
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'happy');
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'happy');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Prices Own']);
    $otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Prices Other']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Washes']);
    $product = api_fixtures()->createProduct([
        'name' => 'Limited Wash Product',
        'category' => $category['id'],
        'price' => 98765,
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    api_fixtures()->linkDepartmentCategory((int)$otherDepartment['id'], (int)$category['id']);
    limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 1234);
    limited_backoffice_price_insert((int)$otherDepartment['id'], (int)$product['id'], 4321);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $departments = api_client()->get('/limited-backoffice/departments', $session['headers']);
    $departments
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect(array_column($departments->data(), 'id'))
        ->toContain((int)$department['id'])
        ->not->toContain((int)$otherDepartment['id']);

    $prices = api_client()->get('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', $session['headers']);
    $prices
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect($prices->body)->not->toContain('98765');
    expect($prices->data()['categories'][0]['products'][0])
        ->toMatchArray([
            'id' => (int)$product['id'],
            'name' => 'Limited Wash Product',
            'price' => 1234,
        ]);

    $updated = api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'prices' => [
            ['product_id' => (int)$product['id'], 'price' => '2222'],
        ],
    ], $session['headers']);

    $updated
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect(limited_backoffice_price_value((int)$department['id'], (int)$product['id']))->toBe(2222);
    expect(limited_backoffice_price_value((int)$otherDepartment['id'], (int)$product['id']))->toBe(4321);
    expect($updated->data()['categories'][0]['products'][0]['price'] ?? null)->toBe(2222);
});

it('returns saved prices and collapses legacy duplicate department price rows', function (): void {
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'legacy duplicates');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Legacy Duplicate Prices']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Legacy Duplicate Category']);
    $product = api_fixtures()->createProduct([
        'name' => 'Legacy Duplicate Price',
        'category' => $category['id'],
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);

    $db = api_test_runtime()->db();
    $index = $db->query("SHOW INDEX FROM `product_department_prices` WHERE `Key_name` = 'uniq_product_department_prices_lookup'");
    if ($index === false) {
        throw new RuntimeException('Unable to inspect product_department_prices lookup index.');
    }
    $hadIndex = (int)$index->num_rows > 0;
    if ($hadIndex) {
        $db->query('ALTER TABLE `product_department_prices` DROP INDEX `uniq_product_department_prices_lookup`');
    }

    try {
        limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 111);
        limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 222);
        expect(limited_backoffice_price_rows((int)$department['id'], (int)$product['id']))->toHaveCount(2);

        $session = limited_backoffice_manager_session([(int)$department['id']]);
        $updated = api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
            'prices' => [
                ['product_id' => (int)$product['id'], 'price' => 333],
            ],
        ], $session['headers']);

        $updated
            ->assertStatus(200)
            ->assertEnvelope()
            ->assertSuccess();

        $rows = limited_backoffice_price_rows((int)$department['id'], (int)$product['id']);
        expect($rows)->toHaveCount(1);
        expect((int)$rows[0]['price'])->toBe(333);
        expect($updated->data()['categories'][0]['products'][0]['price'] ?? null)->toBe(333);
    } finally {
        $db->query(
            'DELETE FROM `product_department_prices` WHERE `department_id` = ' . (int)$department['id'] .
            ' AND `product_id` = ' . (int)$product['id']
        );
        if ($hadIndex) {
            $db->query(
                'ALTER TABLE `product_department_prices`
                 ADD UNIQUE KEY `uniq_product_department_prices_lookup` (`department_id`, `product_id`)'
            );
        }
    }
});

it('updates department prices when the price table has no updated_at column', function (): void {
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'schema compatibility');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Prices Legacy Schema']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Prices Legacy Category']);
    $products = [
        api_fixtures()->createProduct(['name' => 'Legacy Price One', 'category' => $category['id']]),
        api_fixtures()->createProduct(['name' => 'Legacy Price Two', 'category' => $category['id']]),
        api_fixtures()->createProduct(['name' => 'Legacy Price Three', 'category' => $category['id']]),
    ];
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);

    foreach ($products as $index => $product) {
        limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 100 + $index);
    }

    $session = limited_backoffice_manager_session([(int)$department['id']]);
    limited_backoffice_without_department_prices_updated_at(function () use ($department, $products, $session): void {
        $updated = api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
            'prices' => [
                ['product_id' => (int)$products[0]['id'], 'price' => '999999'],
                ['product_id' => (int)$products[1]['id'], 'price' => '999999'],
                ['product_id' => (int)$products[2]['id'], 'price' => '99999'],
            ],
        ], $session['headers']);

        $updated
            ->assertStatus(200)
            ->assertEnvelope()
            ->assertSuccess();

        expect(limited_backoffice_price_value((int)$department['id'], (int)$products[0]['id']))->toBe(999999);
        expect(limited_backoffice_price_value((int)$department['id'], (int)$products[1]['id']))->toBe(999999);
        expect(limited_backoffice_price_value((int)$department['id'], (int)$products[2]['id']))->toBe(99999);
    });
});

it('deduplicates products from duplicate department category links', function (): void {
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'dedupe');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Duplicate Products']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Duplicate Category']);
    $firstProduct = api_fixtures()->createProduct([
        'name' => 'Duplicate Price A',
        'category' => $category['id'],
    ]);
    $secondProduct = api_fixtures()->createProduct([
        'name' => 'Duplicate Price B',
        'category' => $category['id'],
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    limited_backoffice_price_insert((int)$department['id'], (int)$firstProduct['id'], 111);
    limited_backoffice_price_insert((int)$department['id'], (int)$secondProduct['id'], 222);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $response = api_client()->get('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', $session['headers']);
    $response
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $productIds = [];
    foreach ($response->data()['categories'] as $departmentCategory) {
        foreach ($departmentCategory['products'] as $departmentProduct) {
            $productIds[] = (int)$departmentProduct['id'];
        }
    }

    expect($productIds)->toBe([(int)$firstProduct['id'], (int)$secondProduct['id']]);
});

it('deduplicates missing product setup gaps from duplicate department category links', function (): void {
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'dedupe failure');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Duplicate Setup Gap']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Duplicate Setup Category']);
    $product = api_fixtures()->createProduct([
        'name' => 'Duplicate Missing Product',
        'category' => $category['id'],
        'price' => 88888,
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $response = api_client()->get('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', $session['headers']);
    $response
        ->assertStatus(409)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Department price setup is incomplete.');

    expect(array_column($response->data()['missing_products'], 'id'))->toBe([(int)$product['id']]);
    expect($response->body)->not->toContain('88888');
});

it('rejects cross-department price access, body spoofing, and outside products', function (): void {
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'auth');
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'auth');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Scope Own']);
    $otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Scope Other']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Scope Category']);
    $otherCategory = api_fixtures()->createCategory(['name' => 'Limited Scope Other Category']);
    $product = api_fixtures()->createProduct(['category' => $category['id']]);
    $outsideProduct = api_fixtures()->createProduct(['category' => $otherCategory['id']]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    api_fixtures()->linkDepartmentCategory((int)$otherDepartment['id'], (int)$otherCategory['id']);
    limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 100);
    limited_backoffice_price_insert((int)$otherDepartment['id'], (int)$outsideProduct['id'], 200);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    api_client()->get('/limited-backoffice/departments/' . (int)$otherDepartment['id'] . '/prices', $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$otherDepartment['id']]);

    api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'department_id' => (int)$otherDepartment['id'],
        'prices' => [
            ['product_id' => (int)$product['id'], 'price' => 111],
        ],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Department ID in body does not match the route.');

    api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'prices' => [
            ['product_id' => (int)$product['id'], 'price' => 111],
            ['product_id' => (int)$outsideProduct['id'], 'price' => 111],
        ],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Product is not available for this department.');

    expect(limited_backoffice_price_value((int)$department['id'], (int)$product['id']))->toBe(100);
    expect(limited_backoffice_price_value((int)$otherDepartment['id'], (int)$outsideProduct['id']))->toBe(200);
});

it('fails price setup gaps without exposing product defaults', function (): void {
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'failure');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Setup Gap']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Setup Gap Category']);
    $product = api_fixtures()->createProduct([
        'name' => 'Setup Gap Product',
        'category' => $category['id'],
        'price' => 87654,
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $response = api_client()->get('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', $session['headers']);
    $response
        ->assertStatus(409)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Department price setup is incomplete.');

    expect($response->body)->not->toContain('87654');
    expect($response->data()['missing_products'][0]['id'] ?? null)->toBe((int)$product['id']);
});

it('defaults missing custom-only department prices to sentinel without exposing fallback prices', function (): void {
    api_test_covers('GET /limited-backoffice/departments', 'happy');
    api_test_covers('GET /limited-backoffice/departments/{departmentId}/prices', 'happy');
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'happy');

    $department = api_fixtures()->createDepartment([
        'name' => 'Limited Custom Pricing Only',
        'custom_pricing_only' => 1,
    ]);
    $otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Other Pricing']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Custom Pricing Category']);
    $product = api_fixtures()->createProduct([
        'name' => 'Custom Missing Product',
        'category' => $category['id'],
        'price' => 87654,
    ]);
    $otherProduct = api_fixtures()->createProduct([
        'name' => 'Custom Missing Other Product',
        'category' => $category['id'],
        'price' => 76543,
    ]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    api_fixtures()->linkDepartmentCategory((int)$otherDepartment['id'], (int)$category['id']);
    limited_backoffice_price_insert((int)$otherDepartment['id'], (int)$product['id'], 4321);
    limited_backoffice_price_insert((int)$otherDepartment['id'], (int)$otherProduct['id'], 5432);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $departments = api_client()->get('/limited-backoffice/departments', $session['headers']);
    $departments
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
    expect($departments->data()[0]['custom_pricing_only'] ?? null)->toBeTrue();

    $response = api_client()->get('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', $session['headers']);
    $response
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect($response->body)->not->toContain('87654');
    expect($response->body)->not->toContain('76543');
    expect($response->body)->not->toContain('4321');
    expect($response->body)->not->toContain('5432');
    expect($response->data()['department']['custom_pricing_only'] ?? null)->toBeTrue();
    $products = [];
    foreach ($response->data()['categories'] as $departmentCategory) {
        foreach ($departmentCategory['products'] as $departmentProduct) {
            $products[(int)$departmentProduct['id']] = $departmentProduct;
        }
    }
    expect($products[(int)$product['id']]['price'] ?? null)->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE);
    expect($products[(int)$otherProduct['id']]['price'] ?? null)->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE);

    $updated = api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'prices' => [
            ['product_id' => (int)$product['id'], 'price' => 2222],
        ],
    ], $session['headers']);
    $updated
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $updatedProducts = [];
    foreach ($updated->data()['categories'] as $departmentCategory) {
        foreach ($departmentCategory['products'] as $departmentProduct) {
            $updatedProducts[(int)$departmentProduct['id']] = $departmentProduct;
        }
    }
    expect($updated->body)->not->toContain('87654');
    expect($updated->body)->not->toContain('76543');
    expect($updated->body)->not->toContain('4321');
    expect($updated->body)->not->toContain('5432');
    expect($updatedProducts[(int)$product['id']]['price'] ?? null)->toBe(2222);
    expect($updatedProducts[(int)$otherProduct['id']]['price'] ?? null)->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE);
});

it('rejects invalid price batches and leaves existing prices unchanged', function (): void {
    api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'validation');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Invalid Prices']);
    $category = api_fixtures()->createCategory(['name' => 'Limited Invalid Prices Category']);
    $firstProduct = api_fixtures()->createProduct(['category' => $category['id']]);
    $secondProduct = api_fixtures()->createProduct(['category' => $category['id']]);
    api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
    limited_backoffice_price_insert((int)$department['id'], (int)$firstProduct['id'], 100);
    limited_backoffice_price_insert((int)$department['id'], (int)$secondProduct['id'], 200);

    $session = limited_backoffice_manager_session([(int)$department['id']]);

    foreach ([null, '', 'abc', -1] as $invalidPrice) {
        api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
            'prices' => [
                ['product_id' => (int)$firstProduct['id'], 'price' => 999],
                ['product_id' => (int)$secondProduct['id'], 'price' => $invalidPrice],
            ],
        ], $session['headers'])
            ->assertStatus(400)
            ->assertEnvelope()
            ->assertSuccess(false);

        expect(limited_backoffice_price_value((int)$department['id'], (int)$firstProduct['id']))->toBe(100);
        expect(limited_backoffice_price_value((int)$department['id'], (int)$secondProduct['id']))->toBe(200);
    }

    api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'prices' => [
            ['product_id' => (int)$firstProduct['id'], 'price' => 999],
            ['product_id' => (int)$firstProduct['id'], 'price' => 888],
            ['product_id' => (int)$secondProduct['id'], 'price' => 777],
        ],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Duplicate product price rows are not allowed.');

    expect(limited_backoffice_price_value((int)$department['id'], (int)$firstProduct['id']))->toBe(100);
    expect(limited_backoffice_price_value((int)$department['id'], (int)$secondProduct['id']))->toBe(200);

    api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [
        'prices' => [
            ['product_id' => (int)$firstProduct['id'], 'price' => 999],
        ],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Price is required for every department product.');

    expect(limited_backoffice_price_value((int)$department['id'], (int)$firstProduct['id']))->toBe(100);
});

it('keeps higher limited backoffice roles as cashier permission supersets', function (): void {
    $cashierPermissions = limited_backoffice_role_preset_permissions('cashier');
    expect($cashierPermissions)->not->toBeEmpty();

    foreach (['operations_lead', 'department_admin'] as $roleKey) {
        $rolePermissions = limited_backoffice_role_preset_permissions($roleKey);
        $missingPermissions = array_values(array_diff($cashierPermissions, $rolePermissions));

        if ($missingPermissions !== []) {
            throw new RuntimeException(
                $roleKey . ' must include all cashier permissions; missing: ' . implode(', ', $missingPermissions)
            );
        }
    }
});

it('lets superusers migrate existing employee accounts to limited backoffice employees', function (): void {
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/migrate', 'happy');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Migration Department']);
    $legacyGroup = api_fixtures()->createGroup(['name' => 'Legacy Employee Group'], [
        'employee_public_data',
    ]);
    $legacyEmployee = api_fixtures()->createUser([
        'customer_number' => 0,
        'display_name' => 'Legacy Counter Employee',
        'email' => 'legacy-counter@example.test',
        'group_id' => $legacyGroup['id'],
    ]);
    $superuserSession = api_fixtures()->createUserSession([], ['group_id' => 1]);

    $departments = api_client()->get('/limited-backoffice/departments', $superuserSession['headers']);
    $departments
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
    expect(array_map('intval', array_column($departments->data(), 'id')))->toContain((int)$department['id']);

    $migrated = api_client()->post('/limited-backoffice/employees/' . (int)$legacyEmployee['id'] . '/migrate', [
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
    ], $superuserSession['headers']);

    $migrated
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    limited_backoffice_cleanup_created_employee((int)$legacyEmployee['id']);
    expect($migrated->data()['id'] ?? null)->toBe((int)$legacyEmployee['id']);
    expect($migrated->data()['customer_number'] ?? null)->toBe(0);
    expect($migrated->data()['display_name'] ?? null)->toBe('Legacy Counter Employee');
    expect($migrated->data()['email'] ?? null)->toBe('legacy-counter@example.test');
    expect($migrated->data()['role']['key'] ?? null)->toBe('cashier');
    expect(array_map('intval', array_column($migrated->data()['departments'] ?? [], 'id')))->toBe([(int)$department['id']]);

    $metadata = api_test_runtime()->queryOne(
        'SELECT `managed_group_id`, `role_key`, `department_ids`
         FROM `limited_backoffice_employees`
         WHERE `user_id` = ' . (int)$legacyEmployee['id'] . ' LIMIT 1'
    );
    expect($metadata)->not->toBeNull();
    $managedGroupId = (int)($metadata['managed_group_id'] ?? 0);
    expect($managedGroupId)->toBeGreaterThan(0);
    expect($managedGroupId)->not->toBe((int)$legacyGroup['id']);
    expect($metadata['role_key'] ?? null)->toBe('cashier');
    expect(json_decode((string)($metadata['department_ids'] ?? '[]'), true))->toBe([(int)$department['id']]);

    $userRow = api_test_runtime()->queryOne(
        'SELECT `customer_number`, `group_id`, `display_name`, `email`
         FROM `users`
         WHERE `id` = ' . (int)$legacyEmployee['id'] . ' LIMIT 1'
    );
    expect((int)($userRow['customer_number'] ?? -1))->toBe(0);
    expect((int)($userRow['group_id'] ?? 0))->toBe($managedGroupId);
    expect($userRow['display_name'] ?? null)->toBe('Legacy Counter Employee');
    expect($userRow['email'] ?? null)->toBe('legacy-counter@example.test');

    $permissionRows = api_test_runtime()->db()->query(
        'SELECT `permission` FROM `groups_permissions` WHERE `group_id` = ' . $managedGroupId
    )->fetch_all(MYSQLI_ASSOC);
    $permissions = array_column($permissionRows, 'permission');
    expect($permissions)
        ->toContain('admin')
        ->toContain('user')
        ->toContain('permissions_list_own')
        ->toContain('employee_public_data')
        ->toContain('department_access_' . (int)$department['id'])
        ->toContain('fetch_order')
        ->toContain('search_customers')
        ->toContain('add_order_attachments')
        ->toContain('list_number_plate_scans')
        ->toContain('add_bookings')
        ->not->toContain('superuser');

    $listed = api_client()->get('/limited-backoffice/employees', $superuserSession['headers']);
    $listedIds = array_map('intval', array_column($listed->data(), 'id'));
    expect($listedIds)->toContain((int)$legacyEmployee['id']);
});

it('rejects unsafe limited backoffice employee migrations', function (): void {
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/migrate', 'auth');
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/migrate', 'validation');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Migration Rejections']);
    $limitedManagerSession = limited_backoffice_manager_session([(int)$department['id']]);
    $superuserSession = api_fixtures()->createUserSession([], ['group_id' => 1]);
    $legacyEmployee = api_fixtures()->createUser([
        'customer_number' => 0,
        'display_name' => 'Unsafe Migration Employee',
        'email' => 'unsafe-migration-employee@example.test',
    ]);

    api_client()->post('/limited-backoffice/employees/' . (int)$legacyEmployee['id'] . '/migrate', [
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $limitedManagerSession['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['superuser']);

    api_client()->post('/limited-backoffice/employees/' . (int)$superuserSession['user']['id'] . '/migrate', [
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $superuserSession['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Managers cannot edit themselves.');

    $customerUser = api_fixtures()->createUser(['customer_number' => 99112233]);
    api_client()->post('/limited-backoffice/employees/' . (int)$customerUser['id'] . '/migrate', [
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $superuserSession['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Only employee accounts with customer number 0 can be migrated.');

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Already Managed Migration',
        'email' => 'already-managed-migration@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $limitedManagerSession['headers']);
    $alreadyManagedId = (int)($created->data()['id'] ?? 0);
    expect($alreadyManagedId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($alreadyManagedId);

    api_client()->post('/limited-backoffice/employees/' . $alreadyManagedId . '/migrate', [
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
    ], $superuserSession['headers'])
        ->assertStatus(409)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('User is already a limited backoffice employee.');

    $targetSuperuser = api_fixtures()->createUser([
        'customer_number' => 0,
        'email' => 'target-superuser-migration@example.test',
        'group_id' => 1,
    ]);
    api_client()->post('/limited-backoffice/employees/' . (int)$targetSuperuser['id'] . '/migrate', [
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $superuserSession['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot migrate superuser accounts.');
});

it('creates updates lists and deactivates scoped employees without exposing raw permissions', function (): void {
    limited_backoffice_without_users_deleted_at(function (): void {
    api_test_covers('GET /limited-backoffice/roles', 'happy');
    api_test_covers('GET /limited-backoffice/employees', 'happy');
    api_test_covers('POST /limited-backoffice/employees', 'happy');
    api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'happy');
    api_test_covers('DELETE /limited-backoffice/employees/{employeeId}', 'happy');

    $usersDeletedAtColumn = api_test_runtime()->db()->query("SHOW COLUMNS FROM `users` LIKE 'deleted_at'");
    expect($usersDeletedAtColumn)->not->toBeFalse();
    expect((int)$usersDeletedAtColumn->num_rows)->toBe(0);

    $department = api_fixtures()->createDepartment(['name' => 'Limited Employee Department']);
    $session = limited_backoffice_manager_session([(int)$department['id']], limited_backoffice_all_role_permissions());

    $roles = api_client()->get('/limited-backoffice/roles', $session['headers']);
    $roles
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect(array_column($roles->data(), 'key'))->toBe(['viewer', 'cashier', 'booking_coordinator', 'operations_lead', 'department_admin']);
    $rolesByKey = array_column($roles->data(), null, 'key');
    expect($rolesByKey['viewer']['label'] ?? null)->toBe('Deactivated');
    expect($rolesByKey['viewer']['permission_groups'] ?? null)->toBe([
        [
            'key' => 'account',
            'capabilities' => ['sign_in', 'view_own_permissions'],
        ],
    ]);
    $cashierGroups = array_column($rolesByKey['cashier']['permission_groups'] ?? [], 'capabilities', 'key');
    expect($cashierGroups['orders'] ?? null)->toBe([
        'view_orders',
        'create_orders',
        'edit_orders',
        'complete_orders',
        'view_order_items',
        'create_order_items',
        'update_order_lines',
        'remove_order_lines',
        'charge_orders',
    ]);
    expect($cashierGroups['products'] ?? null)->toBe([
        'view_product_catalog',
        'view_product_recommendations',
    ]);
    expect($cashierGroups['customers'] ?? null)->toBe([
        'search_customers',
        'view_customer_details',
        'view_customer_notes',
        'add_customer_notes',
        'view_customer_flags',
    ]);
    expect($cashierGroups['vehicles'] ?? null)->toBe([
        'search_vehicles',
        'view_vehicle_matches',
        'view_vehicle_history',
    ]);
    expect($cashierGroups['attachments'] ?? null)->toBe([
        'view_order_attachments',
        'add_order_attachments',
        'download_order_attachments',
    ]);
    expect($cashierGroups['scanner'] ?? null)->toBe([
        'view_plate_scans',
    ]);
    expect($cashierGroups['bookings'] ?? null)->toBe([
        'view_department_bookings',
        'view_own_bookings',
        'update_bookings',
        'create_bookings',
        'mark_bookings_complete',
        'send_booking_confirmations',
    ]);
    $departmentAdminGroups = array_column($rolesByKey['department_admin']['permission_groups'] ?? [], 'capabilities', 'key');
    expect($departmentAdminGroups['limited_backoffice'] ?? null)->toBe([
        'open_limited_backoffice',
        'manage_department_prices',
        'view_customer_pricing',
        'manage_customer_pricing',
        'manage_employee_access',
    ]);
    expect($roles->body)->not->toContain('department_access_');
    $rolePayload = $roles->data();
    $rolePayloadStrings = [];
    array_walk_recursive($rolePayload, static function ($value) use (&$rolePayloadStrings): void {
        if (is_string($value)) {
            $rolePayloadStrings[] = $value;
        }
    });
    foreach ([
        'list_orders',
        'list_departments',
        'fetch_order',
        'add_order',
        'edit_order',
        'delete_order',
        'mark_order_as_completed',
        'list_order_items',
        'add_order_items',
        'edit_order_items',
        'delete_order_items',
        'list_order_attachments',
        'list_products',
        'list_categories',
        'list_department_categories',
        'list_department_order_recommended',
        'vehicle_product_suggestions',
        'get_user_from_customer_number',
        'list_customer_notes',
        'add_customer_note',
        'list_customer_attributes',
        'view_vehicle_status',
        'list_unknown_customer_vehicles',
        'list_vehicle_customer_suggestions',
        'department_license_plate_lookup',
        'department_vehicle_order_last_five',
        'list_number_plate_scans',
        'list_department_number_plate_scanners',
        'charge_order',
        'get_payment_intent',
        'confirm_payment_intent',
        'modules_stripe_department_terminal_readers_list',
        'modules_stripe_invoice_send',
        'list_bookings',
        'list_own_bookings',
        'edit_bookings',
        'add_booking',
        'add_bookings',
        'complete_bookings',
        'resend_booking_confirmations',
        'department_timebookings_entries_get',
        'department_timebookings_entries_post',
        'department_timebookings_entries_put',
        'list_department_daily_reports',
        'list_notifications',
        'list_own_notifications',
        'statistics_orders_new',
        'statistics_bookings_new',
        'limited_backoffice_access',
        'limited_backoffice_prices_manage',
        'limited_backoffice_customer_pricing_view',
        'limited_backoffice_customer_pricing_manage',
        'limited_backoffice_employees_manage',
    ] as $rawPermission) {
        expect($rolePayloadStrings)->not->toContain($rawPermission);
    }

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited Cashier',
        'email' => 'limited-cashier@example.test',
        'phone_country_code' => 45,
        'phone' => 12345678,
        'password' => 'Secret123!',
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);

    $created
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);
    expect($created->data()['user_id'] ?? null)->toBe($employeeId);
    expect($created->data()['customer_number'] ?? null)->toBe(0);
    expect($created->data()['email'] ?? null)->toBe('limited-cashier@example.test');
    expect($created->data()['phone_country_code'] ?? null)->toBe(45);
    expect($created->data()['phone'] ?? null)->toBe(12345678);
    expect($created->body)->not->toContain('department_access_');
    expect($created->body)->not->toContain('permissions');

    $groupRow = api_test_runtime()->queryOne(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
    );
    $groupId = (int)($groupRow['managed_group_id'] ?? 0);
    $permissionRows = api_test_runtime()->db()->query(
        'SELECT `permission` FROM `groups_permissions` WHERE `group_id` = ' . $groupId
    )->fetch_all(MYSQLI_ASSOC);
    $permissions = array_column($permissionRows, 'permission');
    expect($permissions)
        ->toContain('admin')
        ->toContain('user')
        ->toContain('permissions_list_own')
        ->toContain('department_access_' . (int)$department['id'])
        ->toContain('employee_public_data')
        ->toContain('add_order')
        ->toContain('fetch_order')
        ->toContain('list_products')
        ->toContain('search_customers')
        ->toContain('get_user_from_customer_number')
        ->toContain('search_vehicles')
        ->toContain('list_order_attachments')
        ->toContain('add_order_attachments')
        ->toContain('download_order_attachments')
        ->toContain('list_number_plate_scans')
        ->toContain('modules_stripe_department_terminal_readers_list')
        ->toContain('list_bookings')
        ->toContain('edit_bookings')
        ->toContain('add_bookings')
        ->not->toContain('superuser');

    $employeeToken = api_fixtures()->createAuthToken($employeeId);
    $employeeSession = api_client()->get('/auth/session', api_fixtures()->bearerHeaders($employeeToken));
    $employeeSession
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
    expect($employeeSession->data()['permissions'] ?? [])
        ->toContain('admin')
        ->toContain('permissions_list_own')
        ->toContain('department_access_' . (int)$department['id'])
        ->toContain('add_order')
        ->toContain('list_products')
        ->toContain('search_customers')
        ->toContain('search_vehicles')
        ->toContain('add_order_attachments')
        ->toContain('list_bookings')
        ->toContain('edit_bookings')
        ->not->toContain('superuser');

    $publicEmployees = api_client()->get('/public/employees');
    $publicEmployeeIds = array_map('intval', array_column($publicEmployees->data(), 'id'));
    expect($publicEmployeeIds)->toContain($employeeId);

    $updated = api_client()->put('/limited-backoffice/employees/' . $employeeId, [
        'display_name' => 'Limited Lead',
        'email' => 'limited-lead@example.test',
        'phone_country_code' => 358,
        'phone' => 87654321,
        'role_key' => 'operations_lead',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);
    $updated
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect($updated->data()['display_name'] ?? null)->toBe('Limited Lead');
    expect($updated->data()['email'] ?? null)->toBe('limited-lead@example.test');
    expect($updated->data()['phone_country_code'] ?? null)->toBe(358);
    expect($updated->data()['phone'] ?? null)->toBe(87654321);
    expect($updated->data()['role']['key'] ?? null)->toBe('operations_lead');

    $list = api_client()->get('/limited-backoffice/employees', $session['headers']);
    $ids = array_column($list->data(), 'id');
    expect($ids)->toContain($employeeId);
    $listedEmployee = null;
    foreach ($list->data() as $employee) {
        if ((int)($employee['id'] ?? 0) === $employeeId) {
            $listedEmployee = $employee;
            break;
        }
    }
    expect($listedEmployee)->not->toBeNull();
    expect($listedEmployee['user_id'] ?? null)->toBe($employeeId);
    expect($listedEmployee['email'] ?? null)->toBe('limited-lead@example.test');
    expect($listedEmployee['phone_country_code'] ?? null)->toBe(358);
    expect($listedEmployee['phone'] ?? null)->toBe(87654321);
    expect($list->body)->not->toContain('department_access_');

    $deactivated = api_client()->delete('/limited-backoffice/employees/' . $employeeId, null, $session['headers']);
    $deactivated
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    expect($deactivated->data()['active'] ?? true)->toBeFalse();
    $userRow = api_test_runtime()->queryOne('SELECT `customer_number`, `password`, `group_id` FROM `users` WHERE `id` = ' . $employeeId);
    expect($userRow)->not->toBeNull();
    expect((int)($userRow['customer_number'] ?? -1))->toBe(0);
    expect(array_key_exists('password', $userRow ?? []))->toBeTrue();
    expect($userRow['password'])->toBeNull();
    expect((int)($userRow['group_id'] ?? -1))->toBe(0);
    $publicEmployeesAfterDeactivation = api_client()->get('/public/employees');
    $publicEmployeeIdsAfterDeactivation = array_map('intval', array_column($publicEmployeesAfterDeactivation->data(), 'id'));
    expect($publicEmployeeIdsAfterDeactivation)->not->toContain($employeeId);
    $employeeRow = api_test_runtime()->queryOne(
        'SELECT `deactivated_at` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
    );
    expect($employeeRow['deactivated_at'] ?? null)->not->toBeNull();
    });
});

it('caps manager-gated limited employee permissions while keeping baseline role permissions', function (): void {
    api_test_covers('POST /limited-backoffice/employees', 'auth');
    api_test_covers('GET /limited-backoffice/roles', 'auth');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Permission Cap']);
    $session = limited_backoffice_manager_session([(int)$department['id']], [
        'list_orders',
    ]);

    $roles = api_client()->get('/limited-backoffice/roles', $session['headers']);
    $rolesByKey = array_column($roles->data(), null, 'key');
    $operationsLeadGroups = array_column($rolesByKey['operations_lead']['permission_groups'] ?? [], 'capabilities', 'key');
    expect($operationsLeadGroups['account'] ?? null)->toBe(['sign_in', 'view_own_permissions']);
    expect($operationsLeadGroups['departments'] ?? null)->toBe(['view_departments', 'view_daily_reports']);
    expect($operationsLeadGroups['orders'] ?? null)->toBe(['view_orders']);
    expect($operationsLeadGroups['notifications'] ?? null)->toBe(['view_notifications']);
    expect($operationsLeadGroups['reports'] ?? null)->toBe(['view_order_statistics', 'view_booking_statistics']);
    expect($roles->body)->not->toContain('create_orders');

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited Capped Lead',
        'email' => 'limited-capped@example.test',
        'password' => 'Secret123!',
        'role_key' => 'operations_lead',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);
    $created
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $groupRow = api_test_runtime()->queryOne(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . $employeeId . ' LIMIT 1'
    );
    $groupId = (int)($groupRow['managed_group_id'] ?? 0);
    $permissionRows = api_test_runtime()->db()->query(
        'SELECT `permission` FROM `groups_permissions` WHERE `group_id` = ' . $groupId
    )->fetch_all(MYSQLI_ASSOC);
    $permissions = array_column($permissionRows, 'permission');

    expect($permissions)
        ->toContain('admin')
        ->toContain('user')
        ->toContain('permissions_list_own')
        ->toContain('employee_public_data')
        ->toContain('list_departments')
        ->toContain('list_orders')
        ->toContain('list_department_daily_reports')
        ->toContain('list_notifications')
        ->toContain('list_own_notifications')
        ->toContain('statistics_orders_new')
        ->toContain('statistics_bookings_new')
        ->toContain('department_access_' . (int)$department['id'])
        ->not->toContain('add_order')
        ->not->toContain('fetch_order')
        ->not->toContain('list_products')
        ->not->toContain('search_customers')
        ->not->toContain('add_bookings')
        ->not->toContain('delete_order')
        ->not->toContain(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
});

it('generates reusable QR login links for active scoped employees', function (): void {
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'happy');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Login Link Department']);
    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited QR Employee',
        'email' => 'limited-qr@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);
    $created
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $response = api_client()->post(
        '/limited-backoffice/employees/' . $employeeId . '/login-link',
        [],
        $session['headers']
    );
    $response
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $loginPath = (string)($response->data()['login_path'] ?? '');
    expect($response->data()['employee_id'] ?? null)->toBe($employeeId);
    expect($loginPath)->toMatch('/^\/login\/qr\?token=[a-f0-9]{64}$/');

    parse_str((string)parse_url($loginPath, PHP_URL_QUERY), $query);
    $token = (string)($query['token'] ?? '');
    expect($token)->toMatch('/^[a-f0-9]{64}$/');

    $tokenRow = api_test_runtime()->queryOne(
        "SELECT `user_id`, `type` FROM `tokens` WHERE `token` = '" .
        api_test_runtime()->db()->real_escape_string($token) .
        "' LIMIT 1"
    );
    expect($tokenRow)->not->toBeNull();
    expect((int)($tokenRow['user_id'] ?? 0))->toBe($employeeId);
    expect($tokenRow['type'] ?? null)->toBe('AUTH_TOKEN');

    $list = api_client()->get('/limited-backoffice/employees', $session['headers']);
    $list
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
    expect($list->body)->not->toContain($token);
    expect($list->body)->not->toContain('login_path');
});

it('rejects invalid limited backoffice employee QR login link generation', function (): void {
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'auth');
    api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'validation');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Login Link Own']);
    $otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Login Link Other']);
    $session = limited_backoffice_manager_session([(int)$department['id']]);
    $otherSession = limited_backoffice_manager_session([(int)$otherDepartment['id']]);

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited Link Target',
        'email' => 'limited-link-target@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);
    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $withoutManageEmployees = api_fixtures()->createUserSession([
        limited_backoffice_service::PERMISSION_ACCESS,
        'department_access_' . (int)$department['id'],
    ]);
    api_client()->post(
        '/limited-backoffice/employees/' . $employeeId . '/login-link',
        [],
        $withoutManageEmployees['headers']
    )
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions([limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES]);

    api_client()->post(
        '/limited-backoffice/employees/' . (int)$session['user']['id'] . '/login-link',
        [],
        $session['headers']
    )
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Managers cannot edit themselves.');

    api_client()->post('/limited-backoffice/employees/999999999/login-link', [], $session['headers'])
        ->assertStatus(404)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Managed employee not found.');

    $otherCreated = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited Other Department Target',
        'email' => 'limited-other-target@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$otherDepartment['id']],
    ], $otherSession['headers']);
    $otherEmployeeId = (int)($otherCreated->data()['id'] ?? 0);
    expect($otherEmployeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($otherEmployeeId);

    api_client()->post(
        '/limited-backoffice/employees/' . $otherEmployeeId . '/login-link',
        [],
        $session['headers']
    )
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$otherDepartment['id']]);

    api_client()->delete('/limited-backoffice/employees/' . $employeeId, null, $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    api_client()->post('/limited-backoffice/employees/' . $employeeId . '/login-link', [], $session['headers'])
        ->assertStatus(409)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot create a login link for an inactive employee.');

    $superuser = api_fixtures()->createUser(['group_id' => 1]);
    api_test_runtime()->db()->query(
        'INSERT INTO `limited_backoffice_employees`
            (`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
         VALUES (' . (int)$superuser['id'] . ", 1, 'department_admin', '[" . (int)$department['id'] . "]', " . (int)$session['user']['id'] . ')'
    );
    api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$superuser['id']]);

    api_client()->post('/limited-backoffice/employees/' . (int)$superuser['id'] . '/login-link', [], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot manage superuser accounts.');

    $sharedGroup = api_fixtures()->createGroup();
    $firstSharedUser = api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
    api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
    $departmentJson = '[' . (int)$department['id'] . ']';
    api_test_runtime()->db()->query(
        'INSERT INTO `limited_backoffice_employees`
            (`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
         VALUES (' . (int)$firstSharedUser['id'] . ', ' . (int)$sharedGroup['id'] . ", 'viewer', '" . $departmentJson . "', " . (int)$session['user']['id'] . ')'
    );
    api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$firstSharedUser['id']]);

    api_client()->post('/limited-backoffice/employees/' . (int)$firstSharedUser['id'] . '/login-link', [], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot manage shared groups.');
});

it('includes limited employees in the regular employee list and protects raw user edits', function (): void {
    api_test_covers('GET /users', 'limited backoffice employee list');
    api_test_covers('PUT /users', 'limited backoffice guard');
    api_test_covers('GET /superuser/user', 'limited backoffice managed flag');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Regular List']);
    $managerSession = limited_backoffice_manager_session([(int)$department['id']], [
        'permissions_list_own',
    ]);

    $regularEmployee = api_fixtures()->createUser([
        'customer_number' => 0,
        'display_name' => 'Regular Backoffice Employee',
    ], ['employee_public_data']);

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited Listed Employee',
        'email' => 'limited-listed@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $managerSession['headers']);
    $created
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);
    expect($created->data()['customer_number'] ?? null)->toBe(0);

    $adminSession = api_fixtures()->createUserSession([
        'get_user',
        'list_users',
        'edit_user',
    ]);

    $withoutLimited = api_client()->get('/users?page=1&limit=50&filters=customer_number:0', $adminSession['headers']);
    $withoutLimitedIds = array_map('intval', array_column($withoutLimited->data(), 'id'));
    expect($withoutLimitedIds)->toContain((int)$regularEmployee['id']);
    expect($withoutLimitedIds)->not->toContain($employeeId);

    $withLimited = api_client()->get(
        '/users?page=1&limit=50&filters=customer_number:0&include_limited_backoffice_employees=true',
        $adminSession['headers']
    );
    $usersById = array_column($withLimited->data(), null, 'id');
    expect(array_keys($usersById))->toContain((int)$regularEmployee['id']);
    expect(array_keys($usersById))->toContain($employeeId);
    expect($usersById[$employeeId]['limited_backoffice_managed'] ?? null)->toBeTrue();
    expect($usersById[(int)$regularEmployee['id']]['limited_backoffice_managed'] ?? null)->toBeFalse();

    $superuserDetail = api_client()->get('/superuser/user?user_id=' . $employeeId, $adminSession['headers']);
    $superuserDetail
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
    expect($superuserDetail->data()['limited_backoffice_managed'] ?? null)->toBeTrue();

    $userRow = api_test_runtime()->queryOne(
        'SELECT `customer_number`, `group_id` FROM `users` WHERE `id` = ' . $employeeId . ' LIMIT 1'
    );
    $customerNumber = (int)($userRow['customer_number'] ?? 0);
    $groupId = (int)($userRow['group_id'] ?? 0);
    expect($customerNumber)->toBe(0);

    api_client()->put('/users', [
        'id' => $employeeId,
        'customer_number' => $customerNumber + 1,
        'role' => $groupId,
        'display_name' => 'Blocked Customer Change',
    ], $adminSession['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Limited backoffice managed users cannot change customer number.');

    api_client()->put('/users', [
        'id' => $employeeId,
        'customer_number' => $customerNumber,
        'role' => 0,
        'display_name' => 'Blocked Role Change',
    ], $adminSession['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Limited backoffice managed users cannot change role.');

    api_client()->put('/users', [
        'id' => $employeeId,
        'customer_number' => $customerNumber,
        'role' => $groupId,
        'display_name' => 'Edited Limited Listed Employee',
    ], $adminSession['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $updatedUserRow = api_test_runtime()->queryOne(
        'SELECT `customer_number`, `group_id`, `display_name` FROM `users` WHERE `id` = ' . $employeeId . ' LIMIT 1'
    );
    expect((int)($updatedUserRow['customer_number'] ?? 0))->toBe($customerNumber);
    expect((int)($updatedUserRow['group_id'] ?? 0))->toBe($groupId);
    expect($updatedUserRow['display_name'] ?? null)->toBe('Edited Limited Listed Employee');
});

it('accepts employees without optional phone details', function (): void {
    api_test_covers('POST /limited-backoffice/employees', 'happy');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Employee No Phone']);
    $session = limited_backoffice_manager_session([(int)$department['id']]);

    $created = api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Limited No Phone',
        'email' => 'limited-no-phone@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ], $session['headers']);

    $created
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);
    expect($created->data()['customer_number'] ?? null)->toBe(0);
    expect(array_key_exists('phone_country_code', $created->data()))->toBeTrue();
    expect(array_key_exists('phone', $created->data()))->toBeTrue();
    expect($created->data()['phone_country_code'])->toBeNull();
    expect($created->data()['phone'])->toBeNull();
});

it('rejects employee scopes roles raw permissions self edits superusers and shared groups', function (): void {
    api_test_covers('POST /limited-backoffice/employees', 'validation');
    api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'validation');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Employee Own']);
    $otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Employee Other']);
    $session = limited_backoffice_manager_session([(int)$department['id']]);

    api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Outside Employee',
        'email' => 'outside@example.test',
        'password' => 'Secret123!',
        'role_key' => 'cashier',
        'department_ids' => [(int)$otherDepartment['id']],
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$otherDepartment['id']]);

    api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Raw Employee',
        'email' => 'raw@example.test',
        'password' => 'Secret123!',
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
        'permissions' => ['superuser'],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Raw permission and group assignment is not allowed.');

    api_client()->post('/limited-backoffice/employees', [
        'display_name' => 'Unknown Role Employee',
        'email' => 'unknown-role@example.test',
        'password' => 'Secret123!',
        'role_key' => 'superuser',
        'department_ids' => [(int)$department['id']],
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Unknown role.');

    api_client()->put('/limited-backoffice/employees/' . (int)$session['user']['id'], [
        'display_name' => 'Self Edit',
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Managers cannot edit themselves.');

    $superuser = api_fixtures()->createUser(['group_id' => 1]);
    api_test_runtime()->db()->query(
        'INSERT INTO `limited_backoffice_employees`
            (`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
         VALUES (' . (int)$superuser['id'] . ", 1, 'department_admin', '[" . (int)$department['id'] . "]', " . (int)$session['user']['id'] . ')'
    );
    api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$superuser['id']]);

    api_client()->put('/limited-backoffice/employees/' . (int)$superuser['id'], [
        'display_name' => 'Edited Superuser',
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot manage superuser accounts.');

    $sharedGroup = api_fixtures()->createGroup();
    $firstSharedUser = api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
    api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
    $departmentJson = '[' . (int)$department['id'] . ']';
    api_test_runtime()->db()->query(
        'INSERT INTO `limited_backoffice_employees`
            (`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
         VALUES (' . (int)$firstSharedUser['id'] . ', ' . (int)$sharedGroup['id'] . ", 'viewer', '" . $departmentJson . "', " . (int)$session['user']['id'] . ')'
    );
    api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$firstSharedUser['id']]);

    api_client()->put('/limited-backoffice/employees/' . (int)$firstSharedUser['id'], [
        'display_name' => 'Edited Shared Group',
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Cannot manage shared groups.');
});

it('rejects invalid limited backoffice employee contact details', function (): void {
    api_test_covers('POST /limited-backoffice/employees', 'validation');
    api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'validation');

    $department = api_fixtures()->createDepartment(['name' => 'Limited Employee Contact Validation']);
    $session = limited_backoffice_manager_session([(int)$department['id']]);
    $basePayload = [
        'display_name' => 'Contact Employee',
        'email' => 'contact@example.test',
        'password' => 'Secret123!',
        'role_key' => 'viewer',
        'department_ids' => [(int)$department['id']],
    ];

    api_client()->post('/limited-backoffice/employees', array_diff_key($basePayload, ['email' => true]), $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Email is required.');

    api_client()->post('/limited-backoffice/employees', [
        ...$basePayload,
        'email' => 'not-an-email',
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Email must be a valid email address.');

    api_client()->post('/limited-backoffice/employees', [
        ...$basePayload,
        'phone_country_code' => 45,
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Phone country code and phone number must be provided together.');

    api_client()->post('/limited-backoffice/employees', [
        ...$basePayload,
        'phone_country_code' => 1,
        'phone' => 12345678,
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Phone country code is not supported.');

    api_client()->post('/limited-backoffice/employees', [
        ...$basePayload,
        'phone_country_code' => 45,
        'phone' => '12ab',
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Phone values must contain digits only.');

    $created = api_client()->post('/limited-backoffice/employees', $basePayload, $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    $employeeId = (int)($created->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    api_client()->put('/limited-backoffice/employees/' . $employeeId, [
        'email' => '',
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Email is required.');

    api_client()->put('/limited-backoffice/employees/' . $employeeId, [
        'phone_country_code' => 45,
        'phone' => '123',
    ], $session['headers'])
        ->assertStatus(400)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMessage('Phone number must be 4-15 digits.');
});

it('includes list_departments in all active limited backoffice role presets', function (): void {
    foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
        $permissions = limited_backoffice_role_preset_permissions($roleKey);
        expect($permissions)->toContain('list_departments');
    }
});

it('includes list_department_daily_reports (dagsopgørelse) in all active limited backoffice role presets', function (): void {
    foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
        $permissions = limited_backoffice_role_preset_permissions($roleKey);
        expect($permissions)->toContain('list_department_daily_reports');
    }
});

it('includes list_notifications and list_own_notifications (Notifikationer) in all active limited backoffice role presets', function (): void {
    foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
        $permissions = limited_backoffice_role_preset_permissions($roleKey);
        expect($permissions)
            ->toContain('list_notifications')
            ->toContain('list_own_notifications');
    }
});

it('includes statistics_orders_new and statistics_bookings_new (Overblik) in all active limited backoffice role presets', function (): void {
    foreach (['cashier', 'booking_coordinator', 'operations_lead', 'department_admin'] as $roleKey) {
        $permissions = limited_backoffice_role_preset_permissions($roleKey);
        expect($permissions)
            ->toContain('statistics_orders_new')
            ->toContain('statistics_bookings_new');
    }
});

it('includes customer pricing permissions in the department admin limited backoffice role preset', function (): void {
    $permissions = limited_backoffice_role_preset_permissions('department_admin');
    expect($permissions)
        ->toContain(limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING)
        ->toContain(limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING);
});

it('assigns list_departments to managed employees and enforces list_departments permission on GET /departments', function (): void {
    api_test_covers('GET /departments', 'limited backoffice employee');

    $department = api_fixtures()->createDepartment(['name' => 'LB Dept List Test']);
    $manager = limited_backoffice_manager_session([(int)$department['id']]);

    $result = api_client()->post('/limited-backoffice/employees', [
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
        'display_name' => 'Dept List Test Employee',
        'email' => 'dept-list-test@example.test',
        'password' => 'Secret123!',
    ], $manager['headers']);

    $result->assertStatus(200)->assertEnvelope()->assertSuccess();
    $employeeId = (int)($result->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $stmt = api_test_runtime()->db()->prepare(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ? LIMIT 1'
    );
    $stmt->bind_param('i', $employeeId);
    $stmt->execute();
    $employeeRow = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    $managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
    expect($managedGroupId)->toBeGreaterThan(0);

    $permStmt = api_test_runtime()->db()->prepare(
        'SELECT 1 FROM `groups_permissions` WHERE `group_id` = ? AND `permission` = ? LIMIT 1'
    );
    $listDepts = 'list_departments';
    $permStmt->bind_param('is', $managedGroupId, $listDepts);
    $permStmt->execute();
    $permRow = $permStmt->get_result()->fetch_assoc();
    $permStmt->close();
    expect($permRow)->not->toBeNull('Managed employee must have list_departments permission');
});

it('assigns list_department_daily_reports to managed employees', function (): void {
    $department = api_fixtures()->createDepartment(['name' => 'LB Daily Report Perm Test']);
    $manager = limited_backoffice_manager_session([(int)$department['id']]);

    $result = api_client()->post('/limited-backoffice/employees', [
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
        'display_name' => 'Daily Report Perm Employee',
        'email' => 'daily-report-perm@example.test',
        'password' => 'Secret123!',
    ], $manager['headers']);

    $result->assertStatus(200)->assertEnvelope()->assertSuccess();
    $employeeId = (int)($result->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $stmt = api_test_runtime()->db()->prepare(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ? LIMIT 1'
    );
    $stmt->bind_param('i', $employeeId);
    $stmt->execute();
    $employeeRow = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    $managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
    expect($managedGroupId)->toBeGreaterThan(0);

    $permStmt = api_test_runtime()->db()->prepare(
        'SELECT 1 FROM `groups_permissions` WHERE `group_id` = ? AND `permission` = ? LIMIT 1'
    );
    $dailyReports = 'list_department_daily_reports';
    $permStmt->bind_param('is', $managedGroupId, $dailyReports);
    $permStmt->execute();
    $permRow = $permStmt->get_result()->fetch_assoc();
    $permStmt->close();
    expect($permRow)->not->toBeNull('Managed employee must have list_department_daily_reports permission');
});

it('assigns list_notifications and list_own_notifications to managed employees', function (): void {
    $department = api_fixtures()->createDepartment(['name' => 'LB Notifications Perm Test']);
    $manager = limited_backoffice_manager_session([(int)$department['id']]);

    $result = api_client()->post('/limited-backoffice/employees', [
        'role_key' => 'cashier',
        'department_ids' => [(int)$department['id']],
        'display_name' => 'Notifications Perm Employee',
        'email' => 'notifications-perm@example.test',
        'password' => 'Secret123!',
    ], $manager['headers']);

    $result->assertStatus(200)->assertEnvelope()->assertSuccess();
    $employeeId = (int)($result->data()['id'] ?? 0);
    expect($employeeId)->toBeGreaterThan(0);
    limited_backoffice_cleanup_created_employee($employeeId);

    $stmt = api_test_runtime()->db()->prepare(
        'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ? LIMIT 1'
    );
    $stmt->bind_param('i', $employeeId);
    $stmt->execute();
    $employeeRow = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    $managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
    expect($managedGroupId)->toBeGreaterThan(0);

    foreach (['list_notifications', 'list_own_notifications'] as $perm) {
        $stmt = api_test_runtime()->db()->prepare(
            'SELECT 1 FROM `groups_permissions` WHERE `group_id` = ? AND `permission` = ? LIMIT 1'
        );
        $stmt->bind_param('is', $managedGroupId, $perm);
        $stmt->execute();
        $permRow = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        expect($permRow)->not->toBeNull("Managed employee must have $perm permission");
    }
});

it('assigns statistics permissions (Overblik) to all active limited backoffice role presets including cashier and booking_coordinator', function (): void {
    $department = api_fixtures()->createDepartment(['name' => 'LB Stats Perm Test']);
    $manager = limited_backoffice_manager_session([(int)$department['id']]);

    foreach (['cashier', 'booking_coordinator'] as $roleKey) {
        $result = api_client()->post('/limited-backoffice/employees', [
            'role_key' => $roleKey,
            'department_ids' => [(int)$department['id']],
            'display_name' => 'Stats Perm ' . $roleKey,
            'email' => 'stats-perm-' . $roleKey . '@example.test',
            'password' => 'Secret123!',
        ], $manager['headers']);

        $result->assertStatus(200)->assertEnvelope()->assertSuccess();
        $employeeId = (int)($result->data()['id'] ?? 0);
        expect($employeeId)->toBeGreaterThan(0);
        limited_backoffice_cleanup_created_employee($employeeId);

        $stmtEmp = api_test_runtime()->db()->prepare(
            'SELECT `managed_group_id` FROM `limited_backoffice_employees` WHERE `user_id` = ? LIMIT 1'
        );
        $stmtEmp->bind_param('i', $employeeId);
        $stmtEmp->execute();
        $employeeRow = $stmtEmp->get_result()->fetch_assoc();
        $stmtEmp->close();
        $managedGroupId = (int)($employeeRow['managed_group_id'] ?? 0);
        expect($managedGroupId)->toBeGreaterThan(0);

        foreach (['statistics_orders_new', 'statistics_bookings_new'] as $perm) {
            $stmt = api_test_runtime()->db()->prepare(
                'SELECT 1 FROM `groups_permissions` WHERE `group_id` = ? AND `permission` = ? LIMIT 1'
            );
            $stmt->bind_param('is', $managedGroupId, $perm);
            $stmt->execute();
            $permRow = $stmt->get_result()->fetch_assoc();
            $stmt->close();
            expect($permRow)->not->toBeNull("$roleKey managed employee must have $perm permission");
        }
    }
});

it('enforces department access when creating an order via POST /orders', function (): void {
    api_test_covers('POST /orders', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Create Allowed Dept']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Create Denied Dept']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Create Customer']);

    $session = api_fixtures()->createUserSession([
        'add_order',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should succeed for accessible department
    $created = api_client()->post('/orders', [
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
        'reference' => 'DEPT-ACCESS-TEST',
        'notes' => '',
        'reg_1' => 'ABCD111',
    ], $session['headers']);
    $created->assertStatus(200)->assertEnvelope()->assertSuccess();

    // Should fail for inaccessible department
    api_client()->post('/orders', [
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
        'reference' => 'DEPT-ACCESS-DENIED',
        'notes' => '',
        'reg_1' => 'ABCD222',
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
});

it('enforces department access on the existing order when editing via PUT /order', function (): void {
    api_test_covers('PUT /order', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Edit Allowed Dept']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Edit Denied Dept']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Edit Customer']);

    $orderInAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderInDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);

    $session = api_fixtures()->createUserSession([
        'edit_order',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should succeed editing order in accessible department
    api_client()->put('/order', [
        'id' => (int)$orderInAllowed['id'],
        'notes' => 'updated',
    ], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();

    // Should fail editing order in inaccessible department
    api_client()->put('/order', [
        'id' => (int)$orderInDenied['id'],
        'notes' => 'should be denied',
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);
});

it('enforces department access when moving an order to a new department via PUT /order', function (): void {
    api_test_covers('PUT /order', 'department access move');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Move Allowed Dept']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Move Denied Dept']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Move Customer']);

    $order = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);

    $sessionBothDepts = api_fixtures()->createUserSession([
        'edit_order',
        'department_access_' . (int)$allowedDepartment['id'],
        'department_access_' . (int)$deniedDepartment['id'],
    ]);

    $sessionOnlyAllowed = api_fixtures()->createUserSession([
        'edit_order',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should fail when moving to inaccessible department (user only has access to allowedDepartment)
    api_client()->put('/order', [
        'id' => (int)$order['id'],
        'department_id' => (int)$deniedDepartment['id'],
    ], $sessionOnlyAllowed['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed when user has access to both departments
    api_client()->put('/order', [
        'id' => (int)$order['id'],
        'department_id' => (int)$deniedDepartment['id'],
    ], $sessionBothDepts['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});

it('enforces department access when marking an order as completed via POST /orders/mark_as_completed', function (): void {
    api_test_covers('POST /orders/mark_as_completed', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Complete Allowed Dept']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Complete Denied Dept']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Complete Customer']);

    $orderAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);

    $session = api_fixtures()->createUserSession([
        'mark_order_as_completed',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should fail for inaccessible department
    api_client()->post('/orders/mark_as_completed', [
        'id' => (int)$orderDenied['id'],
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed for accessible department
    api_client()->post('/orders/mark_as_completed', [
        'id' => (int)$orderAllowed['id'],
    ], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});

it('enforces department access when deleting order attachments via DELETE /orders/attachments', function (): void {
    api_test_covers('DELETE /orders/attachments', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Del Attach Allowed']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Del Attach Denied']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Del Attach Customer']);

    $orderAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);
    $attachmentAllowed = api_fixtures()->createOrderAttachment(['order_id' => (int)$orderAllowed['id']]);
    $attachmentDenied = api_fixtures()->createOrderAttachment(['order_id' => (int)$orderDenied['id']]);

    $session = api_fixtures()->createUserSession([
        'delete_order_attachments',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should fail for inaccessible department
    api_client()->delete('/orders/attachments', [
        'order_id' => (int)$orderDenied['id'],
        'attachment_id' => (int)$attachmentDenied['id'],
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed for accessible department
    api_client()->delete('/orders/attachments', [
        'order_id' => (int)$orderAllowed['id'],
        'attachment_id' => (int)$attachmentAllowed['id'],
    ], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});

it('enforces department access when adding order items via POST /order/items', function (): void {
    api_test_covers('POST /order/items', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Add Allowed']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Add Denied']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Item Add Customer']);
    $category = api_fixtures()->createCategory(['name' => 'Order Item Dept Access Category']);
    $product = api_fixtures()->createProduct(['name' => 'Order Item Dept Access Product', 'category' => $category['id']]);

    $orderAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);

    $session = api_fixtures()->createUserSession([
        'add_order_items',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should fail for inaccessible department
    api_client()->post('/order/items', [
        'order_id' => (int)$orderDenied['id'],
        'product_id' => (int)$product['id'],
        'quantity' => 1,
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed for accessible department
    api_client()->post('/order/items', [
        'order_id' => (int)$orderAllowed['id'],
        'product_id' => (int)$product['id'],
        'quantity' => 1,
    ], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});

it('enforces department access when deleting order items via DELETE /order/items', function (): void {
    api_test_covers('DELETE /order/items', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Del Allowed']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Del Denied']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Item Del Customer']);
    $category = api_fixtures()->createCategory(['name' => 'Order Item Del Category']);
    $product = api_fixtures()->createProduct(['name' => 'Order Item Del Product', 'category' => $category['id']]);

    $orderAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);

    $session = api_fixtures()->createUserSession([
        'delete_order_items',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    $superuserSession = api_fixtures()->createUserSession([], ['group_id' => 1]);
    $itemAllowedResponse = api_client()->post('/order/items', [
        'order_id' => (int)$orderAllowed['id'],
        'product_id' => (int)$product['id'],
        'quantity' => 1,
    ], $superuserSession['headers']);
    $itemAllowed = $itemAllowedResponse->data();

    $itemDeniedResponse = api_client()->post('/order/items', [
        'order_id' => (int)$orderDenied['id'],
        'product_id' => (int)$product['id'],
        'quantity' => 1,
    ], $superuserSession['headers']);
    $itemDenied = $itemDeniedResponse->data();

    // Should fail for inaccessible department
    api_client()->delete('/order/items', ['id' => (int)$itemDenied['id']], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed for accessible department
    api_client()->delete('/order/items', ['id' => (int)$itemAllowed['id']], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});

it('enforces department access when editing order items via PUT /order/items', function (): void {
    api_test_covers('PUT /order/items', 'department access');

    $allowedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Edit Allowed']);
    $deniedDepartment = api_fixtures()->createDepartment(['name' => 'Order Item Edit Denied']);
    $customer = api_fixtures()->createUser(['display_name' => 'Order Item Edit Customer']);
    $category = api_fixtures()->createCategory(['name' => 'Order Item Edit Category']);
    $product = api_fixtures()->createProduct(['name' => 'Order Item Edit Product', 'category' => $category['id']]);

    $orderAllowed = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$allowedDepartment['id'],
    ]);
    $orderDenied = api_fixtures()->createOrder([
        'customer_id' => $customer['customer_number'],
        'department_id' => (int)$deniedDepartment['id'],
    ]);
    $itemAllowed = api_fixtures()->createOrderItem([
        'order_id' => (int)$orderAllowed['id'],
        'product_id' => (int)$product['id'],
        'cashier_id' => 1,
        'price' => 100,
    ]);
    $itemDenied = api_fixtures()->createOrderItem([
        'order_id' => (int)$orderDenied['id'],
        'product_id' => (int)$product['id'],
        'cashier_id' => 1,
        'price' => 200,
    ]);

    $session = api_fixtures()->createUserSession([
        'edit_order_items',
        'list_order_items',
        'department_access_' . (int)$allowedDepartment['id'],
    ]);

    // Should fail for inaccessible department
    api_client()->put('/order/items', [
        'id' => (int)$itemDenied['id'],
        'price' => 999,
        'notes' => '',
        'reference' => '',
        'quantity' => 1,
    ], $session['headers'])
        ->assertStatus(403)
        ->assertEnvelope()
        ->assertSuccess(false)
        ->assertMissingPermissions(['department_access_' . (int)$deniedDepartment['id']]);

    // Should succeed for accessible department
    api_client()->put('/order/items', [
        'id' => (int)$itemAllowed['id'],
        'price' => 150,
        'notes' => '',
        'reference' => '',
        'quantity' => 1,
    ], $session['headers'])
        ->assertStatus(200)
        ->assertEnvelope()
        ->assertSuccess();
});
