<?php

namespace Tests\Feature\Accounting;

use App\Domain\Accounting\Services\LedgerPoster;
use App\Domain\Agents\Models\Agent;
use App\Domain\Applicants\Models\Applicant;
use App\Domain\Billing\Models\Invoice;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use Database\Seeders\ChartOfAccountsSeeder;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class FinancialReportTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

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

        $this->seed(ChartOfAccountsSeeder::class);
    }

    private function user(string $role): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole($role);
    }

    private function applicant(array $attributes = []): Applicant
    {
        return Applicant::factory()->for($this->company)->create(array_merge([
            'branch_id' => $this->branch->id,
            'destination_country' => 'SA',
        ], $attributes));
    }

    /** Invoices an applicant and takes the money, so the books have a cycle. */
    private function cycle(Applicant $applicant, int $charge, int $collect, ?string $dueOn = null): Invoice
    {
        $manager = $this->user('accounts-manager');

        $this->actingAs($manager)->postJson("/api/v1/applicants/{$applicant->uuid}/invoices", [
            'currency' => 'BDT',
            'due_on' => $dueOn,
            'lines' => [['description' => 'Service charge', 'unit_amount' => $charge]],
        ])->assertCreated();

        $invoice = Invoice::where('applicant_id', $applicant->id)->latest('id')->firstOrFail();
        $this->actingAs($manager)->postJson("/api/v1/invoices/{$invoice->uuid}/issue")->assertOk();

        if ($collect > 0) {
            $this->actingAs($this->user('cashier'))
                ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                    'amount' => $collect,
                    'currency' => 'BDT',
                    'method' => 'cash',
                    'invoice_id' => $invoice->uuid,
                ])->assertCreated();
        }

        return $invoice->fresh();
    }

    public function test_the_balance_sheet_balances(): void
    {
        $this->cycle($this->applicant(), 100000, 60000);

        $sheet = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/balance-sheet')
            ->assertOk()
            ->json('data');

        // Assets: 60,000 cash + 40,000 receivable. Equity: 100,000 profit.
        $this->assertSame(100000, $sheet['asset_total']);
        $this->assertSame(100000, $sheet['retained_earnings']);
        $this->assertTrue($sheet['is_balanced']);
    }

    public function test_spending_reduces_retained_earnings_on_the_balance_sheet(): void
    {
        $this->cycle($this->applicant(), 100000, 100000);

        app(LedgerPoster::class)->post($this->company->id, 'Office rent', [
            ['account' => '6150', 'debit' => 30000],
            ['account' => '1100', 'credit' => 30000],
        ]);

        $sheet = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/balance-sheet')
            ->assertOk()
            ->json('data');

        $this->assertSame(70000, $sheet['asset_total']);
        $this->assertSame(70000, $sheet['retained_earnings']);
        $this->assertTrue($sheet['is_balanced']);
    }

    public function test_receivables_aging_buckets_what_applicants_owe(): void
    {
        $this->cycle($this->applicant(), 50000, 0, now()->addWeek()->toDateString());
        $this->cycle($this->applicant(), 80000, 0, now()->subDays(100)->toDateString());

        $aging = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/receivables-aging')
            ->assertOk()
            ->json('data');

        $this->assertSame(50000, $aging['buckets']['current']);
        $this->assertSame(80000, $aging['buckets']['over_90']);
        $this->assertSame(130000, $aging['total']);
    }

    public function test_profitability_per_applicant_nets_revenue_against_cost(): void
    {
        $applicant = $this->applicant();
        $this->cycle($applicant, 100000, 100000);

        // A medical cost booked against the same file.
        app(LedgerPoster::class)->post($this->company->id, 'Medical for applicant', [
            ['account' => '5100', 'debit' => 25000, 'applicant_id' => $applicant->id],
            ['account' => '1100', 'credit' => 25000],
        ]);

        $report = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/profitability')
            ->assertOk()
            ->json('data');

        $this->assertCount(1, $report['applicants']);
        $this->assertSame(100000, $report['applicants'][0]['revenue']);
        $this->assertSame(25000, $report['applicants'][0]['cost']);
        $this->assertSame(75000, $report['applicants'][0]['margin']);
    }

    public function test_revenue_can_be_split_by_destination(): void
    {
        $this->cycle($this->applicant(['destination_country' => 'SA']), 100000, 0);
        $this->cycle($this->applicant(['destination_country' => 'AE']), 60000, 0);

        $report = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/revenue-by/destination')
            ->assertOk()
            ->json('data');

        $this->assertSame(160000, $report['total']);
        $this->assertSame('SA', $report['rows'][0]['label']);
        $this->assertSame(100000, $report['rows'][0]['revenue']);
    }

    public function test_revenue_can_be_split_by_agent(): void
    {
        $agent = Agent::factory()->for($this->company)->create(['branch_id' => $this->branch->id]);

        $this->cycle($this->applicant(['agent_id' => $agent->id]), 90000, 0);
        $this->cycle($this->applicant(), 40000, 0);

        $report = $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/revenue-by/agent')
            ->assertOk()
            ->json('data');

        $labels = array_column($report['rows'], 'label');

        $this->assertContains($agent->name, $labels);
        // An applicant who walked in has no agent behind them.
        $this->assertContains('Direct', $labels);
        $this->assertSame(130000, $report['total']);
    }

    public function test_an_unknown_revenue_dimension_is_not_found(): void
    {
        $this->actingAs($this->user('accounts-manager'))
            ->getJson('/api/v1/ledger/revenue-by/something-else')
            ->assertNotFound();
    }

    public function test_financial_reports_need_the_permission(): void
    {
        foreach (['balance-sheet', 'profitability', 'revenue-by/branch'] as $path) {
            $this->actingAs($this->user('cashier'))
                ->getJson("/api/v1/ledger/{$path}")
                ->assertForbidden();
        }
    }
}
