<?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);

/**
 * Task 2.7 — страховка от двойного списания вокруг смены источника.
 *
 * Шов §9b «двойное списание»: переделка матча на слепок (Task 2.2/2.6) могла бы
 * развести project_id у CSV-recovered лида и догоняющего webhook одного физического
 * лида. Если бы они попали в РАЗНЫЕ проекты — merge по (tenant, phone, project_id)
 * не сработал бы → второй Deal → второе списание.
 *
 * Тест гоняется с ФЛАГОМ ВКЛ (routing_match_by_snapshot=true), чтобы доказать:
 * новый матч по слепку оставляет project_id сходящимся, merge срабатывает,
 * списание ровно одно. Это страховка — она зелёная и ДО, и ПОСЛЕ переделки.
 * Если позеленела до и покраснела после — матч развёл project_id (см. план §9b).
 */
beforeEach(function (): void {
    $this->seed(PricingTierSeeder::class);
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");

    // Новый матч по слепку ВКЛ — тестируем риск §9b именно на нём.
    DB::table('system_settings')->updateOrInsert(
        ['key' => 'routing_match_by_snapshot'],
        ['value' => 'true', 'type' => 'bool', 'updated_at' => now()],
    );

    // sms supplier_project — самая рискованная ветка матча (sms_senders[0]+keyword).
    $this->sp = SupplierProject::factory()->create([
        'platform' => 'B3',
        'signal_type' => 'sms',
        'unique_key' => 'Caranga',
    ]);

    $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' => 'sms',
        'signal_identifier' => null,
        'sms_senders' => ['Caranga'],
        'sms_keyword' => null,
        'supplier_b3_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);

    // Слепки на сегодня и завтра — несут sms-источник Caranga.
    insertSmsSnapshotFor($this->project, Carbon::today('Europe/Moscow')->toDateString());
    insertSmsSnapshotFor($this->project, Carbon::tomorrow('Europe/Moscow')->toDateString());
});

function insertSmsSnapshotFor(Project $project, string $date): void
{
    DB::table('project_routing_snapshots')->insert([
        'snapshot_date' => $date,
        'project_id' => $project->id,
        'tenant_id' => $project->tenant_id,
        'daily_limit' => 100,
        'delivery_days_mask' => 127,
        'regions' => '{}',
        'signal_type' => 'sms',
        'signal_identifier' => null,
        'sms_senders' => json_encode(['Caranga']),
        'sms_keyword' => null,
        'expected_volume' => 100,
        'delivered_count' => 0,
        'created_at' => now(),
    ]);
}

function runNoDoubleChargeJob(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),
    );
}

it('не списывает дважды когда CSV-recovery и webhook одного лида приходят вокруг смены источника', function (): void {
    $phone = '79161234567';

    // ── CSV-recovered лид (vid=null) → создаёт Deal ──
    $csvLead = SupplierLead::factory()->create([
        'platform' => 'B3',
        'phone' => $phone,
        'vid' => null,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'project' => 'B3_Caranga',
            'phone' => $phone,
            'time' => now()->subHour()->getTimestamp(),
        ],
        'received_at' => now()->subHour(),
        'recovered_from_csv_at' => now()->subHour(),
        'source' => 'csv_recovery',
        'processed_at' => null,
    ]);
    runNoDoubleChargeJob($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(LeadCharge::where('deal_id', $csvDeal->id)->count())->toBe(1);
    $balanceAfterCsv = (string) $this->tenant->fresh()->balance_rub;

    // ── webhook того же физического лида (vid=int) приходит позже ──
    $webhookLead = SupplierLead::factory()->create([
        'platform' => 'B3',
        'phone' => $phone,
        'vid' => 555,
        'supplier_project_id' => $this->sp->id,
        'raw_payload' => [
            'vid' => 555,
            'project' => 'B3_Caranga',
            'phone' => $phone,
            'time' => now()->subMinutes(15)->getTimestamp(),
        ],
        'received_at' => now()->subMinutes(15),
        'source' => 'webhook',
        'processed_at' => null,
    ]);
    runNoDoubleChargeJob($webhookLead->id);

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

    // ── ОДНА сделка, ОДНО списание, баланс не списан дважды ──
    $deals = Deal::where('phone', $phone)->get();
    expect($deals)->toHaveCount(1, '§9b: webhook после CSV merge'.'ится в один deal — project_id сошёлся под матчем по слепку');
    expect((int) $deals->first()->source_crm_id)->toBe(555);
    expect(LeadCharge::where('deal_id', $csvDeal->id)->count())->toBe(1, '§9b: второго списания нет');
    expect((string) $this->tenant->fresh()->balance_rub)->toBe($balanceAfterCsv, '§9b: баланс не списан второй раз');
});
