<?php

declare(strict_types=1);

use App\Exceptions\Supplier\SupplierAuthException;
use App\Exceptions\Supplier\SupplierClientException;
use App\Exceptions\Supplier\SupplierTransientException;
use App\Jobs\Supplier\RefreshSupplierSessionJob;
use App\Services\Supplier\Dto\SupplierProjectDto;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
use Tests\Unit\Supplier\Stubs\ThrowingRefreshSupplierSessionJob;

uses(TestCase::class);

beforeEach(function (): void {
    Cache::store('redis')->put('supplier:session', [
        'phpsessid' => 'abc123sessid',
        'csrf' => 'csrf-token-xyz',
        'refreshed_at' => now()->toIso8601String(),
    ], now()->addHours(6));

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

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

test('listProjects attaches PHPSESSID cookie and X-CSRF-Token header', function (): void {
    Http::fake([
        // URL клиента — /admin/visit/rt-projects-load (см. SupplierPortalClient::listProjects);
        // старый паттерн без /visit/ не матчил → запрос уходил в неподходящий fallback-fake.
        'crm.bp-gr.ru/admin/visit/rt-projects-load*' => Http::response('[]', 200),
    ]);

    app(SupplierPortalClient::class)->listProjects();

    Http::assertSent(function ($request): bool {
        $cookieHeader = $request->header('Cookie')[0] ?? '';
        $csrfHeader = $request->header('X-CSRF-Token')[0] ?? '';

        return str_contains($cookieHeader, 'PHPSESSID=abc123sessid')
            && $csrfHeader === 'csrf-token-xyz';
    });
});

test('401 triggers RefreshSupplierSessionJob sync and retries once', function (): void {
    Bus::fake([RefreshSupplierSessionJob::class]);

    Http::fakeSequence('crm.bp-gr.ru/*')
        ->push('Unauthorized', 401)
        ->push('[]', 200);

    app(SupplierPortalClient::class)->listProjects();

    Bus::assertDispatchedSync(RefreshSupplierSessionJob::class);
    Http::assertSentCount(2);
});

test('sticky 401 throws SupplierAuthException after one retry', function (): void {
    Bus::fake([RefreshSupplierSessionJob::class]);

    Http::fakeSequence('crm.bp-gr.ru/*')
        ->push('Unauthorized', 401)
        ->push('Still unauthorized', 401);

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierAuthException::class);
});

test('5xx response throws SupplierTransientException', function (): void {
    Http::fake(['crm.bp-gr.ru/*' => Http::response('upstream gateway error', 503)]);

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierTransientException::class);
});

test('4xx not 401/403 throws SupplierClientException', function (): void {
    Http::fake(['crm.bp-gr.ru/*' => Http::response('bad payload', 422)]);

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierClientException::class);
});

test('network error throws SupplierTransientException', function (): void {
    Http::fake(function (): void {
        throw new ConnectionException('Connection refused');
    });

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierTransientException::class);
});

test('saveProject POSTs to /admin/visit/rt-project-save with full payload and returns external id', function (): void {
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '99001'],
            200,
        ),
    ]);

    $dto = new SupplierProjectDto(
        platform: 'B1',
        signalType: 'site',
        uniqueKey: 'example.com',
        limit: 5,
        workdays: [1, 2, 3, 4, 5],
        regions: [],
        regionsReverse: false,
        status: 'active',
    );

    $id = app(SupplierPortalClient::class)->saveProject($dto);

    expect($id)->toBe(99001);
    // Verified live 2026-05-19 (Task 1 recon): тело Vuex-state c srcrt=true для B1.
    Http::assertSent(fn ($r): bool => $r->method() === 'POST'
        && str_ends_with($r->url(), '/admin/visit/rt-project-save')
        && ($r['name'] ?? null) === 'example.com'
        && ($r['type'] ?? null) === 'hosts'
        && ($r['srcrt'] ?? null) === true
        && ($r['id'] ?? null) === 0);
});

test('updateProject POSTs to /admin/visit/rt-project-save with id + full payload', function (): void {
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-save' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null, 'id' => '12345'],
            200,
        ),
    ]);

    $dto = new SupplierProjectDto(
        platform: 'B2',
        signalType: 'call',
        uniqueKey: '79991234567',
        limit: 10,
        workdays: [1, 2, 3],
        regions: [77],
        regionsReverse: false,
        status: 'active',
    );

    app(SupplierPortalClient::class)->updateProject(externalId: 12345, dto: $dto);

    // Update = тот же endpoint что save, но с id:N (verified 2026-05-19 recon).
    Http::assertSent(fn ($r): bool => $r->method() === 'POST'
        && str_ends_with($r->url(), '/admin/visit/rt-project-save')
        && ((int) ($r['id'] ?? 0)) === 12345
        && ($r['type'] ?? null) === 'calls'
        && ($r['srcbl'] ?? null) === true);
});

test('deleteProject POSTs to /admin/visit/rt-project-delete with id only', function (): void {
    Http::fake([
        'crm.bp-gr.ru/admin/visit/rt-project-delete' => Http::response(
            ['status' => 'OK', 'message' => '', 'result' => null],
            200,
        ),
    ]);

    app(SupplierPortalClient::class)->deleteProject(externalId: 12345);

    // ID идёт строкой в JSON-body (verified 2026-05-19 recon: {"id":"12345"}).
    Http::assertSent(fn ($r): bool => $r->method() === 'POST'
        && str_ends_with($r->url(), '/admin/visit/rt-project-delete')
        && ($r['id'] ?? null) === '12345');
});

test('malformed portal_url throws SupplierClientException, not auth path', function (): void {
    config(['services.supplier.portal_url' => '/no-scheme-no-host']);
    Http::fake();  // не должен быть вызван

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierClientException::class);
});

test('RefreshSupplierSessionJob throws during retry path translated to SupplierAuthException', function (): void {
    // Initial session valid → first request goes through → 401 → triggers refresh sync
    Http::fake(['crm.bp-gr.ru/*' => Http::response('Unauthorized', 401)]);

    // Override container binding — simulates Task 5 Playwright failure
    app()->bind(
        RefreshSupplierSessionJob::class,
        fn () => new ThrowingRefreshSupplierSessionJob('Simulated Playwright crash during retry'),
    );

    $caught = null;
    try {
        app(SupplierPortalClient::class)->listProjects();
    } catch (SupplierAuthException $e) {
        $caught = $e;
    }

    expect($caught)->toBeInstanceOf(SupplierAuthException::class);
    // Message MUST mention "refresh failed" — proves translation, not sticky-401 path
    expect($caught->getMessage())->toContain('Session refresh failed')
        ->and($caught->getPrevious())->toBeInstanceOf(RuntimeException::class)
        ->and($caught->getPrevious()?->getMessage())->toBe('Simulated Playwright crash during retry');
});

test('RefreshSupplierSessionJob throws during initial loadSession translated to SupplierAuthException', function (): void {
    // Force loadSession path: clear cache so request() enters loadSession() refresh branch
    Cache::store('redis')->forget('supplier:session');

    app()->bind(
        RefreshSupplierSessionJob::class,
        fn () => new ThrowingRefreshSupplierSessionJob('Simulated Playwright crash during loadSession'),
    );

    $caught = null;
    try {
        app(SupplierPortalClient::class)->listProjects();
    } catch (SupplierAuthException $e) {
        $caught = $e;
    }

    expect($caught)->toBeInstanceOf(SupplierAuthException::class);
    // Message MUST mention "loadSession" — proves translation from raw RuntimeException
    expect($caught->getMessage())->toContain('loadSession')
        ->and($caught->getPrevious())->toBeInstanceOf(RuntimeException::class)
        ->and($caught->getPrevious()?->getMessage())->toBe('Simulated Playwright crash during loadSession');
});

test('200 HTML login page triggers RefreshSupplierSessionJob sync and retries once', function (): void {
    Bus::fake([RefreshSupplierSessionJob::class]);

    Http::fakeSequence('crm.bp-gr.ru/*')
        ->push(
            '<html><body><form action="/login"><input id="loginform-username" name="LoginForm[username]"></form></body></html>',
            200,
            ['Content-Type' => 'text/html; charset=utf-8'],
        )
        ->push('{"projects":[]}', 200, ['Content-Type' => 'application/json']);

    app(SupplierPortalClient::class)->listProjects();

    Bus::assertDispatchedSync(RefreshSupplierSessionJob::class);
    Http::assertSentCount(2);
});

test('sticky HTML login page after retry throws SupplierAuthException', function (): void {
    Bus::fake([RefreshSupplierSessionJob::class]);

    Http::fakeSequence('crm.bp-gr.ru/*')
        ->push(
            '<html><input id="loginform-username"></html>',
            200,
            ['Content-Type' => 'text/html; charset=utf-8'],
        )
        ->push(
            '<html><input id="loginform-username"></html>',
            200,
            ['Content-Type' => 'text/html; charset=utf-8'],
        );

    expect(fn () => app(SupplierPortalClient::class)->listProjects())
        ->toThrow(SupplierAuthException::class, 'Portal returned login page after refresh');
});

test('JSON response with substring "loginform-username" is NOT misclassified as login page', function (): void {
    Http::fake([
        'crm.bp-gr.ru/*' => Http::response(
            '{"projects":[{"name":"loginform-username is just a string here"}]}',
            200,
            ['Content-Type' => 'application/json'],
        ),
    ]);

    $result = app(SupplierPortalClient::class)->listProjects();

    expect($result)->toHaveCount(1);
    Http::assertSentCount(1);  // no retry — JSON header skips login-detect
});

test('200 response without Content-Type header is NOT detected as login page', function (): void {
    // Документирует контракт: пустой Content-Type → str_starts_with('','text/html') === false → детект пропускается.
    Http::fake([
        'crm.bp-gr.ru/*' => Http::response('{"projects":[]}', 200),  // no Content-Type header
    ]);

    app(SupplierPortalClient::class)->listProjects();

    Http::assertSentCount(1);  // no retry — empty Content-Type fails the text/html gate
});
