<?php

declare(strict_types=1);

use App\Models\Deal;
use App\Models\LeadCharge;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;

/**
 * Тесты GET /api/deals — list-endpoint для DealsView/KanbanView (замена MOCK_DEALS).
 *
 * Покрывает: фильтры (status_in, project_id, manager_id, search), сортировку
 * по received_at DESC, RLS-изоляцию между tenant'ами, относительные поля
 * (project_name, manager_name/initials), 401/404, пагинацию (limit/offset).
 */
uses(DatabaseTransactions::class);

beforeEach(function () {
    $this->tenant = Tenant::factory()->create();
    $this->otherTenant = Tenant::factory()->create();

    $this->user = User::factory()->for($this->tenant)->create();
    $this->actingAs($this->user);

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $this->project = Project::factory()->for($this->tenant)->create(['name' => 'Окна Москва']);
    $this->project2 = Project::factory()->for($this->tenant)->create(['name' => 'Натяжные потолки']);

    $this->manager = User::factory()->for($this->tenant)->create([
        'first_name' => 'Иван',
        'last_name' => 'Петров',
        'email' => 'ivan@example.test',
    ]);
});

test('GET /api/deals возвращает 401 без auth', function () {
    auth()->logout();
    $this->getJson('/api/deals')->assertStatus(401);
});

test('GET /api/deals возвращает пустой список для tenant без сделок', function () {
    $r = $this->getJson('/api/deals');

    $r->assertStatus(200)
        ->assertJson(['deals' => [], 'total' => 0, 'limit' => 100, 'offset' => 0]);
});

test('GET /api/deals возвращает сделки tenant\'а с проектом и менеджером', function () {
    $deal = Deal::factory()
        ->for($this->tenant)
        ->for($this->project)
        ->create([
            'phone' => '+7 (999) 111-11-11',
            'contact_name' => 'Анна С.',
            'status' => 'new',
            'manager_id' => $this->manager->id,
        ]);

    $r = $this->getJson('/api/deals');

    $r->assertStatus(200);
    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.id'))->toBe($deal->id);
    expect($r->json('deals.0.phone'))->toBe('+7 (999) 111-11-11');
    expect($r->json('deals.0.project_name'))->toBe('Окна Москва');
    expect($r->json('deals.0.manager_name'))->toBe('Иван П.');
    expect($r->json('deals.0.manager_initials'))->toBe('ИП');
    expect($r->json('deals.0.contact_name'))->toBe('Анна С.');
    expect($r->json('deals.0.received_at'))->toBeString();
});

test('GET /api/deals не возвращает сделки чужого tenant\'а (RLS)', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);

    DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
    $foreignProject = Project::factory()->for($this->otherTenant)->create();
    Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(['status' => 'new']);

    $r = $this->getJson('/api/deals');

    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.tenant_id'))->toBe($this->tenant->id);
});

test('GET /api/deals сортирует по received_at DESC', function () {
    $oldest = Deal::factory()->for($this->tenant)->for($this->project)->create([
        'received_at' => now()->subHours(3),
    ]);
    $newest = Deal::factory()->for($this->tenant)->for($this->project)->create([
        'received_at' => now()->subMinutes(1),
    ]);
    $middle = Deal::factory()->for($this->tenant)->for($this->project)->create([
        'received_at' => now()->subHours(1),
    ]);

    $r = $this->getJson('/api/deals');

    expect($r->json('deals.0.id'))->toBe($newest->id);
    expect($r->json('deals.1.id'))->toBe($middle->id);
    expect($r->json('deals.2.id'))->toBe($oldest->id);
});

test('GET /api/deals фильтрует по status_in[]', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'won']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'lost']);

    $r = $this->getJson('/api/deals?status_in[]=new&status_in[]=won');

    expect($r->json('total'))->toBe(2);
    $statuses = collect($r->json('deals'))->pluck('status')->sort()->values()->all();
    expect($statuses)->toBe(['new', 'won']);
});

test('GET /api/deals фильтрует по project_id', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create();
    Deal::factory()->for($this->tenant)->for($this->project)->create();
    Deal::factory()->for($this->tenant)->for($this->project2)->create();

    $r = $this->getJson('/api/deals?project_id='.$this->project2->id);

    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.project_name'))->toBe('Натяжные потолки');
});

test('GET /api/deals фильтрует по manager_id', function () {
    $other = User::factory()->for($this->tenant)->create();

    Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $this->manager->id]);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $other->id]);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]);

    $r = $this->getJson('/api/deals?manager_id='.$this->manager->id);

    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.manager_id'))->toBe($this->manager->id);
});

test('GET /api/deals фильтрует по search (phone + contact_name, ILIKE)', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create([
        'phone' => '+7 (999) 111-11-11',
        'contact_name' => 'Анна Соколова',
    ]);
    Deal::factory()->for($this->tenant)->for($this->project)->create([
        'phone' => '+7 (903) 222-22-22',
        'contact_name' => 'Дмитрий Петров',
    ]);

    expect($this->getJson('/api/deals?search=Соколова')
        ->json('total'))->toBe(1);

    expect($this->getJson('/api/deals?search=903')
        ->json('total'))->toBe(1);

    expect($this->getJson('/api/deals?search=сокол') // case-insensitive ILIKE
        ->json('total'))->toBe(1);
});

test('GET /api/deals поддерживает limit + offset', function () {
    foreach (range(1, 5) as $i) {
        Deal::factory()->for($this->tenant)->for($this->project)->create([
            'received_at' => now()->subMinutes($i),
        ]);
    }

    $r = $this->getJson('/api/deals?limit=2&offset=1');

    expect($r->json('total'))->toBe(5);
    expect($r->json('limit'))->toBe(2);
    expect($r->json('offset'))->toBe(1);
    expect(count($r->json('deals')))->toBe(2);
});

test('GET /api/deals?only_deleted=true возвращает только soft-deleted', function () {
    $alive = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $deleted1 = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $deleted2 = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $deleted1->delete();
    $deleted2->delete();

    $r = $this->getJson('/api/deals?only_deleted=true');

    expect($r->json('total'))->toBe(2);
    $ids = collect($r->json('deals'))->pluck('id')->all();
    expect($ids)->not->toContain($alive->id);
    expect($ids)->toContain($deleted1->id);
    expect($ids)->toContain($deleted2->id);
});

test('GET /api/deals (без only_deleted) НЕ возвращает soft-deleted (default)', function () {
    $alive = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $deleted = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $deleted->delete();

    $r = $this->getJson('/api/deals');

    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.id'))->toBe($alive->id);
});

test('GET /api/deals?only_deleted=true изолирует чужие удалённые сделки', function () {
    DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
    $foreignProject = Project::factory()->for($this->otherTenant)->create();
    $foreign = Deal::factory()->for($this->otherTenant)->for($foreignProject)->create();
    $foreign->delete();

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $own = Deal::factory()->for($this->tenant)->for($this->project)->create();
    $own->delete();

    $r = $this->getJson('/api/deals?only_deleted=true');

    expect($r->json('total'))->toBe(1);
    expect($r->json('deals.0.id'))->toBe($own->id);
});

test('GET /api/deals возвращает manager_name/initials = null если manager_id null', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]);

    $r = $this->getJson('/api/deals');

    expect($r->json('deals.0.manager_id'))->toBeNull();
    expect($r->json('deals.0.manager_name'))->toBeNull();
    expect($r->json('deals.0.manager_initials'))->toBeNull();
});

// Sprint 4 Phase A (audit O-perf-04): keyset pagination через cursor.

test('GET /api/deals с cursor возвращает следующую страницу через keyset', function () {
    // 5 сделок с received_at через 1 минуту (фиксируем порядок).
    $base = now()->subHours(5);
    $ids = [];
    for ($i = 0; $i < 5; $i++) {
        $ids[] = Deal::factory()
            ->for($this->tenant)
            ->for($this->project)
            ->create([
                'status' => 'new',
                'received_at' => $base->copy()->addMinutes($i),
            ])->id;
    }

    // Первая страница без cursor: limit=2 → последние 2 (по received_at DESC).
    $r1 = $this->getJson('/api/deals?limit=2');
    $r1->assertStatus(200);
    expect($r1->json('deals'))->toHaveLength(2);
    expect($r1->json('deals.0.id'))->toBe($ids[4]);
    expect($r1->json('deals.1.id'))->toBe($ids[3]);

    // Cursor для следующей страницы — base64 от {r:received_at,i:id} последнего элемента.
    $cursor = base64_encode((string) json_encode([
        'r' => $r1->json('deals.1.received_at'),
        'i' => $r1->json('deals.1.id'),
    ]));

    $r2 = $this->getJson('/api/deals?limit=2&cursor='.$cursor);
    $r2->assertStatus(200);
    expect($r2->json('deals'))->toHaveLength(2);
    expect($r2->json('deals.0.id'))->toBe($ids[2]);
    expect($r2->json('deals.1.id'))->toBe($ids[1]);
});

test('GET /api/deals с невалидным cursor возвращает 422', function () {
    $r = $this->getJson('/api/deals?cursor=not-base64-json');
    $r->assertStatus(422);
    expect($r->json('message'))->toBeString();
});

test('GET /api/deals возвращает next_cursor когда есть ещё страницы', function () {
    $base = now()->subHours(3);
    for ($i = 0; $i < 3; $i++) {
        Deal::factory()->for($this->tenant)->for($this->project)->create([
            'status' => 'new',
            'received_at' => $base->copy()->addMinutes($i),
        ]);
    }

    $r = $this->getJson('/api/deals?limit=2');
    $r->assertStatus(200);
    expect($r->json('next_cursor'))->toBeString();
    expect($r->json('next_cursor'))->not->toBeEmpty();

    // Последняя страница: next_cursor = null.
    $cursor = $r->json('next_cursor');
    $r2 = $this->getJson('/api/deals?limit=2&cursor='.$cursor);
    $r2->assertStatus(200);
    expect($r2->json('next_cursor'))->toBeNull();
});

test('GET /api/deals?count_only=1 возвращает только total без массива deals', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'won']);

    $r = $this->getJson('/api/deals?count_only=1');

    $r->assertStatus(200);
    expect($r->json('total'))->toBe(2);
    expect($r->json('deals'))->toBeNull();
});

test('GET /api/deals?count_only=1 учитывает фильтры (status_in)', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'won']);

    expect($this->getJson('/api/deals?count_only=1&status_in[]=new')->json('total'))->toBe(2);
});

test('GET /api/deals?count_only=1 изолирует чужой tenant (RLS)', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);

    DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
    $foreignProject = Project::factory()->for($this->otherTenant)->create();
    Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(['status' => 'new']);

    expect($this->getJson('/api/deals?count_only=1')->json('total'))->toBe(1);
});

test('GET /api/deals фильтрует по received_from/received_to', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-10 12:00:00']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-15 12:00:00']);
    Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-20 12:00:00']);

    $r = $this->getJson('/api/deals?received_from=2026-05-12&received_to=2026-05-16');

    expect($r->json('total'))->toBe(1);
});

test('GET /api/deals received_to включает весь день (конец дня)', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-16 23:30:00']);

    expect($this->getJson('/api/deals?received_to=2026-05-16')->json('total'))->toBe(1);
});

test('GET /api/deals возвращает comment/city/project_signal_type', function () {
    $this->project->update(['signal_type' => 'call', 'signal_identifier' => '79990001122']);
    $deal = Deal::factory()->for($this->tenant)->for($this->project)->create([
        'comment' => 'перезвонить',
        'city' => 'Казань',
    ]);

    $r = $this->getJson('/api/deals');

    expect($r->json('deals.0.comment'))->toBe('перезвонить');
    expect($r->json('deals.0.city'))->toBe('Казань');
    expect($r->json('deals.0.project_signal_type'))->toBe('call');
});

test('GET /api/deals возвращает 422 на невалидную received_from', function () {
    $this->getJson('/api/deals?received_from=не-дата')->assertStatus(422);
});

test('GET /api/deals возвращает 422 на невалидную received_to', function () {
    $this->getJson('/api/deals?received_to=garbage')->assertStatus(422);
});

/* ----------------------------------------------------------------------
 * U4 (UX-аудит 25.06): цена лида должна быть видна и в листинге, не только
 * в детальном show — иначе панель и канбан показывают «—» при реальном списании.
 * -------------------------------------------------------------------- */

test('GET /api/deals отдаёт cost_kopecks из lead_charges для rub-списания', function () {
    $deal = Deal::factory()->for($this->tenant)->for($this->project)->create();

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    LeadCharge::create([
        'tenant_id' => $this->tenant->id,
        'deal_id' => $deal->id,
        'deal_received_at' => $deal->received_at,
        'tier_no' => 1,
        'charge_source' => 'rub',
        'price_per_lead_kopecks' => 7000,
        'charged_at' => now(),
        'created_at' => now(),
    ]);

    $r = $this->getJson('/api/deals');

    $r->assertStatus(200);
    expect($r->json('deals.0.cost_kopecks'))->toBe(7000);
});

test('GET /api/deals cost_kopecks = null если списания нет', function () {
    Deal::factory()->for($this->tenant)->for($this->project)->create();

    $r = $this->getJson('/api/deals');

    $r->assertStatus(200);
    expect($r->json('deals.0.cost_kopecks'))->toBeNull();
});
