<?php

declare(strict_types=1);

namespace Tests\Feature\Metrics;

use App\Enums\IncomeCategory;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Income;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\User;
use App\Services\Metrics\BusinessMetricsService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * La BD de dev es una copia de produccion, pero CI corre con BD limpia: cada
 * test crea su propia empresa y solo asevera sobre ella.
 */
class BusinessMetricsServiceTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $user;

    private Credit $credit;

    private Payment $payment;

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

        $this->company = Company::factory()->create(['status' => Company::STATUS_ACTIVE]);
        $this->user = User::factory()->create(['company_id' => $this->company->id]);

        $this->credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'status' => Credit::STATUS_ACTIVE,
        ]);

        $installment = Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'interest_amount' => 10000,
            'principal_amount' => 50000,
            'total_amount' => 60000,
        ]);

        // Pago que cubre el interes completo ($10.000) el 2026-09-01.
        $this->payment = Payment::create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'amount' => 25000,
            'payment_date' => '2026-09-01',
            'registered_by_user_id' => $this->user->id,
            'voided' => false,
        ]);

        DB::table('payment_installment')->insert([
            'payment_id' => $this->payment->id,
            'installment_id' => $installment->id,
            'applied_amount' => 25000,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    /** @param array<string, mixed> $extra Atributos que exija la categoria (p. ej. credit_id en desembolsos). */
    private function expense(string $category, string $type, float $amount, array $extra = []): void
    {
        Expense::create(array_merge([
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'category' => $category,
            'expense_type' => $type,
            'amount' => $amount,
            'operation_date' => '2026-09-02',
            'requires_approval' => false,
            'description' => 'Gasto de prueba',
        ], $extra));
    }

    /**
     * El modelo Income valida integridad: las categorias ligadas a pago exigen
     * payment_id + credit_id, y las operativas NO pueden llevarlos. Decidimos
     * con el propio enum en vez de adivinar por categoria.
     */
    private function income(string $category, float $amount): void
    {
        $atributos = [
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'category' => $category,
            'amount' => $amount,
            'operation_date' => '2026-09-03',
            'description' => 'Ingreso de prueba',
        ];

        if (IncomeCategory::from($category)->isPaymentLinked()) {
            $atributos['payment_id'] = $this->payment->id;
            $atributos['credit_id'] = $this->credit->id;
        }

        Income::create($atributos);
    }

    /** @return array<string, mixed> */
    private function metrics(): array
    {
        return app(BusinessMetricsService::class)->forPeriod(
            $this->company->id,
            Carbon::parse('2026-09-01'),
            Carbon::parse('2026-09-30'),
        );
    }

    #[Test]
    public function la_utilidad_es_interes_menos_gastos_operativos(): void
    {
        $this->expense('office_supplies', 'operational', 4000);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(10000.0, $m['interest_collected'], 0.01);
        $this->assertEqualsWithDelta(4000.0, $m['operating_expenses'], 0.01);
        $this->assertEqualsWithDelta(6000.0, $m['net_profit'], 0.01);
        $this->assertEqualsWithDelta(60.0, $m['margin'], 0.01);
    }

    #[Test]
    public function un_desembolso_no_es_un_gasto_operativo(): void
    {
        // REGRESION: en produccion hay ~$84M de desembolsos con la columna
        // affects_profit en 1. Un desembolso es capital colocado, no un costo:
        // si se restara, la utilidad se desplomaria a una perdida ficticia.
        $this->expense('disbursement', 'credit_disbursement', 5000000, ['credit_id' => $this->credit->id]);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(0.0, $m['operating_expenses'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['net_profit'], 0.01);
    }

    #[Test]
    public function un_retiro_de_utilidad_no_es_un_costo(): void
    {
        // Retirar utilidad es repartirla, no gastarla generandola. Restarla
        // aqui la contaria dos veces.
        $this->expense('profit_withdrawal', 'profit_withdrawal', 3000);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(0.0, $m['operating_expenses'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['net_profit'], 0.01);
    }

    #[Test]
    public function un_ingreso_legacy_payment_no_se_cuenta_dos_veces(): void
    {
        // El pago YA esta representado por la derivacion del ledger de cuotas.
        // Si ademas sumaramos su fila en `incomes`, la utilidad se dispararia.
        $this->income('payment', 25000);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(0.0, $m['other_profit_income'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['profit_income'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['net_profit'], 0.01);
    }

    #[Test]
    public function una_fila_loan_payment_interest_no_duplica_el_interes_derivado(): void
    {
        // Blindaje a futuro: el dia que alguien empiece a escribir la categoria
        // `loan_payment_interest` (el saneamiento del ledger es proyecto aparte),
        // el interes NO puede contarse dos veces — ya viene por derivacion.
        $this->income('loan_payment_interest', 10000);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(0.0, $m['other_profit_income'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['profit_income'], 0.01);
        $this->assertEqualsWithDelta(10000.0, $m['net_profit'], 0.01);
    }

    #[Test]
    public function las_comisiones_bien_categorizadas_si_suman(): void
    {
        $this->income('late_fee', 2000);

        $m = $this->metrics();

        $this->assertEqualsWithDelta(2000.0, $m['other_profit_income'], 0.01);
        $this->assertEqualsWithDelta(12000.0, $m['profit_income'], 0.01);
    }

    #[Test]
    public function sin_ingresos_de_utilidad_el_margen_es_null_no_cero(): void
    {
        $m = app(BusinessMetricsService::class)->forPeriod(
            $this->company->id,
            Carbon::parse('2026-01-01'),
            Carbon::parse('2026-01-31'),
        );

        $this->assertEqualsWithDelta(0.0, $m['profit_income'], 0.01);
        $this->assertNull($m['margin']);
    }
}
