<?php

declare(strict_types=1);

use App\Jobs\RouteSupplierLeadJob;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
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 Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;

/**
 * Task 2 plan 2026-05-29-supplier-webhook-fast-fail-and-stuck-cleanup.md
 *
 * Tests the fast-fail guard in RouteSupplierLeadJob::handle():
 * if supplier_lead.error contains a terminal pattern ('does not support',
 * 'platform mismatch', 'no matching supplier_project') and processed_at IS NULL,
 * the job marks processed and exits without writing to failed_webhook_jobs.
 *
 * Correction 1/2: uses RouteSupplierLeadJob (not ProcessSupplierWebhookJob).
 * Correction 3: fast-fail inserted between the 2 existing idempotency guards
 * and parseProjectField call.
 */
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);

// ---------- helpers --------------------------------------------------------

function dispatchHandleSync(int $leadId): void
{
    $job = new RouteSupplierLeadJob($leadId);
    $job->handle(
        app(LeadRouter::class),
        app(SupplierProjectResolver::class),
        app(NotificationService::class),
        app(LedgerService::class),
        app(LeadDistributor::class),
        app(RegionTagResolver::class),
    );
}

function countFailedWebhookJobs(): int
{
    return (int) DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->count();
}

// ---------- setup ----------------------------------------------------------

beforeEach(function (): void {
    // Ensure pgsql_supplier sees the same transaction via shared PDO.
    DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->delete();
    // Create one shared SupplierProject so all tests in this file share it —
    // avoids unique constraint violations from repeated factory calls.
    $this->sharedProject = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'call',
        'unique_key' => 'fast-fail-test-'.uniqid(),
    ]);
});

// ---------- tests ----------------------------------------------------------

it('fast-fails when supplier_lead has terminal "does not support" error and processed_at IS NULL', function (): void {
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'platform' => 'B1',
        'error' => 'B1 platform does not support SMS signals (supplier limitation: chk_supplier_projects_b1_not_for_sms)',
        'processed_at' => null,
    ]);

    $beforeFails = countFailedWebhookJobs();

    dispatchHandleSync($lead->id);

    $afterFails = countFailedWebhookJobs();
    expect($afterFails)->toBe($beforeFails, 'fast-fail must not write to failed_webhook_jobs');

    $fresh = $lead->fresh();
    expect($fresh?->processed_at)->not->toBeNull('fast-fail must mark processed_at');
    expect($fresh?->error)->toContain('[fast-failed by RouteSupplierLeadJob]');
});

it('fast-fails when error contains "platform mismatch"', function (): void {
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'platform' => 'B2',
        'error' => 'Routing failed: platform mismatch for this lead type',
        'processed_at' => null,
    ]);

    $beforeFails = countFailedWebhookJobs();
    dispatchHandleSync($lead->id);

    expect(countFailedWebhookJobs())->toBe($beforeFails);
    expect($lead->fresh()?->processed_at)->not->toBeNull();
});

it('fast-fails when error contains "no matching supplier_project"', function (): void {
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'platform' => 'B3',
        'error' => 'no matching supplier_project found for identifier ваши_деньги',
        'processed_at' => null,
    ]);

    $beforeFails = countFailedWebhookJobs();
    dispatchHandleSync($lead->id);

    expect(countFailedWebhookJobs())->toBe($beforeFails);
    expect($lead->fresh()?->processed_at)->not->toBeNull();
});

it('does NOT fast-fail when lead error is null (normal new lead)', function (): void {
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'platform' => 'B1',
        'error' => null,
        'processed_at' => null,
    ]);

    // Normal path will throw (no matching supplier_project in test env) — that's OK.
    // The important thing: no fast-fail terminal mark has been set on the lead.
    try {
        dispatchHandleSync($lead->id);
    } catch (Throwable) {
        // expected
    }

    $fresh = $lead->fresh();
    $wasFastFailed = $fresh?->processed_at !== null
        && str_contains($fresh?->error ?? '', '[fast-failed by RouteSupplierLeadJob]');
    expect($wasFastFailed)->toBeFalse('must not fast-fail a lead with no prior error');
});

it('does NOT fast-fail when lead already has processed_at set (idempotency guard fires first)', function (): void {
    $processedAt = now()->subMinutes(5);
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'error' => 'B1 platform does not support SMS signals',
        'processed_at' => $processedAt,
    ]);

    // Should return early due to processed_at guard, not the fast-fail guard.
    dispatchHandleSync($lead->id);

    // processed_at must remain unchanged (not overwritten by fast-fail)
    $fresh = $lead->fresh();
    expect($fresh?->processed_at?->toDateTimeString())
        ->toBe($processedAt->toDateTimeString(), 'processed_at must not change when already set');
    // error must not get the fast-fail suffix
    expect($fresh?->error)->not->toContain('[fast-failed by RouteSupplierLeadJob]');
});

it('does NOT fast-fail for transient connection errors not matching terminal patterns', function (): void {
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $this->sharedProject->id,
        'platform' => 'B1',
        'error' => 'Connection refused to PostgreSQL at 127.0.0.1',
        'processed_at' => null,
    ]);

    try {
        dispatchHandleSync($lead->id);
    } catch (Throwable) {
        // expected — transient errors may rethrow
    }

    $fresh = $lead->fresh();
    $wasFastFailed = $fresh?->processed_at !== null
        && str_contains($fresh?->error ?? '', '[fast-failed by RouteSupplierLeadJob]');
    expect($wasFastFailed)->toBeFalse('transient errors must not trigger fast-fail');
});
