<?php

declare(strict_types=1);

use App\Exceptions\Supplier\SupplierTransientException;
use App\Jobs\RouteSupplierLeadJob;
use App\Jobs\Supplier\CsvReconcileJob;
use App\Jobs\Supplier\RefreshSupplierSessionJob;
use App\Mail\CsvDriftAlertMail;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Services\Supplier\SupplierCsvParser;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\Concerns\SharesSupplierPdo;

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

/**
 * Hard re-puts `supplier:session` immediately before the SUT reads it.
 *
 * Parallel-test race fix: other Supplier tests (Sync*, Cleanup*) call
 * `Cache::store('redis')->forget('supplier:session')` in their afterEach.
 * In `--parallel` mode all workers share Redis DB+prefix, so a concurrent
 * afterEach can wipe our session between beforeEach `put` and the SUT call,
 * triggering PlaywrightBridge auto-refresh (which has no credentials).
 *
 * Calling this immediately before the job dispatches the HTTP request
 * minimizes the race window. The test still tolerates the rare case where
 * another test's afterEach runs between this put and the SUT's read — but
 * empirically the window is too small for that to fire.
 */
function putSupplierSession(): void
{
    Cache::store('redis')->put(
        'supplier:session',
        ['phpsessid' => 'test', 'csrf' => 'test'],
        now()->addHour(),
    );
}

beforeEach(function () {
    Mail::fake();
    // Partial fake: only RouteSupplierLeadJob is intercepted (what we assert on).
    // RefreshSupplierSessionJob must NOT be faked — it must run our mock below
    // so that loadSession() can recover if a concurrent afterEach wipes the session.
    Bus::fake([RouteSupplierLeadJob::class]);
    // Bind a mock that re-puts the session when dispatch_sync triggers it during a race.
    app()->bind(RefreshSupplierSessionJob::class, fn () => new class
    {
        public function handle(): void
        {
            putSupplierSession();
        }
    });
    // NB: NOT Cache::store('redis')->flush() — flush wipes session keys belonging to
    // OTHER parallel tests (cross-pollution). Just forget our reserved keys + re-put.
    Cache::store('redis')->forget('supplier:csv_reconcile');
    putSupplierSession();
    config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
    config(['services.supplier.alert_email' => 'ops@liderra.ru']);
});

afterEach(function () {
    Cache::store('redis')->forget('supplier:csv_reconcile');
});

function csvBody(array $rows): string
{
    $out = "vid;project;tag;phone;phones;time\n";
    foreach ($rows as $r) {
        $out .= "{$r['vid']};{$r['project']};;{$r['phone']};{$r['phone']};{$r['time']}\n";
    }

    return $out;
}

function makeSupplierProject(): SupplierProject
{
    return SupplierProject::factory()->create([
        'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'a.com',
    ]);
}

it('matches existing leads, no missing — status=ok, no alert', function () {
    $sp = makeSupplierProject();
    $now = time();

    $vids = [];
    for ($i = 0; $i < 10; $i++) {
        $vid = (int) ('11100000'.$i);  // numeric vid because BIGINT
        $vids[] = $vid;
        SupplierLead::factory()->create([
            'vid' => $vid,
            'phone' => '79991234567',
            'supplier_project_id' => $sp->id,
            'received_at' => now()->subHour(),
        ]);
    }

    $rows = [];
    foreach ($vids as $vid) {
        $rows[] = ['vid' => (string) $vid, 'project' => 'B1_a.com', 'phone' => '79991234567', 'time' => $now - 3600];
    }
    Http::fake(['crm.bp-gr.ru/admin/report/index*' => Http::response(csvBody($rows), 200)]);

    putSupplierSession();
    app(CsvReconcileJob::class)->handle(
        app(SupplierPortalClient::class),
        app(SupplierCsvParser::class),
        app(Mailer::class),
    );

    $log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
    expect($log->status)->toBe('ok');
    expect((int) $log->total_csv_rows)->toBe(10);
    expect((int) $log->matched_count)->toBe(10);
    expect((int) $log->recovered_count)->toBe(0);

    Mail::assertNothingSent();
    Bus::assertNothingDispatched();
});

it('drift 10% (1 missing of 10) → alert email + 1 RouteJob dispatched', function () {
    $sp = makeSupplierProject();
    $now = time();

    $vids = [];
    for ($i = 0; $i < 10; $i++) {
        $vids[] = (int) ('22200000'.$i);
    }

    // Existing 9 of 10
    for ($i = 0; $i < 9; $i++) {
        SupplierLead::factory()->create([
            'vid' => $vids[$i],
            'phone' => '79991234567',
            'supplier_project_id' => $sp->id,
            'received_at' => now()->subHour(),
        ]);
    }

    $rows = [];
    foreach ($vids as $vid) {
        $rows[] = ['vid' => (string) $vid, 'project' => 'B1_a.com', 'phone' => '79991234567', 'time' => $now - 3600];
    }
    Http::fake(['crm.bp-gr.ru/admin/report/index*' => Http::response(csvBody($rows), 200)]);

    putSupplierSession();
    app(CsvReconcileJob::class)->handle(
        app(SupplierPortalClient::class),
        app(SupplierCsvParser::class),
        app(Mailer::class),
    );

    $log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
    expect($log->status)->toBe('drift_alert');
    expect((float) $log->drift_ratio)->toBeGreaterThan(0.05);
    expect((int) $log->recovered_count)->toBe(1);

    Mail::assertSent(CsvDriftAlertMail::class, 1);
    Bus::assertDispatched(RouteSupplierLeadJob::class, 1);
});

it('drift 1% (1 missing of 100) → status=ok, no alert', function () {
    $sp = makeSupplierProject();
    $now = time();

    $vids = [];
    for ($i = 0; $i < 100; $i++) {
        $vids[] = (int) ('33300'.str_pad((string) $i, 4, '0', STR_PAD_LEFT));
    }

    for ($i = 0; $i < 99; $i++) {
        SupplierLead::factory()->create([
            'vid' => $vids[$i],
            'phone' => '79991234567',
            'supplier_project_id' => $sp->id,
            'received_at' => now()->subHour(),
        ]);
    }

    $rows = [];
    foreach ($vids as $vid) {
        $rows[] = ['vid' => (string) $vid, 'project' => 'B1_a.com', 'phone' => '79991234567', 'time' => $now - 3600];
    }
    Http::fake(['crm.bp-gr.ru/admin/report/index*' => Http::response(csvBody($rows), 200)]);

    putSupplierSession();
    app(CsvReconcileJob::class)->handle(
        app(SupplierPortalClient::class),
        app(SupplierCsvParser::class),
        app(Mailer::class),
    );

    $log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
    expect($log->status)->toBe('ok');
    expect((int) $log->recovered_count)->toBe(1);
    Mail::assertNothingSent();
});

it('empty CSV → status=ok, drift=0, no alert', function () {
    Http::fake(['crm.bp-gr.ru/admin/report/index*' => Http::response("vid;project;tag;phone;phones;time\n", 200)]);

    putSupplierSession();
    app(CsvReconcileJob::class)->handle(
        app(SupplierPortalClient::class),
        app(SupplierCsvParser::class),
        app(Mailer::class),
    );

    $log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
    expect($log->status)->toBe('ok');
    expect((int) $log->total_csv_rows)->toBe(0);
});

it('SupplierTransientException → status=failed, error_message recorded', function () {
    Http::fake(['crm.bp-gr.ru/*' => Http::response('Server Error', 500)]);

    putSupplierSession();
    expect(fn () => app(CsvReconcileJob::class)->handle(
        app(SupplierPortalClient::class),
        app(SupplierCsvParser::class),
        app(Mailer::class),
    )
    )->toThrow(SupplierTransientException::class);

    $log = DB::table('supplier_csv_reconcile_log')->latest('id')->first();
    expect($log->status)->toBe('failed');
    expect($log->error_message)->toContain('500');
});

it('Schedule entry: hourly cron registered', function () {
    /** @var Schedule $schedule */
    $schedule = app(Schedule::class);

    $events = $schedule->events();
    $hasCsv = collect($events)->contains(function ($event) {
        $repr = (string) ($event->description ?? '');
        if (property_exists($event, 'job')) {
            $repr .= ' '.((string) $event->job);
        }

        return str_contains($repr, 'CsvReconcileJob');
    });
    expect($hasCsv)->toBeTrue();
});
