<?php

declare(strict_types=1);

use App\Jobs\RouteSupplierLeadJob;
use App\Models\SupplierLead;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;

/**
 * Plan 3 Task 8: RetryFailedSupplierJobsCommand.
 *
 * Schema adaptation (db/schema.sql v8.11 failed_webhook_jobs):
 *   - НЕТ supplier_lead_id колонки → марка supplier-flow rows:
 *     tenant_id IS NULL AND raw_payload->>'supplier_lead_id' IS NOT NULL
 *     (см. RouteSupplierLeadJob::failed() — он вставляет именно так).
 *   - НЕТ retry_attempts/last_retried_at → используем existing колонки:
 *     - retry_count (INT) — счётчик оставшихся попыток (decrement при каждом retry).
 *     - retried_at (TIMESTAMPTZ) — last retry timestamp (cooldown 1h).
 *     - resolved_at (TIMESTAMPTZ) — терминальное состояние (исключает retry).
 *     - failed_at (TIMESTAMPTZ) — window 24h (старше — skip).
 *
 * Semantics retry_count: при создании row в failed_webhook_jobs RouteSupplierLeadJob
 * сетит retry_count = $tries = 3 (max попыток queue-уровня). Command интерпретирует
 * это значение как "оставшиеся manual retries"; при каждом retry decrement; при
 * достижении 0 — set resolved_at=NOW() со статусом "exhausted" (через JSON marker
 * в raw_payload).
 */
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);

beforeEach(function (): void {
    Bus::fake();

    // Очищаем persistent garbage из failed_webhook_jobs (рудимент старых
    // test-сессий, когда pgsql_supplier не был частью DatabaseTransactions).
    // Этот DELETE сам выполняется в текущей pgsql-транзакции через shared PDO
    // (SharesSupplierPdo trait) и откатится по завершении теста — реальные
    // production-данные не страдают.
    DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->delete();
});

/**
 * Helper: вставка supplier-marked failed_webhook_jobs row.
 *
 * @param  array<string, mixed>  $overrides
 */
function insertFailedSupplierRow(array $overrides = []): int
{
    $supplierLead = SupplierLead::factory()->create([
        'processed_at' => null,
    ]);

    $payload = [
        'supplier_lead_id' => $supplierLead->id,
    ];

    $defaults = [
        'tenant_id' => null,
        'webhook_log_id' => null,
        'raw_payload' => json_encode($payload, JSON_UNESCAPED_UNICODE),
        'exception' => 'Test failure',
        'retry_count' => 3,
        'failed_at' => now()->subMinutes(30),
        'retried_at' => null,
        'resolved_at' => null,
    ];

    $row = array_merge($defaults, $overrides);

    return (int) DB::connection('pgsql_supplier')
        ->table('failed_webhook_jobs')
        ->insertGetId($row);
}

test('dispatches RouteSupplierLeadJob for each eligible supplier-flow row', function (): void {
    $id1 = insertFailedSupplierRow();
    $id2 = insertFailedSupplierRow();

    $this->artisan('supplier:retry-failed')->assertExitCode(0);
    // suppress unused warning — IDs needed for fresh() verification below.
    expect($id1)->toBeGreaterThan(0)->and($id2)->toBeGreaterThan(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 2);

    // Both rows должны иметь обновлённый retried_at и decremented retry_count.
    foreach ([$id1, $id2] as $id) {
        $row = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($id);
        expect($row->retried_at)->not->toBeNull();
        expect((int) $row->retry_count)->toBe(2);
    }
});

test('skips rows recently retried within cooldown (<1h)', function (): void {
    $recentlyRetried = insertFailedSupplierRow([
        'retried_at' => now()->subMinutes(30), // < 1h ago
    ]);
    $eligible = insertFailedSupplierRow([
        'retried_at' => now()->subHours(2), // > 1h ago
    ]);

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 1);

    // Recently retried row — не тронут.
    $skipped = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($recentlyRetried);
    expect((int) $skipped->retry_count)->toBe(3); // не decremented

    // Eligible row — retried.
    $processed = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($eligible);
    expect((int) $processed->retry_count)->toBe(2);
});

test('decrements retry_count and updates retried_at on dispatched row', function (): void {
    $id = insertFailedSupplierRow(['retry_count' => 3]);

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    $row = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($id);
    expect((int) $row->retry_count)->toBe(2);
    expect($row->retried_at)->not->toBeNull();
});

test('marks resolved_at when retry_count reaches 0 (max attempts exhausted)', function (): void {
    // retry_count=1 → после dispatch'а станет 0 → set resolved_at=NOW().
    $id = insertFailedSupplierRow(['retry_count' => 1]);

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 1);

    $row = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($id);
    expect((int) $row->retry_count)->toBe(0);
    expect($row->resolved_at)->not->toBeNull();
});

test('skips rows older than 24h (window safety cap)', function (): void {
    $tooOld = insertFailedSupplierRow([
        'failed_at' => now()->subDays(2),
    ]);
    $fresh = insertFailedSupplierRow([
        'failed_at' => now()->subHours(12),
    ]);

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 1);

    $skipped = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($tooOld);
    expect($skipped->retried_at)->toBeNull(); // не тронут
});

test('skips rows already resolved', function (): void {
    $resolved = insertFailedSupplierRow([
        'resolved_at' => now()->subHours(1),
    ]);
    $unresolved = insertFailedSupplierRow();

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 1);

    $skipped = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($resolved);
    expect((int) $skipped->retry_count)->toBe(3); // не тронут
});

test('skips non-supplier rows (tenant_id IS NOT NULL OR missing supplier_lead_id)', function (): void {
    // Обычный tenant-bound failed webhook (НЕ supplier-flow).
    $tenant = Tenant::factory()->create();
    $tenantBoundId = DB::connection('pgsql_supplier')
        ->table('failed_webhook_jobs')
        ->insertGetId([
            'tenant_id' => $tenant->id,
            'webhook_log_id' => null,
            'raw_payload' => json_encode(['foo' => 'bar'], JSON_UNESCAPED_UNICODE),
            'exception' => 'Other failure',
            'retry_count' => 3,
            'failed_at' => now()->subMinutes(30),
            'retried_at' => null,
            'resolved_at' => null,
        ]);

    // Supplier-flow row (tenant_id NULL, supplier_lead_id present).
    $supplierId = insertFailedSupplierRow();

    $this->artisan('supplier:retry-failed')->assertExitCode(0);

    Bus::assertDispatchedTimes(RouteSupplierLeadJob::class, 1);

    $skipped = DB::connection('pgsql_supplier')->table('failed_webhook_jobs')->find($tenantBoundId);
    expect($skipped->retried_at)->toBeNull();
    expect((int) $skipped->retry_count)->toBe(3);
});

test('reports dispatched count via output and Log', function (): void {
    insertFailedSupplierRow();
    insertFailedSupplierRow();
    insertFailedSupplierRow();

    $this->artisan('supplier:retry-failed')
        ->expectsOutputToContain('Re-dispatched 3')
        ->assertExitCode(0);
});

test('handles empty queue gracefully (0 eligible rows)', function (): void {
    // No rows inserted.
    $this->artisan('supplier:retry-failed')
        ->expectsOutputToContain('Re-dispatched 0')
        ->assertExitCode(0);

    Bus::assertNotDispatched(RouteSupplierLeadJob::class);
});
