<?php

declare(strict_types=1);

use App\Jobs\RouteSupplierLeadJob;
use App\Models\Deal;
use App\Models\LeadCharge;
use App\Models\Project;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Billing\LedgerService;
use App\Services\LeadDistributor;
use App\Services\LeadRouter;
use App\Services\NotificationService;
use App\Services\RegionTagResolver;
use App\Services\SupplierProjects\SupplierProjectResolver;
use Carbon\Carbon;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;

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

/**
 * Phase 2 — webhook ↔ CSV-recovered idempotency.
 *
 * Сценарий (наблюдался на prod 2026-05-25, 37 дублей tenant client1):
 *   1. Поставщик шлёт webhook → 302 (теряется тело) — Phase 1 уже починила.
 *   2. CsvReconcileJob через 30 мин видит лид в CSV, не находит supplier_lead
 *      по (phone, project) → создаёт recovered SupplierLead (vid=NULL,
 *      source='csv_recovery') → RouteSupplierLeadJob → Deal с source_crm_id=NULL.
 *   3. Поставщик ретраит webhook (ещё 15 мин) → новый SupplierLead с vid=<int>
 *      → RouteSupplierLeadJob → создаёт второй Deal с тем же phone+project
 *      → биллинг списывает второй раз.
 *
 * Phase 2 fix: шаг 3 находит существующий CSV-recovered deal, обновляет
 * source_crm_id, привязывает webhook supplier_lead к существующему deal через
 * supplier_lead_deliveries, НЕ создаёт второй Deal, НЕ списывает повторно.
 */
beforeEach(function (): void {
    $this->seed(PricingTierSeeder::class);
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");

    // Shared supplier_project для всех тестов (B1, site, domain race-csv.ru).
    $this->sp = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'race-csv.ru',
    ]);

    $this->tenant = Tenant::factory()->create([
        'balance_rub' => '10000.00',
        'delivered_in_month' => 0,
    ]);

    $this->project = Project::factory()->create([
        'tenant_id' => $this->tenant->id,
        'signal_type' => 'site',
        'signal_identifier' => 'race-csv.ru',
        'supplier_b1_project_id' => $this->sp->id,
        'is_active' => true,
        'daily_limit_target' => 100,
        'effective_daily_limit_today' => 100,
        'delivered_today' => 0,
        'delivery_days_mask' => 127,
        'region_mask' => 255,
    ]);

    linkProjectToSupplier($this->project, $this->sp);

    createRoutingSnapshotFromProject($this->project, null, 'site', 'race-csv.ru', 100);
    createRoutingSnapshotFromProject($this->project, Carbon::tomorrow('Europe/Moscow')->toDateString(), 'site', 'race-csv.ru', 100);
});

/**
 * Dispatch helper — mirrors runRouteJob() / dispatchJob() from other test files.
 */
function runRaceJob(int $supplierLeadId): void
{
    (new RouteSupplierLeadJob($supplierLeadId))->handle(
        app(LeadRouter::class),
        app(SupplierProjectResolver::class),
        app(NotificationService::class),
        app(LedgerService::class),
        app(LeadDistributor::class),
        app(RegionTagResolver::class),
    );
}

// ---------------------------------------------------------------------------
// Test 1 — Main bug reproduction: CSV-recovery followed by webhook retry
//          ДОЛЖЕН дать 1 deal + 1 charge (сейчас даёт 2+2 → FAILING).
// ---------------------------------------------------------------------------
it('webhook after CSV-recovered merges into existing deal (no duplicate, no double-charge)', function (): void {
    $phone = '79991000001';

    // ── Step 1: CSV-recovered SupplierLead (vid=null, source='csv_recovery') ──
    // Это то, что CsvReconcileJob создаёт: звонок найден в CSV поставщика,
    // но настоящего webhook_log'а нет → вид неизвестен (vid=null).
    $csvLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => null,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subHour()->getTimestamp(),
        ],
        'received_at' => now()->subHour(),
        'recovered_from_csv_at' => now()->subHour(),
        'source' => 'csv_recovery',
        'processed_at' => null,
    ]);

    // RouteSupplierLeadJob обрабатывает CSV-recovered лид → создаёт Deal с source_crm_id=NULL.
    runRaceJob($csvLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");
    $csvDeal = Deal::where('phone', $phone)->first();
    expect($csvDeal)->not->toBeNull('CSV recovery должен был создать Deal');
    expect($csvDeal->source_crm_id)->toBeNull('CSV-recovered deal должен иметь source_crm_id=NULL');

    $chargesAfterCsv = LeadCharge::where('deal_id', $csvDeal->id)->count();
    expect($chargesAfterCsv)->toBe(1, 'После CSV-recovery должен быть ровно 1 LeadCharge');

    $balanceAfterCsv = (string) $this->tenant->fresh()->balance_rub;

    // ── Step 2: поставщик ретраит webhook 15 мин спустя с настоящим vid ──
    // Это то, что создаёт дубль на проде: новый SupplierLead с vid != null,
    // phone + project те же → RouteSupplierLeadJob создаёт ВТОРОЙ Deal.
    $webhookLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => 1672819986,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 1672819986,
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subMinutes(15)->getTimestamp(),
        ],
        'received_at' => now()->subMinutes(15),
        'source' => 'webhook',
        'processed_at' => null,
    ]);

    runRaceJob($webhookLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");

    // ── Assertions ──

    // Assertion 1: по-прежнему ОДИН deal, но source_crm_id теперь заполнен.
    $deals = Deal::where('phone', $phone)->get();
    expect($deals)->toHaveCount(1, 'Phase 2: webhook после CSV-recovery должен ОБНОВИТЬ существующий deal, а не создать второй');
    expect($deals->first()->source_crm_id)->toBe(1672819986, 'source_crm_id должен быть обновлён от webhook vid');

    // Assertion 2: НЕТ второго LeadCharge — биллинг не списывается дважды.
    $chargesAfterWebhook = LeadCharge::where('deal_id', $csvDeal->id)->count();
    expect($chargesAfterWebhook)->toBe(1, 'Phase 2: второй LeadCharge создан не должен быть');

    // Assertion 3: баланс НЕ списан второй раз.
    $balanceAfterWebhook = (string) $this->tenant->fresh()->balance_rub;
    expect($balanceAfterWebhook)->toBe($balanceAfterCsv, 'Phase 2: баланс после webhook не должен уменьшиться');

    // Assertion 4: supplier_lead_deliveries содержит ОБА supplier_lead_id,
    //   привязанных к ОДНОМУ deal_id.
    $deliveries = DB::table('supplier_lead_deliveries')
        ->where('deal_id', $csvDeal->id)
        ->get();
    expect($deliveries)->toHaveCount(2, 'Оба SupplierLead (csv + webhook) должны быть в supplier_lead_deliveries');
    $deliveredLeadIds = $deliveries->pluck('supplier_lead_id')->sort()->values()->all();
    expect($deliveredLeadIds)->toContain($csvLead->id);
    expect($deliveredLeadIds)->toContain($webhookLead->id);
});

// ---------------------------------------------------------------------------
// Test 1b — merge НЕ меняет received_at CSV-recovered сделки.
//   Regression-guard инцидента 26.05.2026 04:12–05:03 UTC (9 failed_jobs):
//   received_at — partition key, lead_charges имеет FK (deal_id, deal_received_at)
//   ON DELETE CASCADE / ON UPDATE NO ACTION. Любое изменение received_at при merge
//   ломало FK на COMMIT → каскадный риск. Код (RouteSupplierLeadJob:371-393) сохраняет
//   received_at как есть; этот тест пиннит инвариант (раньше не assert'ился).
// ---------------------------------------------------------------------------
it('merge preserves CSV-recovered received_at unchanged (FK-partition safety, no extra charge)', function (): void {
    $phone = '79991000004';
    $csvTime = now()->subHours(3);

    // ── Step 1: CSV-recovered сделка с известным received_at (3 часа назад) ──
    $csvLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => null,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => $csvTime->getTimestamp(),
        ],
        'received_at' => $csvTime,
        'recovered_from_csv_at' => $csvTime,
        'source' => 'csv_recovery',
        'processed_at' => null,
    ]);
    runRaceJob($csvLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");
    $csvDeal = Deal::where('phone', $phone)->first();
    expect($csvDeal)->not->toBeNull('CSV recovery должен был создать Deal');
    $receivedAtBefore = $csvDeal->received_at;
    expect($receivedAtBefore->getTimestamp())->toBe($csvTime->getTimestamp(), 'received_at сделки = время CSV-лида');

    // ── Step 2: догоняющий webhook с реальным vid и ДРУГИМ time (15 мин назад) ──
    $webhookLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => 1672819990,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 1672819990,
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subMinutes(15)->getTimestamp(),
        ],
        'received_at' => now()->subMinutes(15),
        'source' => 'webhook',
        'processed_at' => null,
    ]);
    runRaceJob($webhookLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");

    // ── Assertions ──
    $deals = Deal::where('phone', $phone)->get();
    expect($deals)->toHaveCount(1, 'merge: одна сделка, не две');

    $merged = $deals->first();
    // Ключевой инвариант: received_at НЕ изменился webhook-временем (15 мин назад),
    // остался временем CSV-лида (3 часа назад) — FK-partition safety.
    expect($merged->received_at->getTimestamp())->toBe(
        $csvTime->getTimestamp(),
        'merge НЕ должен менять received_at (partition key + lead_charges FK)',
    );
    expect((int) $merged->source_crm_id)->toBe(1672819990, 'source_crm_id обновлён webhook vid');

    // Списание ровно одно — merge не доначисляет.
    expect(LeadCharge::where('deal_id', $merged->id)->count())->toBe(1, 'merge: второго списания нет');
});

// ---------------------------------------------------------------------------
// Test 2 — Spec B regression: два webhook с РАЗНЫМИ vid → два deal (by-design).
//          Наш Phase 2 fix НЕ должен блокировать это.
// ---------------------------------------------------------------------------
it('two webhooks with DIFFERENT vids both create deals (Spec B — за повторы поставщика берём)', function (): void {
    $phone = '79991000002';

    // Первый webhook, vid=100.
    $lead1 = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => 100,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 100,
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subHour()->getTimestamp(),
        ],
        'received_at' => now()->subHour(),
        'source' => 'webhook',
        'processed_at' => null,
    ]);
    runRaceJob($lead1->id);

    // Второй webhook, vid=200 (другой лид поставщика, тот же телефон+проект).
    $lead2 = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => 200,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 200,
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subMinutes(30)->getTimestamp(),
        ],
        'received_at' => now()->subMinutes(30),
        'source' => 'webhook',
        'processed_at' => null,
    ]);
    runRaceJob($lead2->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");

    // Spec B: оба webhook'а имеют source_crm_id != null.
    // Условие merge (source_crm_id IS NULL) не срабатывает → два deal,
    // два LeadCharge. Spec B Phase 1 (commit ccfecd5e) за повторы поставщика берём.
    $deals = Deal::where('phone', $phone)->get();
    expect($deals)->toHaveCount(2, 'Два webhook с разными vid должны создавать два deal (Spec B)');
    $sourceCrmIds = $deals->pluck('source_crm_id')->sort()->values()->all();
    expect($sourceCrmIds)->toContain(100);
    expect($sourceCrmIds)->toContain(200);
    expect(LeadCharge::whereIn('deal_id', $deals->pluck('id'))->count())->toBe(2);
});

// ---------------------------------------------------------------------------
// Test 3 — Boundary: CSV-recovered deal старше 24h НЕ мержится с новым webhook.
//          Окно merge — 24h. Старый лид не считается «активным» duplicate.
// ---------------------------------------------------------------------------
it('csv-recovered deal older than 24h is NOT merged with new webhook', function (): void {
    $phone = '79991000003';

    // CSV-recovered SupplierLead, обработанный 2 дня назад.
    $csvLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => null,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->subDays(2)->getTimestamp(),
        ],
        'received_at' => now()->subDays(2),
        'recovered_from_csv_at' => now()->subDays(2),
        'source' => 'csv_recovery',
        'processed_at' => null,
    ]);
    runRaceJob($csvLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");
    $csvDeal = Deal::where('phone', $phone)->first();
    expect($csvDeal)->not->toBeNull('CSV-recovered deal должен существовать');

    // Сбросим processed_at у tenant-level проекта: delivered_today накопился,
    // нужно сбросить счётчик чтобы второй deal тоже прошёл лимит.
    $this->project->update(['delivered_today' => 0]);

    // Webhook приходит сейчас — deal CSV-recovery старше 24h → не мержится.
    $webhookLead = SupplierLead::factory()->create([
        'platform' => 'B1',
        'phone' => $phone,
        'vid' => 999,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 999,
            'project' => 'B1_race-csv.ru',
            'phone' => $phone,
            'time' => now()->getTimestamp(),
        ],
        'received_at' => now(),
        'source' => 'webhook',
        'processed_at' => null,
    ]);
    runRaceJob($webhookLead->id);

    DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant->id}'");

    // Два deal: старый CSV-recovered (2 дня назад) + новый от webhook.
    // Merge НЕ происходит — CSV-recovered вне 24h окна.
    $deals = Deal::where('phone', $phone)->get();
    expect($deals)->toHaveCount(2, 'CSV-recovered deal старше 24h — merge не происходит, создаётся новый deal от webhook');
});
