<?php

declare(strict_types=1);

use App\Jobs\RouteSupplierLeadJob;
use App\Models\Deal;
use App\Models\Project;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Billing\LedgerService;
use App\Services\DuplicateDetector;
use App\Services\LeadRouter;
use App\Services\NotificationService;
use App\Services\SupplierProjects\SupplierProjectResolver;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Mockery as M;
use Tests\Concerns\SharesSupplierPdo;

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

beforeEach(function (): void {
    // Plan 4 Task 4: LedgerService требует наличия активных PricingTier'ов для tier-resolve.
    $this->seed(PricingTierSeeder::class);
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
});

function runRouteJob(int $supplierLeadId): void
{
    (new RouteSupplierLeadJob($supplierLeadId))->handle(
        app(LeadRouter::class),
        app(SupplierProjectResolver::class),
        app(DuplicateDetector::class),
        app(NotificationService::class),
        app(LedgerService::class),
    );
}

it('routes 1 lead to N tenants — creates N deal copies (sharing-model)', function (): void {
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'vashinvestor.ru',
    ]);

    $tenants = collect();
    $projects = collect();
    for ($i = 0; $i < 3; $i++) {
        $t = Tenant::factory()->create(['balance_leads' => 100]);
        $tenants->push($t);
        $projects->push(Project::factory()->create([
            'tenant_id' => $t->id,
            'supplier_b1_project_id' => $supplier->id,
            'signal_type' => 'site',
            'signal_identifier' => 'vashinvestor.ru',
            'is_active' => true,
            'delivered_today' => 0,
            'delivered_in_month' => 0,
        ]));
    }

    $vid = 432176649;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => [
            'vid' => $vid,
            'project' => 'B1_vashinvestor.ru',
            'tag' => 'tag',
            'phone' => '79991234567',
            'phones' => ['79991234567'],
            'time' => now()->getTimestamp(),
        ],
    ]);

    runRouteJob($lead->id);

    $lead->refresh();
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(3);
    expect($lead->supplier_project_id)->toBe($supplier->id);

    foreach ($projects as $i => $p) {
        $tenant = $tenants[$i];
        $p->refresh();
        expect($p->delivered_today)->toBe(1);
        expect($p->delivered_in_month)->toBe(1);

        DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
        $deals = Deal::query()
            ->where('tenant_id', $tenant->id)
            ->where('source_crm_id', $vid)
            ->get();
        expect($deals)->toHaveCount(1);
    }
});

it('decrements balance_leads for each tenant by 1', function (): void {
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'test.ru',
    ]);
    $tenant = Tenant::factory()->create(['balance_leads' => 100]);
    Project::factory()->create([
        'tenant_id' => $tenant->id,
        'supplier_b1_project_id' => $supplier->id,
        'signal_type' => 'site',
        'signal_identifier' => 'test.ru',
        'is_active' => true,
    ]);

    $vid = 99;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    runRouteJob($lead->id);

    expect($tenant->fresh()->balance_leads)->toBe(99);
});

it('marks duplicate via DuplicateDetector — no charge, no counter increment', function (): void {
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'test.ru',
    ]);
    $tenant = Tenant::factory()->create(['balance_leads' => 100]);
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'supplier_b1_project_id' => $supplier->id,
        'signal_type' => 'site',
        'signal_identifier' => 'test.ru',
        'is_active' => true,
        'delivered_today' => 0,
    ]);

    DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
    $master = Deal::create([
        'tenant_id' => $tenant->id,
        'source_crm_id' => 999,
        'project_id' => $project->id,
        'phone' => '79991234567',
        'phones' => ['79991234567'],
        'status' => 'new',
        'received_at' => now()->subHours(2),
    ]);

    $vid = 1000;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    runRouteJob($lead->id);

    expect($tenant->fresh()->balance_leads)->toBe(100);
    expect($project->fresh()->delivered_today)->toBe(0);

    DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
    $duplicate = Deal::where('source_crm_id', $vid)->first();
    expect($duplicate)->not->toBeNull();
    expect($duplicate->duplicate_of_id)->toBe($master->id);
});

it('throws DomainException when payload encodes B1+SMS combo', function (): void {
    $vid = 1;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_TINKOFF', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    expect(fn () => runRouteJob($lead->id))->toThrow(DomainException::class);
});

it('handles orphan supplier_project (no matching liderra-projects) — 0 deals, lead processed', function (): void {
    $vid = 777;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_orphan.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    runRouteJob($lead->id);

    $lead->refresh();
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(0);
    expect($lead->supplier_project_id)->not->toBeNull();
});

it('handles mixed routing: 3 projects, 1 with pre-existing master (dup), 2 clean', function (): void {
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'mixed.ru',
    ]);

    $tenants = collect();
    $projects = collect();
    for ($i = 0; $i < 3; $i++) {
        $t = Tenant::factory()->create(['balance_leads' => 100]);
        $tenants->push($t);
        $projects->push(Project::factory()->create([
            'tenant_id' => $t->id,
            'supplier_b1_project_id' => $supplier->id,
            'signal_type' => 'site',
            'signal_identifier' => 'mixed.ru',
            'is_active' => true,
            'delivered_today' => 0,
            'delivered_in_month' => 0,
        ]));
    }

    // Tenant #0 имеет master deal с тем же phone в окне 24 ч — будет дубль.
    $masterTenant = $tenants[0];
    $masterProject = $projects[0];
    DB::statement("SET LOCAL app.current_tenant_id = '{$masterTenant->id}'");
    $master = Deal::create([
        'tenant_id' => $masterTenant->id,
        'source_crm_id' => 555,
        'project_id' => $masterProject->id,
        'phone' => '79991234567',
        'phones' => ['79991234567'],
        'status' => 'new',
        'received_at' => now()->subHours(2),
    ]);

    $vid = 2222;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_mixed.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    runRouteJob($lead->id);

    $lead->refresh();
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(2); // 2 чистых, 1 дубль не считается

    // Tenant #0: deal помечен duplicate_of_id, balance НЕ списан, delivered_today = 0
    expect($masterTenant->fresh()->balance_leads)->toBe(100);
    expect($masterProject->fresh()->delivered_today)->toBe(0);
    DB::statement("SET LOCAL app.current_tenant_id = '{$masterTenant->id}'");
    $dupDeal = Deal::query()->where('source_crm_id', $vid)->first();
    expect($dupDeal->duplicate_of_id)->toBe($master->id);

    // Tenant #1, #2: balance списан, delivered_today инкрементирован
    foreach ([1, 2] as $i) {
        $t = $tenants[$i];
        $p = $projects[$i];
        expect($t->fresh()->balance_leads)->toBe(99);
        expect($p->fresh()->delivered_today)->toBe(1);
    }
});

it('idempotent on retry — second handle() returns early, no ghost duplicate deals (Plan 2.5 fix #3)', function (): void {
    // BLOCKER #3 (CV.11 audit): RouteSupplierLeadJob::$tries = 3. На retry задачи (после
    // транзиентного сбоя — DB hiccup, queue worker restart) handle() запускался ПОВТОРНО
    // без guard'а на $lead->processed_at. Второй проход создавал ВТОРОЙ Deal в БД с тем
    // же vid (DuplicateDetector помечал его как duplicate, без charge — но deal-row в БД
    // оставался). Также $lead->update(['deals_created_count' => $createdCount]) переписывал
    // счётчик: первый run = 1, второй run = 0 (все дубли) → искажение метрики.
    //
    // Fix: в начале handle() — if ($lead->processed_at !== null) return; — early return.
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'retry-idempotent.ru',
    ]);
    $tenant = Tenant::factory()->create(['balance_leads' => 100]);
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'supplier_b1_project_id' => $supplier->id,
        'signal_type' => 'site',
        'signal_identifier' => 'retry-idempotent.ru',
        'is_active' => true,
        'delivered_today' => 0,
    ]);

    $vid = 7777;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => [
            'vid' => $vid,
            'project' => 'B1_retry-idempotent.ru',
            'phone' => '79991234567',
            'time' => now()->getTimestamp(),
        ],
    ]);

    // 1st run — нормальная обработка.
    runRouteJob($lead->id);
    $lead->refresh();
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(1);
    expect($tenant->fresh()->balance_leads)->toBe(99);
    expect($project->fresh()->delivered_today)->toBe(1);

    DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
    expect(Deal::query()->where('source_crm_id', $vid)->count())->toBe(1);

    // 2nd run — должен быть no-op (idempotent guard на processed_at).
    runRouteJob($lead->id);
    $lead->refresh();

    // Лид остаётся помечен обработанным, deals_created_count НЕ сбросился.
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(1);

    // НИКАКИХ дублей не появилось: balance, counter, deal-row.
    expect($tenant->fresh()->balance_leads)->toBe(99);
    expect($project->fresh()->delivered_today)->toBe(1);

    DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
    expect(Deal::query()->where('source_crm_id', $vid)->count())->toBe(1);
});

it('handles partial failure: one project throws, others continue routing', function (): void {
    // Тест полагается на Tenant SoftDeletes (см. App\Models\Tenant) — soft-delete
    // tenant'а в середине loop'а заставляет Tenant::firstOrFail() выкинуть
    // ModelNotFoundException, что симулирует per-Project failure без мокинга.
    // Если SoftDeletes когда-либо удалят с Tenant — этот тест нужно переписать
    // на runtime-mock или удалить (PHPStan не пропускает $this->markTestSkipped()
    // внутри Pest-closure).
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'partial-failure.ru',
    ]);

    $tenants = collect();
    $projects = collect();
    for ($i = 0; $i < 3; $i++) {
        $t = Tenant::factory()->create(['balance_leads' => 100]);
        $tenants->push($t);
        $projects->push(Project::factory()->create([
            'tenant_id' => $t->id,
            'supplier_b1_project_id' => $supplier->id,
            'signal_type' => 'site',
            'signal_identifier' => 'partial-failure.ru',
            'is_active' => true,
            'delivered_today' => 0,
        ]));
    }

    // Soft-delete tenant #1 — Tenant::firstOrFail() в createDealCopyForProject упадёт.
    $tenants[1]->delete();

    $vid = 3333;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => ['vid' => $vid, 'project' => 'B1_partial-failure.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()],
    ]);

    runRouteJob($lead->id);

    $lead->refresh();
    expect($lead->processed_at)->not->toBeNull();
    expect($lead->deals_created_count)->toBe(2); // tenant 0 + 2; tenant 1 упал

    // Tenants 0 и 2 успешно списаны
    expect($tenants[0]->fresh()->balance_leads)->toBe(99);
    expect($tenants[2]->fresh()->balance_leads)->toBe(99);
});

it('rejects deal copy if delivered_today >= limit at lock time (Plan 2.5 fix #2 race recheck)', function (): void {
    // BLOCKER #2 (CV.11 audit): matchEligibleProjects делает SELECT delivered_today < limit
    // БЕЗ lockForUpdate. Между snapshot SELECT и createDealCopyForProject (которое
    // инкрементит) — окно для concurrent webhook'а:
    //   worker A видит delivered_today=9, limit=10 → OK; createDealCopyForProject → 10.
    //   worker B параллельно видит то же 9 → OK; createDealCopyForProject → 11. OVERCOMMIT.
    //
    // Симуляция: project уже at-limit (delivered_today=1, daily_limit_target=1) к
    // моменту createDealCopyForProject — мокнутый LeadRouter возвращает его как eligible
    // (так, будто matchEligibleProjects делал SELECT когда delivered_today=0).
    //
    // Fix #2: внутри createDealCopyForProject под lockForUpdate(Project) — recheck
    // delivered_today < COALESCE(effective_daily_limit_today, daily_limit_target).
    // Если уже at-limit → return false без charge / counter / deal-row.
    $supplier = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
        'unique_key' => 'race-recheck.ru',
    ]);
    $tenant = Tenant::factory()->create(['balance_leads' => 100]);
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'supplier_b1_project_id' => $supplier->id,
        'signal_type' => 'site',
        'signal_identifier' => 'race-recheck.ru',
        'is_active' => true,
        'daily_limit_target' => 1,
        'effective_daily_limit_today' => null, // COALESCE → daily_limit_target=1
        'delivered_today' => 1,                // ALREADY AT LIMIT (race-window simulation)
        'delivered_in_month' => 5,
    ]);

    $vid = 8888;
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => null,
        'platform' => 'B1',
        'vid' => $vid,
        'phone' => '79991234567',
        'raw_payload' => [
            'vid' => $vid,
            'project' => 'B1_race-recheck.ru',
            'phone' => '79991234567',
            'time' => now()->getTimestamp(),
        ],
    ]);

    // Подсунуть LeadRouter mock, который игнорирует filter и возвращает project,
    // как будто SELECT'нул его при snapshot delivered_today=0.
    $routerMock = M::mock(LeadRouter::class);
    $routerMock->shouldReceive('matchEligibleProjects')
        ->andReturn(new Collection([$project]));
    app()->instance(LeadRouter::class, $routerMock);

    runRouteJob($lead->id);

    $lead->refresh();

    // После fix #2: deal НЕ создан (recheck под lock увидел limit) → 0 deals.
    expect($lead->deals_created_count)->toBe(0);

    // delivered_today остался 1 (НЕ инкрементнулся до 2).
    expect($project->fresh()->delivered_today)->toBe(1);
    // delivered_in_month НЕ инкрементнулся.
    expect($project->fresh()->delivered_in_month)->toBe(5);

    // balance_leads НЕ списан.
    expect($tenant->fresh()->balance_leads)->toBe(100);

    // Deal-row не создался.
    DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
    expect(Deal::query()->where('source_crm_id', $vid)->count())->toBe(0);
});
