<?php

declare(strict_types=1);

use App\Exceptions\Supplier\SupplierAuthException;
use App\Jobs\Supplier\RefreshSupplierSessionJob;
use App\Jobs\Supplier\SyncSupplierProjectsJob;
use App\Mail\SupplierCriticalAlertMail;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\SupplierSyncLog;
use App\Models\Tenant;
use App\Services\Supplier\Channel\AjaxProjectChannel;
use Carbon\Carbon;
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);
uses(SharesSupplierPdo::class);

beforeEach(function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-12 10:00:00', 'Europe/Moscow'));

    Cache::store('redis')->put('supplier:session', [
        'phpsessid' => 'sess',
        'csrf' => 'csrf',
        'refreshed_at' => now()->toIso8601String(),
    ], now()->addHours(6));

    config(['services.supplier.portal_url' => 'https://crm.bp-gr.ru']);
    config(['services.supplier.alert_email' => 'ops@liderra.test']);
});

afterEach(function (): void {
    Cache::store('redis')->forget('supplier:session');
    Carbon::setTestNow();
});

// ---------------------------------------------------------------------------
// Multi-region grouping (merged into single group)
// ---------------------------------------------------------------------------

/**
 * Project regions=[82,83] site → 1 group (merged regions) → tag='РФ' →
 * 1 multi-flag save → 3 supplier_projects (platforms B1/B2/B3)
 * subject_code=null, current_regions=[82,83]; pivot — 3 links for the project.
 */
test('single-group: regions=[82,83] site → merged regions tag=РФ → 3 supplier_projects + 3 pivot links', function (): void {
    $tenant = Tenant::factory()->create();

    /** @var Project $project */
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'persubject.example.com',
        'daily_limit_target' => 9,
        'delivery_days_mask' => 127,
        'regions' => [82, 83],
    ]);
    insertSnapshotForTomorrow($project, regions: '{82,83}');

    // One save (merged regions=[82,83] → tag='РФ') + one listProjects
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '1001'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '1001', 'src' => 'rt', 'name' => 'persubject.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'persubject.example.com'],
                ['id' => '1002', 'src' => 'bl', 'name' => 'persubject.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'persubject.example.com'],
                ['id' => '1003', 'src' => 'mt', 'name' => 'persubject.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'persubject.example.com'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    // 3 supplier_projects (not 6): all regions merged into one group
    $sps = SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'persubject.example.com')
        ->where('signal_type', 'site')
        ->get();

    expect($sps)->toHaveCount(3);
    expect($sps->pluck('platform')->sort()->values()->all())->toBe(['B1', 'B2', 'B3']);

    // subject_code=null (no per-subject split)
    expect($sps->pluck('subject_code')->unique()->all())->toContain(null);

    // regions merged: [82, 83] — sorted ascending, stored on each SP
    expect($sps->firstWhere('platform', 'B1')->current_regions)->toBe([82, 83]);

    // pivot: 3 links (not 6)
    $pivotCount = DB::table('project_supplier_links')
        ->where('project_id', $project->id)
        ->count();
    expect($pivotCount)->toBe(3);
});

// ---------------------------------------------------------------------------
// All-RF pool
// ---------------------------------------------------------------------------

test('all-RF pool: regions=[] → 1 group subject_code=null tag=РФ → 3 supplier_projects', function (): void {
    $tenant = Tenant::factory()->create();

    /** @var Project $project */
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'rf-pool.example.com',
        'daily_limit_target' => 6,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '500'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '500', 'src' => 'rt', 'name' => 'rf-pool.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'rf-pool.example.com'],
                ['id' => '501', 'src' => 'bl', 'name' => 'rf-pool.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'rf-pool.example.com'],
                ['id' => '502', 'src' => 'mt', 'name' => 'rf-pool.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'rf-pool.example.com'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    $sps = SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'rf-pool.example.com')
        ->where('signal_type', 'site')
        ->get();

    expect($sps)->toHaveCount(3);
    expect($sps->pluck('subject_code')->unique()->all())->toContain(null);
    expect($sps->pluck('current_regions')->first())->toBe([]);

    // pivot
    $pivotCount = DB::table('project_supplier_links')
        ->where('project_id', $project->id)
        ->count();
    expect($pivotCount)->toBe(3);
});

// ---------------------------------------------------------------------------
// Order: 2 projects on one (source × subject) → computeOrder
// ---------------------------------------------------------------------------

test('order: 2 projects same source×subject → computeOrder([10,20])=20 split across B1/B2/B3 = 7/7/6', function (): void {
    $tenant = Tenant::factory()->create();

    $project1 = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'order-test.example.com',
        'daily_limit_target' => 10,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project1);

    $project2 = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'order-test.example.com',
        'daily_limit_target' => 20,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project2);

    // saveProjectMultiFlag called once (both projects share same group)
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '600'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '600', 'src' => 'rt', 'name' => 'order-test.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'order-test.example.com'],
                ['id' => '601', 'src' => 'bl', 'name' => 'order-test.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'order-test.example.com'],
                ['id' => '602', 'src' => 'mt', 'name' => 'order-test.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'order-test.example.com'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    // computeOrder([10, 20]) = max(20, ceil(30/3)=10) = 20 (the GROUP order), then split
    // across B1/B2/B3 = 7/7/6 (Σ == 20 — NOT 20 on each = 60, which would be the ×3 overspend).
    $sps = SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'order-test.example.com')
        ->get();

    // Single group → exactly 3 supplier_projects (not 6 as would happen if grouped separately)
    expect($sps)->toHaveCount(3);
    expect($sps->sum('current_limit'))->toBe(20);
    expect($sps->firstWhere('platform', 'B1')->current_limit)->toBe(7);
});

test('limit is DIVIDED across B1/B2/B3 so supplier total == project limit (owner-reported ×3 bug)', function (): void {
    // The owner reported (and we verified live 2026-05-21): call limit 18 → 18/18/18 on the
    // portal = supplier could deliver up to 54. The portal does NOT divide. Fix splits 18 → 6/6/6.
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'call',
        'signal_identifier' => '79135161263',
        'daily_limit_target' => 18,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(['status' => 'OK', 'message' => '', 'id' => '4000'], 200),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(['projects' => [
            ['id' => '4001', 'src' => 'rt', 'name' => '79135161263', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135161263'],
            ['id' => '4002', 'src' => 'bl', 'name' => '79135161263', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135161263'],
            ['id' => '4003', 'src' => 'mt', 'name' => '79135161263', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79135161263'],
        ]], 200),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    // Assert only THIS group's rows (the nightly job syncs every active project in the DB).
    $sps = SupplierProject::on('pgsql_supplier')->where('unique_key', '79135161263')->get();
    expect($sps)->toHaveCount(3);
    expect($sps->sum('current_limit'))->toBe(18); // Σ == project limit (not 54)
    expect($sps->sortBy('platform')->pluck('current_limit', 'platform')->all())
        ->toBe(['B1' => 6, 'B2' => 6, 'B3' => 6]); // 18 / 3
});

// ---------------------------------------------------------------------------
// SMS platforms
// ---------------------------------------------------------------------------

test('sms+keyword → platforms B2+B3 (2 supplier_projects per subject)', function (): void {
    $tenant = Tenant::factory()->create();

    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'sms',
        'signal_identifier' => null,
        'sms_senders' => ['79001234567'],
        'sms_keyword' => 'KVARTIRA',
        'daily_limit_target' => 5,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project, signalType: 'sms', signalIdentifier: null);

    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '700'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '700', 'src' => 'bl', 'name' => '79001234567+KVARTIRA', 'tag' => 'РФ', 'type' => 'sms', 'content' => '79001234567+KVARTIRA'],
                ['id' => '701', 'src' => 'mt', 'name' => '79001234567+KVARTIRA', 'tag' => 'РФ', 'type' => 'sms', 'content' => '79001234567+KVARTIRA'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    $sps = SupplierProject::on('pgsql_supplier')
        ->where('signal_type', 'sms')
        ->get();

    // sms+keyword → B2+B3 only
    expect($sps)->toHaveCount(2);
    expect($sps->pluck('platform')->sort()->values()->all())->toBe(['B2', 'B3']);
    expect($sps->where('platform', 'B1')->count())->toBe(0);
});

test('sms without keyword → platform B3 only (1 supplier_project)', function (): void {
    $tenant = Tenant::factory()->create();

    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'sms',
        'signal_identifier' => null,
        'sms_senders' => ['79009876543'],
        'sms_keyword' => null,
        'daily_limit_target' => 5,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project, signalType: 'sms', signalIdentifier: null);

    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '800'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '800', 'src' => 'mt', 'name' => '79009876543', 'tag' => 'РФ', 'type' => 'sms', 'content' => '79009876543'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    $sps = SupplierProject::on('pgsql_supplier')
        ->where('signal_type', 'sms')
        ->get();

    expect($sps)->toHaveCount(1);
    expect($sps->first()->platform)->toBe('B3');
});

// ---------------------------------------------------------------------------
// Idempotent: repeat run → updateProject (no duplicate supplier_projects/pivot)
// ---------------------------------------------------------------------------

test('idempotent: repeat run with no changes → updateProject not duplicate', function (): void {
    $tenant = Tenant::factory()->create();

    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'idempotent.example.com',
        'daily_limit_target' => 9,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    // First run: create
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '900'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '900', 'src' => 'rt', 'name' => 'idempotent.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'idempotent.example.com'],
                ['id' => '901', 'src' => 'bl', 'name' => 'idempotent.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'idempotent.example.com'],
                ['id' => '902', 'src' => 'mt', 'name' => 'idempotent.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'idempotent.example.com'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    expect(SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'idempotent.example.com')
        ->count())->toBe(3);

    // Second run: no changes → updateProject calls (rt-project-save with id != 0)
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '900'],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    // Still 3 (no duplicates)
    expect(SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'idempotent.example.com')
        ->count())->toBe(3);

    // updateProject sends id != 0
    Http::assertSent(fn ($r) => str_ends_with($r->url(), '/admin/visit/rt-project-save')
        && (int) ($r['id'] ?? 0) !== 0);
});

// ---------------------------------------------------------------------------
// Orthogonal: time budget, auth, abort-50, sync_log
// ---------------------------------------------------------------------------

test('respects time budget by stopping at 20:55 МСК', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-12 20:56:00', 'Europe/Moscow'));

    $tenant = Tenant::factory()->create();
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'time-budget.example.com',
        'daily_limit_target' => 9,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    Http::fake();
    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    Http::assertNothingSent();
});

test('sticky auth error throws and sends critical alert email', function (): void {
    Mail::fake();
    Bus::fake([RefreshSupplierSessionJob::class]);

    $tenant = Tenant::factory()->create();
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'auth-fail.example.com',
        'daily_limit_target' => 9,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    Http::fake([
        'crm.bp-gr.ru/*' => Http::response('Unauthorized', 401),
    ]);

    expect(fn () => (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class)))
        ->toThrow(SupplierAuthException::class);

    Mail::assertQueued(SupplierCriticalAlertMail::class, function (SupplierCriticalAlertMail $mail): bool {
        return $mail->alertType === 'sticky_auth';
    });
});

test('aborts after 50 consecutive transient failures and sends alert', function (): void {
    Mail::fake();
    $tenant = Tenant::factory()->create();

    for ($i = 1; $i <= 60; $i++) {
        $project = Project::factory()->create([
            'tenant_id' => $tenant->id,
            'is_active' => true,
            'signal_type' => 'site',
            'signal_identifier' => "host{$i}.abort.com",
            'daily_limit_target' => 9,
            'delivery_days_mask' => 127,
            'regions' => [],
        ]);
        insertSnapshotForTomorrow($project);
    }

    Http::fake(['crm.bp-gr.ru/*' => Http::response('upstream', 503)]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    Mail::assertQueued(SupplierCriticalAlertMail::class, function (SupplierCriticalAlertMail $mail): bool {
        return $mail->alertType === 'mass_transient';
    });
});

test('writes supplier_sync_log row for each successful action', function (): void {
    $tenant = Tenant::factory()->create();

    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => 'audit-log.example.com',
        'daily_limit_target' => 9,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '555'],
            200,
        ),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response(
            ['projects' => [
                ['id' => '555', 'src' => 'rt', 'name' => 'audit-log.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'audit-log.example.com'],
                ['id' => '556', 'src' => 'bl', 'name' => 'audit-log.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'audit-log.example.com'],
                ['id' => '557', 'src' => 'mt', 'name' => 'audit-log.example.com', 'tag' => 'РФ', 'type' => 'hosts', 'content' => 'audit-log.example.com'],
            ]],
            200,
        ),
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    // 3 supplier_projects created → 3 log rows (one per platform)
    $sp = SupplierProject::on('pgsql_supplier')
        ->where('unique_key', 'audit-log.example.com')
        ->where('platform', 'B1')
        ->first();

    expect($sp)->not->toBeNull();

    $log = SupplierSyncLog::on('pgsql_supplier')
        ->where('supplier_project_id', $sp->id)
        ->first();

    expect($log)->not->toBeNull()
        ->and($log->action)->toBe('create')
        ->and($log->http_status)->toBe(200)
        ->and($log->error_message)->toBeNull();
});

test('nightly: re-creates donor on portal when its external_id no longer exists there', function (): void {
    // Regression mirror of SyncSupplierProjectJobTest: donor deleted on portal → stale
    // external_id in our DB → updateProject is a silent no-op → donor never re-created.
    // Nightly reconciler must detect missing donors (listProjects) and re-create in-place.
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->create([
        'tenant_id' => $tenant->id,
        'is_active' => true,
        'signal_type' => 'call',
        'signal_identifier' => '79993334455',
        'daily_limit_target' => 10,
        'delivery_days_mask' => 127,
        'regions' => [],
    ]);
    insertSnapshotForTomorrow($project);

    foreach (['B1', 'B2', 'B3'] as $platform) {
        SupplierProject::on('pgsql_supplier')->forceCreate([
            'platform' => $platform,
            'signal_type' => 'call',
            'unique_key' => '79993334455',
            'subject_code' => null,
            'supplier_external_id' => 'GONE'.$platform,
            'current_limit' => 10,
            'current_workdays' => [1, 2, 3, 4, 5, 6, 7],
            'current_regions' => [],
            'sync_status' => 'ok',
            'last_synced_at' => now()->subDay(),
        ]);
    }

    $loadCalls = 0;
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(['status' => 'OK', 'message' => '', 'id' => '8003'], 200),
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => function () use (&$loadCalls) {
            $loadCalls++;
            if ($loadCalls === 1) {
                return Http::response(['projects' => []], 200);
            }

            return Http::response(['projects' => [
                ['id' => '8001', 'src' => 'rt', 'name' => '79993334455', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79993334455'],
                ['id' => '8002', 'src' => 'bl', 'name' => '79993334455', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79993334455'],
                ['id' => '8003', 'src' => 'mt', 'name' => '79993334455', 'tag' => 'РФ', 'type' => 'calls', 'content' => '79993334455'],
            ]], 200);
        },
    ]);

    (new SyncSupplierProjectsJob)->handle(app(AjaxProjectChannel::class));

    $sps = SupplierProject::on('pgsql_supplier')->where('unique_key', '79993334455')->orderBy('platform')->get();
    expect($sps)->toHaveCount(3);
    expect($sps->pluck('supplier_external_id')->all())->toBe(['8001', '8002', '8003']);
});
