<?php

declare(strict_types=1);

/**
 * Plan 3 Task 3 — supplier-flow на pgsql_supplier (BYPASSRLS) connection.
 *
 * 4 regression-теста, закрывающих 3 backlog-айтема Plan 2.6:
 *   1. RouteSupplierLeadJob $connection = 'pgsql_supplier' (sanity-check, что свойство выставлено).
 *   2. BLOCKER #6: failed_webhook_jobs INSERT с tenant_id=NULL проходит под pgsql_supplier
 *      (политика tenant_isolation отвергает NULL под обычной ролью; BYPASSRLS обходит).
 *   3. WARN #2: LeadRouter::matchEligibleProjects видит проекты ВСЕХ tenant'ов
 *      без SET LOCAL app.current_tenant_id (sharing-model).
 *   4. WARN #3: ResetDeliveredTodayCommand сбрасывает delivered_today по всем tenant'ам.
 *
 * Spec: docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.
 *
 * NB: На dev DB_USERNAME=postgres (superuser, BYPASSRLS implicit) — fallback в
 * config/database.php pgsql_supplier берёт DB_USERNAME, тесты работают без отдельной
 * роли. На production env-keys DB_SUPPLIER_USERNAME/PASSWORD указывают на crm_supplier_worker.
 */

use App\Jobs\RouteSupplierLeadJob;
use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\LeadRouter;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;

uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);

beforeEach(function (): void {
    // Симулируем условия supplier-flow на queue worker'е: tenant ещё не определён.
    // set_config('app.current_tenant_id', '0', true) — session-scoped + откатывается транзакцией.
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
});

test('RouteSupplierLeadJob declares DB_CONNECTION = pgsql_supplier (Plan 3 Task 3)', function (): void {
    // Job's $connection из Bus\Queueable управляет очередью (sync/database/redis), не БД.
    // Для DB-операций используется константа DB_CONNECTION — failed() callback пишет
    // в failed_webhook_jobs через DB::connection(RouteSupplierLeadJob::DB_CONNECTION).
    // Закрывает BLOCKER #6: INSERT с tenant_id=NULL проходит под BYPASSRLS-роль.
    expect(RouteSupplierLeadJob::DB_CONNECTION)->toBe('pgsql_supplier');
});

test('SyncSupplierProjectJob declares DB_CONNECTION = pgsql_supplier (queue worker has no tenant GUC)', function (): void {
    // Дублирует RouteSupplierLeadJob: создание/правка проекта тоже запускается из очереди,
    // где SetTenantContext-прослойка не отработала. Под обычной ролью crm_app_user
    // SELECT по projects падает 42704 (unrecognized configuration parameter
    // "app.current_tenant_id"). Все DB-операции джоба обязаны идти через pgsql_supplier (BYPASSRLS).
    expect(SyncSupplierProjectJob::DB_CONNECTION)->toBe('pgsql_supplier');
});

test('failed_webhook_jobs INSERT с tenant_id=NULL проходит под pgsql_supplier (BLOCKER #6)', function (): void {
    // Под обычной ролью policy tenant_isolation USING (tenant_id = current_setting('app.current_tenant_id')::bigint)
    // отвергает NULL (NULL :: bigint = NULL, NULL = '0'::bigint → NULL → false).
    // Под pgsql_supplier (BYPASSRLS на prod / postgres superuser на dev) INSERT проходит.
    DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->insert([
        'tenant_id' => null,
        'webhook_log_id' => null,
        'raw_payload' => json_encode(['supplier_lead_id' => 42, 'project' => 'B1_test.ru']),
        'exception' => 'simulated failure for BLOCKER #6 regression test',
        'retry_count' => 3,
        'failed_at' => now(),
    ]);

    $exists = DB::connection('pgsql_supplier')
        ->table('failed_webhook_jobs')
        ->whereNull('tenant_id')
        ->where('exception', 'simulated failure for BLOCKER #6 regression test')
        ->exists();

    expect($exists)->toBeTrue();
});

test("LeadRouter видит проекты всех tenant'ов под pgsql_supplier без SET LOCAL (WARN #2)", function (): void {
    // 6 tenant × 1 проект, все привязаны к одному supplier_project.
    // БЕЗ SET LOCAL app.current_tenant_id (он уже '0' из beforeEach) — под обычной
    // ролью RLS отбросил бы всё (вернул 0); под pgsql_supplier (BYPASSRLS) видны все 6.
    // NB: matchEligibleProjects ограничивает выдачу cap = LeadDistributor::CAP = 3
    //     (лид продаётся ≤3 раз, взвешенный жребий), по одному проекту на тенанта.
    //     Поэтому возвращается 3 (а не 6 и не 0) — это и доказывает кросс-tenant
    //     видимость под BYPASSRLS: при RLS было бы 0.
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'plan3-task3-warn2.example.com',
    ]);

    $tenants = Tenant::factory()->count(6)->create(['balance_leads' => 100, 'balance_rub' => 500]);
    foreach ($tenants as $tenant) {
        $project = Project::factory()->create([
            'tenant_id' => $tenant->id,
            'signal_type' => 'site',
            'signal_identifier' => 'plan3-task3-warn2.example.com',
            'is_active' => true,
            'daily_limit_target' => 10,
            'delivered_today' => 0,
            'delivery_days_mask' => 127,
        ]);
        DB::table('project_supplier_links')->insert([
            'project_id' => $project->id,
            'supplier_project_id' => $supplier->id,
            'platform' => $supplier->platform,
            'subject_code' => $supplier->subject_code,
        ]);
        createRoutingSnapshotFromProject($project, null, 'site', 'plan3-task3-warn2.example.com', 10);
    }

    $router = app(LeadRouter::class);
    $eligible = $router->matchEligibleProjects($supplier);

    // cap=3 из 6 видимых тенантов; ключевое — выбраны РАЗНЫЕ тенанты (кросс-tenant
    // видимость под BYPASSRLS), а не 0 (что было бы под RLS).
    expect($eligible)->toHaveCount(3);
    expect($eligible->pluck('tenant_id')->unique()->count())->toBe(3);
});

test("ResetDeliveredTodayCommand сбрасывает delivered_today по всем tenant'ам (WARN #3)", function (): void {
    // Создаём 3 tenant'а с проектами, у каждого delivered_today=5.
    // Команда должна сбросить все 3 → 0 (под pgsql_supplier BYPASSRLS — без SET LOCAL).
    $tenants = Tenant::factory()->count(3)->create();
    $projectIds = [];
    foreach ($tenants as $tenant) {
        $project = Project::factory()->create([
            'tenant_id' => $tenant->id,
            'delivered_today' => 5,
        ]);
        $projectIds[] = $project->id;
    }

    // @phpstan-ignore-next-line method.notFound (Pest TestCall->artisan() mixin)
    $this->artisan('projects:reset-delivered-today')->assertExitCode(0);

    $remaining = DB::connection('pgsql_supplier')
        ->table('projects')
        ->whereIn('id', $projectIds)
        ->where('delivered_today', '>', 0)
        ->count();

    expect($remaining)->toBe(0);
});
