<?php

declare(strict_types=1);

use App\Models\ActivityLog;
use App\Models\Deal;
use App\Models\Project;
use App\Models\SupplierLeadCost;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\IOFactory;

uses(DatabaseTransactions::class);

beforeEach(function () {
    $this->tenant = Tenant::factory()->create([
        'balance_leads' => 100,
    ]);
});

test('POST /api/deals создаёт сделку с manual source + project firstOrCreate', function () {
    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'Окна Москва',
        'phone' => '+7 (999) 123-45-67',
        'contact_name' => 'Тест Тестов',
        'status' => 'new',
    ]);

    $r->assertStatus(201);
    expect($r->json('deal.id'))->toBeInt();
    expect($r->json('deal.tenant_id'))->toBe($this->tenant->id);
    expect($r->json('deal.phone'))->toBe('+7 (999) 123-45-67');
    expect($r->json('deal.status'))->toBe('new');

    $dealId = $r->json('deal.id');
    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $deal = Deal::query()->where('id', $dealId)->first();
    expect($deal)->not->toBeNull();
    expect($deal->source_crm_id)->toBeNull(); // manual
    expect($deal->contact_name)->toBe('Тест Тестов');

    // Project создан с type='manual'
    $project = Project::find($r->json('deal.project_id'));
    expect($project->name)->toBe('Окна Москва');
    expect($project->type)->toBe('manual');
});

test('POST /api/deals использует существующий project (не дублирует)', function () {
    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $existing = Project::create([
        'tenant_id' => $this->tenant->id,
        'name' => 'Натяжные потолки',
        'type' => 'webhook',
    ]);

    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'Натяжные потолки',
        'phone' => '+7 (999) 000-00-00',
    ]);

    $r->assertStatus(201);
    expect($r->json('deal.project_id'))->toBe($existing->id);

    // Проверяем что НЕТ нового project'а с таким же name
    $count = Project::where('tenant_id', $this->tenant->id)
        ->where('name', 'Натяжные потолки')
        ->count();
    expect($count)->toBe(1);
});

test('POST /api/deals пишет ActivityLog с context.source=manual', function () {
    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 000-00-00',
    ]);
    $r->assertStatus(201);

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $log = ActivityLog::where('deal_id', $r->json('deal.id'))->first();
    expect($log)->not->toBeNull();
    expect($log->event)->toBe(ActivityLog::EVENT_DEAL_CREATED);
    expect($log->context)->toBe(['source' => 'manual']);
});

test('POST /api/deals 422 без обязательных полей', function () {
    $r = $this->postJson('/api/deals', []);
    $r->assertStatus(422);
    expect($r->json('errors'))->toHaveKeys(['tenant_id', 'project_name', 'phone']);
});

test('POST /api/deals 404 при unknown tenant_id', function () {
    $r = $this->postJson('/api/deals', [
        'tenant_id' => 999999,
        'project_name' => 'X',
        'phone' => '+7 (999) 000-00-00',
    ]);
    $r->assertStatus(404);
});

test('POST /api/deals дефолтный status = new если не передан', function () {
    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 000-00-00',
    ]);
    $r->assertStatus(201);
    expect($r->json('deal.status'))->toBe('new');
});

test('POST /api/deals с manager_id → assigned_at = NOW()', function () {
    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $manager = User::factory()->for($this->tenant)->create(['is_active' => true]);

    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 000-00-00',
        'manager_id' => $manager->id,
    ]);
    $r->assertStatus(201);

    $deal = Deal::where('id', $r->json('deal.id'))->first();
    expect($deal->manager_id)->toBe($manager->id);
    expect($deal->assigned_at)->not->toBeNull();
});

test('POST /api/deals manual НЕ списывает баланс tenant\'а', function () {
    $balanceBefore = $this->tenant->balance_leads;

    $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 000-00-00',
    ])->assertStatus(201);

    $this->tenant->refresh();
    expect($this->tenant->balance_leads)->toBe($balanceBefore);
});

test('POST /api/deals manual создаёт SupplierLeadCost если у проекта есть активный supplier', function () {
    // Создаём supplier + проект + project_suppliers связку.
    $supplierId = DB::table('suppliers')->insertGetId([
        'code' => 'test_b1_'.bin2hex(random_bytes(3)),
        'name' => 'Test Supplier',
        'accepts_types' => '{"websites","calls"}',
        'cost_rub' => '15.00',
        'channel' => 'sites',
        'is_active' => true,
        'sort_order' => 1,
        'quality_score' => 1.00,
        'created_at' => now(),
    ]);

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $project = Project::create([
        'tenant_id' => $this->tenant->id,
        'name' => 'WithSupplier',
        'type' => 'manual',
    ]);
    DB::table('project_suppliers')->insert([
        'project_id' => $project->id,
        'supplier_id' => $supplierId,
        'is_active' => true,
        'created_at' => now(),
    ]);

    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'WithSupplier',
        'phone' => '+7 (999) 000-00-00',
    ]);
    $r->assertStatus(201);

    // SupplierLeadCost создан со snapshot cost_rub
    $cost = SupplierLeadCost::query()
        ->where('deal_id', $r->json('deal.id'))
        ->first();
    expect($cost)->not->toBeNull();
    expect($cost->supplier_id)->toBe((int) $supplierId);
    expect((string) $cost->cost_rub)->toBe('15.00');
    expect($cost->supplier_lead_id)->toBeNull(); // manual: нет внешнего id
});

test('POST /api/deals manual БЕЗ supplier'."'а у проекта — без SupplierLeadCost (graceful skip)", function () {
    $r = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'NoSupplier',
        'phone' => '+7 (999) 000-00-00',
    ]);
    $r->assertStatus(201);

    DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
    $cost = SupplierLeadCost::query()
        ->where('deal_id', $r->json('deal.id'))
        ->count();
    expect($cost)->toBe(0);
});

test('POST /api/deals/export возвращает CSV с правильными headers + BOM', function () {
    // Создаём 2 сделки через store endpoint (получаем реальные id).
    $r1 = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 111-11-11',
        'contact_name' => 'Алиса',
    ])->json('deal');
    $r2 = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 222-22-22',
        'contact_name' => 'Боб',
    ])->json('deal');

    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [$r1['id'], $r2['id']],
    ]);

    $r->assertStatus(200);
    expect($r->headers->get('Content-Type'))->toContain('text/csv');
    expect($r->headers->get('Content-Disposition'))->toContain('deals_export_');

    // Sprint 3 Phase A (O-perf-05): export → StreamedResponse через OpenSpout,
    // body читается через streamedContent() (см. TestResponse::streamedContent).
    $body = $r->streamedContent();
    // BOM первый символ
    expect($body)->toStartWith("\u{FEFF}");
    // Headers строка
    expect($body)->toContain('ID;Имя;Телефон;Статус');
    // Контент сделок
    expect($body)->toContain('Алиса');
    expect($body)->toContain('Боб');
    expect($body)->toContain('+7 (999) 111-11-11');
});

test('POST /api/deals/export 422 без ids', function () {
    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
    ]);
    $r->assertStatus(422);
    expect($r->json('errors'))->toHaveKey('ids');
});

test('POST /api/deals/export 404 unknown tenant', function () {
    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => 999999,
        'ids' => [1, 2, 3],
    ]);
    $r->assertStatus(404);
});

test('POST /api/deals/export фильтрует только запрошенные ids (своего tenant\'а)', function () {
    // Создаём 3 сделки одного tenant'а, экспортируем 1 → CSV только её.
    $a = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 111-11-11',
        'contact_name' => 'Алиса',
    ])->json('deal');
    $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 222-22-22',
        'contact_name' => 'Боб',
    ])->json('deal');

    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [$a['id']],
    ]);
    $r->assertStatus(200);
    $body = $r->streamedContent();
    expect($body)->toContain('Алиса');
    expect($body)->not->toContain('Боб');
});

// NB: полная RLS-изоляция (другие tenant'ы скрыты) тестируется отдельно
// через testing_rls_user (NOLOGIN role без BYPASSRLS) — см.
// `tests/Feature/RlsSmokeTest.php` v1.10. В этом тесте используется postgres
// superuser, который BYPASSRLS — RLS-проверка тут была бы false-positive.

test('POST /api/deals/export?format=xlsx возвращает binary с корректным content-type', function () {
    $a = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 111-11-11',
        'contact_name' => 'Алиса',
    ])->json('deal');

    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [$a['id']],
        'format' => 'xlsx',
    ]);

    $r->assertStatus(200);
    expect($r->headers->get('Content-Type'))
        ->toBe('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    expect($r->headers->get('Content-Disposition'))->toContain('.xlsx');
    // XLSX = ZIP-archive, начинается с magic bytes "PK\x03\x04".
    $body = $r->streamedContent();
    expect(substr($body, 0, 4))->toBe("PK\x03\x04");
    expect(strlen($body))->toBeGreaterThan(2000); // sanity: реальный xlsx > 2 KB
});

test('POST /api/deals/export?format=xlsx содержит данные сделки (после распаковки sheet1)', function () {
    $a = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 333-33-33',
        'contact_name' => 'Кириллов',
    ])->json('deal');

    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [$a['id']],
        'format' => 'xlsx',
    ]);

    $tmp = tempnam(sys_get_temp_dir(), 'xlsx_test_');
    file_put_contents($tmp, $r->streamedContent());
    $reader = IOFactory::createReader('Xlsx');
    $book = $reader->load($tmp);
    $sheet = $book->getActiveSheet();

    expect($sheet->getTitle())->toBe('Сделки');
    // Sprint 3 Phase A (O-perf-05): после миграции на OpenSpout streaming,
    // styled-header cells пишутся как inline-string с RichText. Используем
    // getFormattedValue() для plain-string сравнения header'ов; для data-cell'ов
    // OpenSpout продолжает писать обычные shared-strings.
    expect($sheet->getCell('A1')->getFormattedValue())->toBe('ID');
    expect($sheet->getCell('B1')->getFormattedValue())->toBe('Имя');
    expect($sheet->getStyle('A1')->getFont()->getBold())->toBeTrue();
    // Row 2 — реальная сделка. OpenSpout пишет string-cell'ы как inline-string с
    // RichText-обёрткой; для plain-string сравнения используем getFormattedValue().
    // Numeric cell A2 (ID) — обычный numeric, ->getValue() работает.
    expect($sheet->getCell('A2')->getValue())->toBe($a['id']);
    expect($sheet->getCell('B2')->getFormattedValue())->toBe('Кириллов');
    expect($sheet->getCell('C2')->getFormattedValue())->toBe('+7 (999) 333-33-33');

    unlink($tmp);
});

test('POST /api/deals/export 422 на неизвестный format', function () {
    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [1],
        'format' => 'pdf',
    ]);
    $r->assertStatus(422);
    expect($r->json('errors'))->toHaveKey('format');
});

test('POST /api/deals/export по умолчанию (без format) возвращает CSV — backward-compat', function () {
    $a = $this->postJson('/api/deals', [
        'tenant_id' => $this->tenant->id,
        'project_name' => 'X',
        'phone' => '+7 (999) 444-44-44',
        'contact_name' => 'Test',
    ])->json('deal');

    $r = $this->postJson('/api/deals/export', [
        'tenant_id' => $this->tenant->id,
        'ids' => [$a['id']],
    ]);
    $r->assertStatus(200);
    expect($r->headers->get('Content-Type'))->toContain('text/csv');
    expect($r->headers->get('Content-Disposition'))->toContain('.csv');
});
