<?php

declare(strict_types=1);

namespace Tests\Feature\Pwa;

use App\Enums\ExpenseCategory;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * El cobrador ve lo que entregó y lo que gastó, no solo lo que cobró.
 *
 * Su panel mostraba el dinero que entraba pero nada del que salía, y los gastos
 * que registraba desaparecían de su vista: el desglose vive en /pwa/financials,
 * que es solo de admin. Registraba un gasto y no volvía a saber de él — ni
 * siquiera si se lo habían aprobado.
 *
 * Lo que estos tests protegen sobre todo es la FRONTERA: cada cobrador ve lo
 * suyo. Un panel que sume lo de otro no es un error de presentación, es una
 * fuga.
 */
class PanelCobradorGastosYDesembolsosTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $collector;

    private User $otroCollector;

    protected function setUp(): void
    {
        parent::setUp();

        Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);

        $this->company = Company::factory()->create();

        Subscription::factory()->create([
            'company_id' => $this->company->id,
            'plan_id' => Plan::factory()->create()->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addYear(),
        ]);

        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');

        $this->otroCollector = User::factory()->create(['company_id' => $this->company->id]);
        $this->otroCollector->assignRole('collector');
    }

    private function credito(User $cobrador, string $fecha, float $monto): Credit
    {
        return Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => Client::factory()->create(['company_id' => $this->company->id])->id,
            'collector_user_id' => $cobrador->id,
            'status' => Credit::STATUS_ACTIVE,
            'start_date' => $fecha,
            'amount' => $monto,
        ]);
    }

    /**
     * `Expense` no tiene factory; se crea a mano con lo mínimo que exige la
     * tabla. `approval_status` no es mass-assignable a propósito (ver el modelo),
     * así que se asigna aparte.
     */
    private function gasto(User $quien, string $fecha, float $monto, string $estado = 'pending'): Expense
    {
        $gasto = Expense::create([
            'company_id' => $this->company->id,
            'user_id' => $quien->id,
            'operation_date' => $fecha,
            'amount' => $monto,
            'category' => ExpenseCategory::TRANSPORTATION->value,
            'expense_type' => 'operational',
            'affects_cash' => true,
        ]);

        $gasto->approval_status = $estado;
        $gasto->requires_approval = true;
        $gasto->save();

        return $gasto;
    }

    /** @return array<string, mixed> */
    private function panel(): array
    {
        return $this->actingAs($this->collector, 'sanctum')
            ->getJson('/api/pwa/dashboard')
            ->assertOk()
            ->json('stats');
    }

    #[Test]
    public function el_panel_suma_lo_que_el_cobrador_desembolso_hoy(): void
    {
        $this->credito($this->collector, now()->toDateString(), 300000);
        $this->credito($this->collector, now()->toDateString(), 200000);

        // De ayer: no cuenta para hoy.
        $this->credito($this->collector, now()->subDay()->toDateString(), 900000);

        // De otro cobrador: no es suyo.
        $this->credito($this->otroCollector, now()->toDateString(), 700000);

        $stats = $this->panel();

        $this->assertSame(500000.0, (float) $stats['disbursed_today']['amount']);
        $this->assertSame(2, $stats['disbursed_today']['count']);
    }

    #[Test]
    public function el_panel_suma_los_gastos_que_registro_hoy_y_avisa_de_los_pendientes(): void
    {
        $this->gasto($this->collector, now()->toDateString(), 40000, 'pending');
        $this->gasto($this->collector, now()->toDateString(), 10000, 'approved');
        $this->gasto($this->collector, now()->subDay()->toDateString(), 99000, 'pending');
        $this->gasto($this->otroCollector, now()->toDateString(), 88000, 'pending');

        $stats = $this->panel();

        $this->assertSame(50000.0, (float) $stats['expenses_today']['amount']);
        $this->assertSame(2, $stats['expenses_today']['count']);

        // Lo que de verdad le preocupa: cuántos siguen sin respuesta.
        $this->assertSame(1, $stats['expenses_today']['pending']);
    }

    /**
     * Lo que escribe el sistema al desembolsar: un gasto de categoría
     * `disbursement` a nombre de quien lo hizo. Misma forma que en
     * `CreditOperationService`.
     */
    private function desembolsoRegistrado(Credit $credito, User $quien): Expense
    {
        return Expense::create([
            'company_id' => $this->company->id,
            'user_id' => $quien->id,
            'collector_user_id' => $credito->collector_user_id,
            'amount' => $credito->amount,
            'credit_id' => $credito->id,
            'category' => 'disbursement',
            // Lo que deduce `FinanceAutoLogger::resolveExpenseType()`.
            'expense_type' => 'disbursement',
            'operation_date' => $credito->start_date,
            'affects_cash' => true,
            'notes' => 'Desembolso automático del crédito',
        ]);
    }

    #[Test]
    public function un_desembolso_no_cuenta_como_gasto(): void
    {
        // Al desembolsar, el sistema registra un gasto de categoría
        // `disbursement`. El panel del cobrador lo sumaba TAMBIÉN en "Gastos
        // hoy", así que a quien solo había desembolsado le salían dos tarjetas
        // con el mismo dinero, contado dos veces.
        $credito = $this->credito($this->collector, now()->toDateString(), 300000);
        $this->desembolsoRegistrado($credito, $this->collector);

        $this->gasto($this->collector, now()->toDateString(), 15000, 'pending');

        $stats = $this->panel();

        $this->assertSame(300000.0, (float) $stats['disbursed_today']['amount']);

        // Solo el gasto de verdad: el desembolso ya tiene su propia cifra.
        $this->assertSame(15000.0, (float) $stats['expenses_today']['amount']);
        $this->assertSame(1, $stats['expenses_today']['count']);
    }

    #[Test]
    public function mis_gastos_no_lista_los_desembolsos(): void
    {
        $credito = $this->credito($this->collector, now()->toDateString(), 300000);
        $this->desembolsoRegistrado($credito, $this->collector);
        $real = $this->gasto($this->collector, now()->toDateString(), 15000);

        $respuesta = $this->actingAs($this->collector, 'sanctum')
            ->getJson('/api/pwa/expenses/mine')
            ->assertOk();

        $this->assertSame([$real->id], array_column($respuesta->json('data'), 'id'));
        $this->assertSame(15000.0, (float) $respuesta->json('totals.amount'));
    }

    #[Test]
    public function mis_gastos_devuelve_solo_los_propios(): void
    {
        $mio = $this->gasto($this->collector, now()->toDateString(), 40000);
        $this->gasto($this->otroCollector, now()->toDateString(), 88000);

        $respuesta = $this->actingAs($this->collector, 'sanctum')
            ->getJson('/api/pwa/expenses/mine')
            ->assertOk();

        $ids = array_column($respuesta->json('data'), 'id');

        $this->assertSame([$mio->id], $ids);
        $this->assertSame(40000.0, (float) $respuesta->json('totals.amount'));
    }

    #[Test]
    public function mis_gastos_devuelve_la_clave_de_cada_gasto(): void
    {
        // Si la respuesta de un gasto se perdió, está en el servidor y en la cola
        // del teléfono a la vez hasta la próxima ronda. La PWA los empareja por
        // esta clave para no mostrarlo como "sin subir" cuando ya subió.
        $conClave = $this->gasto($this->collector, now()->toDateString(), 15000);
        $conClave->idempotency_key = $clave = (string) Str::uuid();
        $conClave->save();
        $sinClave = $this->gasto($this->collector, now()->toDateString(), 8000);

        $porId = collect(
            $this->actingAs($this->collector, 'sanctum')
                ->getJson('/api/pwa/expenses/mine')
                ->assertOk()
                ->json('data')
        )->keyBy('id');

        $this->assertSame($clave, $porId[$conClave->id]['idempotency_key']);
        // Los gastos del panel o de antes del lote no tienen clave.
        $this->assertArrayHasKey('idempotency_key', $porId[$sinClave->id]);
        $this->assertNull($porId[$sinClave->id]['idempotency_key']);
    }

    #[Test]
    public function mis_gastos_respeta_el_periodo_pedido(): void
    {
        // Fecha fija a mitad de semana y de mes: un lunes, startOfWeek() es hoy
        // y el gasto "de esta semana" caía también en "hoy" (fallaba cada lunes).
        $this->travelTo(Carbon::parse('2026-09-16 12:00:00'));

        $this->gasto($this->collector, now()->toDateString(), 10000);
        $this->gasto($this->collector, now()->startOfWeek()->toDateString(), 20000);
        $this->gasto($this->collector, now()->subMonths(2)->toDateString(), 50000);

        $hoy = $this->actingAs($this->collector, 'sanctum')
            ->getJson('/api/pwa/expenses/mine?period=today')->assertOk();
        $this->assertSame(10000.0, (float) $hoy->json('totals.amount'));

        $mes = $this->actingAs($this->collector, 'sanctum')
            ->getJson('/api/pwa/expenses/mine?period=month')->assertOk();

        // El de hace dos meses queda fuera; los de este mes entran.
        $this->assertGreaterThanOrEqual(10000.0, (float) $mes->json('totals.amount'));
        $this->assertLessThan(50000.0, (float) $mes->json('totals.amount'));
    }

    #[Test]
    public function mis_gastos_exige_sesion(): void
    {
        $this->getJson('/api/pwa/expenses/mine')->assertUnauthorized();
    }
}
