<?php

namespace Tests\Feature\Accounting;

use App\Domain\Accounting\Models\FinancialPeriod;
use App\Domain\Accounting\Models\JournalEntry;
use App\Domain\Accounting\Services\LedgerPoster;
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 Illuminate\Validation\ValidationException;
use Tests\TestCase;

class FinancialPeriodTest 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 openYear(): void
    {
        $this->actingAs($this->user('director'))
            ->postJson('/api/v1/ledger/years', [
                'name' => (string) now()->year,
                'starts_on' => now()->startOfYear()->toDateString(),
            ])
            ->assertCreated()
            ->assertJsonPath('data.periods', 12);
    }

    private function postEntry(string $date = 'today'): void
    {
        app(LedgerPoster::class)->post($this->company->id, 'Test entry', [
            ['account' => '1100', 'debit' => 10000],
            ['account' => '4200', 'credit' => 10000],
        ], null, ['entry_date' => $date === 'today' ? now()->toDateString() : $date]);
    }

    public function test_a_company_without_periods_can_post_freely(): void
    {
        // An agency that has not defined its year yet is not blocked.
        $this->postEntry();

        $this->assertSame(10000, JournalEntry::sum('total_debit'));
    }

    public function test_opening_a_year_creates_twelve_monthly_periods(): void
    {
        $this->openYear();

        $periods = FinancialPeriod::where('company_id', $this->company->id)->orderBy('starts_on')->get();

        $this->assertCount(12, $periods);
        $this->assertSame(now()->startOfYear()->toDateString(), $periods->first()->starts_on->toDateString());
        $this->assertSame(now()->endOfYear()->toDateString(), $periods->last()->ends_on->toDateString());
    }

    public function test_a_closed_period_refuses_new_postings(): void
    {
        $this->openYear();

        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($this->user('director'))
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/close")
            ->assertOk();

        $this->expectException(ValidationException::class);

        $this->postEntry();
    }

    public function test_a_reopened_period_accepts_postings_again(): void
    {
        $this->openYear();

        $director = $this->user('director');
        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($director)->postJson("/api/v1/ledger/periods/{$current->uuid}/close")->assertOk();

        $this->actingAs($director)
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/reopen", [
                'reason' => 'A late supplier bill arrived.',
            ])
            ->assertOk();

        $this->postEntry();

        $this->assertSame(10000, JournalEntry::sum('total_debit'));
    }

    public function test_reopening_needs_a_reason(): void
    {
        $this->openYear();

        $director = $this->user('director');
        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($director)->postJson("/api/v1/ledger/periods/{$current->uuid}/close")->assertOk();

        $this->actingAs($director)
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/reopen", ['reason' => ''])
            ->assertStatus(422);
    }

    public function test_a_locked_period_cannot_be_reopened(): void
    {
        $this->openYear();

        $director = $this->user('director');
        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($director)->postJson("/api/v1/ledger/periods/{$current->uuid}/lock")->assertOk();

        $this->actingAs($director)
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/reopen", ['reason' => 'Please.'])
            ->assertStatus(422);
    }

    public function test_a_closed_period_does_not_block_a_different_month(): void
    {
        $this->openYear();

        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($this->user('director'))
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/close")
            ->assertOk();

        // A date in an open period still posts.
        $other = FinancialPeriod::where('company_id', $this->company->id)
            ->where('id', '!=', $current->id)
            ->where('starts_on', '<', $current->starts_on)
            ->orderByDesc('starts_on')
            ->first() ?? FinancialPeriod::where('company_id', $this->company->id)
            ->where('id', '!=', $current->id)
            ->orderBy('starts_on')
            ->first();

        $this->postEntry($other->starts_on->toDateString());

        $this->assertSame(10000, JournalEntry::sum('total_debit'));
    }

    public function test_closing_a_period_needs_the_permission(): void
    {
        $this->openYear();

        $current = FinancialPeriod::covering($this->company->id, now()->toDateString())->firstOrFail();

        $this->actingAs($this->user('accounts-officer'))
            ->postJson("/api/v1/ledger/periods/{$current->uuid}/close")
            ->assertForbidden();
    }
}
