<?php

declare(strict_types=1);

namespace Tests\Feature\Pwa;

use App\Models\Company;
use App\Models\Expense;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use App\Services\Metrics\CashFlowMetricsService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\Sanctum;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Estandarización de aprobación de egresos + gating de caja.
 *
 * REGLAS:
 *  - admin → egreso auto-aprobado (no requiere aprobación).
 *  - supervisor / collector → egreso PENDIENTE (requiere aprobación del admin).
 *  - La caja (getCashPosition / getCashBase / FinancialController) NO descuenta
 *    egresos pendientes ni rechazados; solo los EFECTIVOS (scopeEffective).
 *    Un egreso se descuenta de caja únicamente cuando el admin lo aprueba.
 */
class ExpenseApprovalCashGatingTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $admin;

    private User $supervisor;

    private User $collector;

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

        foreach (['admin', 'supervisor', 'collector'] as $role) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
        }

        $plan = Plan::factory()->create(['has_pwa_access' => true]);
        $this->company = Company::factory()->create();

        Subscription::factory()->active()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'ends_at' => now()->addYear(),
        ]);

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

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

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

    /** Inserta un egreso directo en DB para fijar approval_status sin disparar hooks. */
    private function insertExpense(array $attrs = []): int
    {
        return DB::table('expenses')->insertGetId(array_merge([
            'company_id' => $this->company->id,
            'user_id' => $this->admin->id,
            'amount' => '50000.00',
            'category' => 'transportation',
            'expense_type' => 'operational',
            'affects_cash' => true,
            'affects_profit' => true,
            'reduces_capital' => false,
            'is_locked' => false,
            'requires_approval' => false,
            'approval_status' => 'approved',
            'operation_date' => now()->toDateString(),
            'created_at' => now(),
            'updated_at' => now(),
        ], $attrs));
    }

    private function insertIncome(string $amount = '200000.00'): void
    {
        DB::table('incomes')->insert([
            'company_id' => $this->company->id,
            'user_id' => $this->admin->id,
            'amount' => $amount,
            'category' => 'service_fee',
            'operation_date' => now()->toDateString(),
            'affects_capital' => false,
            'affects_profit' => true,
            'is_locked' => false,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────
    // Aprobación por rol (POST /api/pwa/expenses)
    // ─────────────────────────────────────────────────────────────────────

    #[Test]
    public function collector_expense_is_pending_approval(): void
    {
        Sanctum::actingAs($this->collector->fresh());

        $response = $this->postJson('/api/pwa/expenses', [
            'amount' => 25000,
            'category' => 'transportation',
            'notes' => 'Combustible visita',
        ]);

        $response->assertCreated()
            ->assertJsonPath('expense.approval_status', 'pending')
            ->assertJsonPath('expense.requires_approval', true);

        $this->assertDatabaseHas('expenses', [
            'id' => $response->json('expense.id'),
            'requires_approval' => 1,
            'approval_status' => 'pending',
            'approved_by_user_id' => null,
        ]);
    }

    #[Test]
    public function supervisor_expense_is_pending_approval(): void
    {
        Sanctum::actingAs($this->supervisor->fresh());

        $this->postJson('/api/pwa/expenses', [
            'amount' => 30000,
            'category' => 'office_supplies',
        ])
            ->assertCreated()
            ->assertJsonPath('expense.approval_status', 'pending')
            ->assertJsonPath('expense.requires_approval', true);
    }

    #[Test]
    public function admin_expense_is_auto_approved(): void
    {
        Sanctum::actingAs($this->admin->fresh());

        $response = $this->postJson('/api/pwa/expenses', [
            'amount' => 40000,
            'category' => 'rent',
        ]);

        $response->assertCreated()
            ->assertJsonPath('expense.approval_status', 'approved')
            ->assertJsonPath('expense.requires_approval', false);

        $this->assertDatabaseHas('expenses', [
            'id' => $response->json('expense.id'),
            'requires_approval' => 0,
            'approval_status' => 'approved',
            'approved_by_user_id' => $this->admin->id,
        ]);
    }

    #[Test]
    public function bad_debt_writeoff_is_rejected_from_pwa_not_500(): void
    {
        // bad_debt_writeoff requiere credit_id (ExpenseCategory::requiresCreditId);
        // el hook del modelo lanzaba RuntimeException → 500. Ya no está en las
        // categorías permitidas, así que se rechaza en validación (422), no revienta.
        Sanctum::actingAs($this->admin->fresh());

        $this->postJson('/api/pwa/expenses', [
            'amount' => 50000,
            'category' => 'bad_debt_writeoff',
        ])
            ->assertStatus(422)
            ->assertJsonValidationErrors('category');

        $this->assertDatabaseMissing('expenses', ['category' => 'bad_debt_writeoff']);
    }

    #[Test]
    public function bad_debt_provision_is_still_allowed_from_pwa(): void
    {
        // bad_debt_provision NO requiere credit_id → sigue siendo un egreso válido.
        Sanctum::actingAs($this->admin->fresh());

        $this->postJson('/api/pwa/expenses', [
            'amount' => 15000,
            'category' => 'bad_debt_provision',
        ])
            ->assertCreated()
            ->assertJsonPath('expense.category', 'bad_debt_provision');
    }

    #[Test]
    public function approval_state_is_not_mass_assignable(): void
    {
        // Defensa en profundidad: ningún request puede auto-aprobar un egreso por
        // mass-assignment. approval_status/requires_approval solo se fijan por los
        // métodos del modelo o sus defaults; fill() debe ignorarlos.
        $expense = (new Expense)->fill([
            'approval_status' => 'pending',   // intento de valor NO-default
            'requires_approval' => true,      // intento de valor NO-default
        ]);

        $this->assertSame('approved', $expense->approval_status, 'approval_status no debe ser mass-assignable');
        $this->assertFalse((bool) $expense->requires_approval, 'requires_approval no debe ser mass-assignable');

        // approved_by_user_id sí es fillable (lo setean servicios de confianza).
        $trusted = (new Expense)->fill(['approved_by_user_id' => 7]);
        $this->assertSame(7, $trusted->approved_by_user_id);
    }

    // ─────────────────────────────────────────────────────────────────────
    // Gating de caja (getCashPosition + scopeEffective)
    // ─────────────────────────────────────────────────────────────────────

    #[Test]
    public function cash_position_excludes_pending_expenses(): void
    {
        $this->insertIncome('200000.00');
        $this->insertExpense(['amount' => '50000.00', 'approval_status' => 'approved', 'requires_approval' => false]);
        $this->insertExpense(['amount' => '80000.00', 'approval_status' => 'pending', 'requires_approval' => true]);

        $cash = app(CashFlowMetricsService::class)->getCashPosition($this->company->id);

        // 200k - 50k (aprobado) = 150k. El pendiente de 80k NO descuenta caja.
        $this->assertEqualsWithDelta(150000.0, $cash, 0.01,
            'La caja no debe descontar egresos pendientes de aprobación');
    }

    #[Test]
    public function rejected_expenses_never_affect_cash(): void
    {
        $this->insertIncome('200000.00');
        $this->insertExpense(['amount' => '90000.00', 'approval_status' => 'rejected', 'requires_approval' => true]);

        $cash = app(CashFlowMetricsService::class)->getCashPosition($this->company->id);

        $this->assertEqualsWithDelta(200000.0, $cash, 0.01,
            'Un egreso rechazado nunca descuenta caja');
    }

    #[Test]
    public function approving_a_pending_expense_deducts_it_from_cash(): void
    {
        $this->insertIncome('200000.00');
        $id = $this->insertExpense(['amount' => '80000.00', 'approval_status' => 'pending', 'requires_approval' => true]);

        $service = app(CashFlowMetricsService::class);

        $before = $service->getCashPosition($this->company->id);
        $this->assertEqualsWithDelta(200000.0, $before, 0.01,
            'Mientras está pendiente, la caja no descuenta el egreso');

        // El admin aprueba el egreso.
        DB::table('expenses')->where('id', $id)->update(['approval_status' => 'approved']);
        Cache::flush();

        $after = $service->getCashPosition($this->company->id);
        $this->assertEqualsWithDelta(120000.0, $after, 0.01,
            'Al aprobar, el egreso se descuenta de caja');
    }

    #[Test]
    public function effective_scope_counts_only_approved_or_auto_approved(): void
    {
        $this->insertExpense(['amount' => '10000.00', 'approval_status' => 'approved', 'requires_approval' => true]);   // aprobado
        $this->insertExpense(['amount' => '20000.00', 'approval_status' => 'approved', 'requires_approval' => false]);  // auto-aprobado
        $this->insertExpense(['amount' => '30000.00', 'approval_status' => 'pending', 'requires_approval' => true]);    // excluido
        $this->insertExpense(['amount' => '40000.00', 'approval_status' => 'rejected', 'requires_approval' => true]);   // excluido

        $total = (float) Expense::where('company_id', $this->company->id)->effective()->sum('amount');

        $this->assertEqualsWithDelta(30000.0, $total, 0.01,
            'effective() solo suma aprobados o que no requieren aprobación');
    }
}
