<?php

namespace Tests\Feature\Accounting;

use App\Domain\Accounting\Enums\JournalStatus;
use App\Domain\Accounting\Models\JournalEntry;
use App\Domain\Accounting\Models\LedgerAccount;
use App\Domain\Accounting\Services\BusinessEventPoster;
use App\Domain\Accounting\Services\LedgerPoster;
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 Illuminate\Validation\ValidationException;
use RuntimeException;
use Tests\TestCase;

class AccountingTest 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(): Applicant
    {
        return Applicant::factory()->for($this->company)->create(['branch_id' => $this->branch->id]);
    }

    private function balanceOf(string $code): int
    {
        return LedgerAccount::where('company_id', $this->company->id)
            ->where('code', $code)
            ->firstOrFail()
            ->balance();
    }

    public function test_an_unbalanced_entry_is_refused(): void
    {
        $this->expectException(ValidationException::class);

        app(LedgerPoster::class)->post($this->company->id, 'Lopsided', [
            ['account' => '1100', 'debit' => 10000],
            ['account' => '4200', 'credit' => 9000],
        ]);
    }

    public function test_a_line_cannot_be_both_a_debit_and_a_credit(): void
    {
        $this->expectException(RuntimeException::class);

        app(LedgerPoster::class)->post($this->company->id, 'Confused line', [
            ['account' => '1100', 'debit' => 10000, 'credit' => 10000],
            ['account' => '4200', 'credit' => 10000, 'debit' => 10000],
        ]);
    }

    public function test_a_heading_cannot_be_posted_to(): void
    {
        $this->expectException(ValidationException::class);

        app(LedgerPoster::class)->post($this->company->id, 'Posting to a heading', [
            ['account' => '1000', 'debit' => 10000],
            ['account' => '4200', 'credit' => 10000],
        ]);
    }

    public function test_issuing_an_invoice_recognises_revenue_and_the_receivable(): void
    {
        $manager = $this->user('accounts-manager');
        $applicant = $this->applicant();

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

        $invoice = Invoice::where('applicant_id', $applicant->id)->firstOrFail();

        $this->assertSame(0, $this->balanceOf('1400'), 'A draft invoice posts nothing.');

        $this->actingAs($manager)->postJson("/api/v1/invoices/{$invoice->uuid}/issue")->assertOk();

        $this->assertSame(50000, $this->balanceOf('1400'));
        $this->assertSame(50000, $this->balanceOf('4200'));
    }

    public function test_collecting_cash_clears_the_receivable(): void
    {
        $manager = $this->user('accounts-manager');
        $applicant = $this->applicant();

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

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

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

        $this->assertSame(30000, $this->balanceOf('1100'), 'Cash went up.');
        $this->assertSame(20000, $this->balanceOf('1400'), 'The receivable came down.');
    }

    public function test_money_taken_before_an_invoice_is_held_as_an_advance(): void
    {
        $applicant = $this->applicant();

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

        // Not revenue yet: the agency owes the applicant a service.
        $this->assertSame(25000, $this->balanceOf('2700'));
        $this->assertSame(25000, $this->balanceOf('1100'));
        $this->assertSame(0, $this->balanceOf('4200'));
    }

    public function test_a_wallet_deposit_only_reaches_the_books_once_verified(): void
    {
        $applicant = $this->applicant();

        $payment = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => 25000,
                'currency' => 'BDT',
                'method' => 'bkash',
                'transaction_reference' => 'BK-ADV-1',
            ])->assertCreated()->json('data.id');

        // Unverified money is not the agency's money, so nothing is posted.
        $this->assertSame(0, $this->balanceOf('1300'));

        $this->actingAs($this->user('accounts-manager'))
            ->postJson("/api/v1/payments/{$payment}/verify")
            ->assertOk();

        $this->assertSame(25000, $this->balanceOf('1300'), 'It landed in the wallet account.');
        $this->assertSame(25000, $this->balanceOf('2700'));
    }

    public function test_the_books_balance_after_a_full_cycle(): void
    {
        $manager = $this->user('accounts-manager');
        $applicant = $this->applicant();

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

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

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

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

        $this->assertTrue($trial['is_balanced']);
        // Guard against a vacuous pass: an empty trial balance balances too.
        $this->assertGreaterThan(0, $trial['total_debit']);
        $this->assertSame($trial['total_debit'], $trial['total_credit']);
    }

    public function test_a_posted_entry_cannot_be_edited_but_can_be_reversed(): void
    {
        $manager = $this->user('accounts-manager');

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

        $this->assertSame(80000, $this->balanceOf('6150'));

        try {
            $entry->description = 'Something else';
            $entry->save();
            $this->fail('A posted entry should not be editable.');
        } catch (RuntimeException) {
            // Expected.
        }

        $this->actingAs($manager)
            ->postJson("/api/v1/ledger/journals/{$entry->fresh()->uuid}/reverse", [
                'reason' => 'Posted to the wrong month.',
            ])
            ->assertOk()
            ->assertJsonPath('data.is_reversal', true);

        $this->assertSame(JournalStatus::Reversed, $entry->fresh()->status);
        $this->assertSame(0, $this->balanceOf('6150'), 'The reversal cancels it out.');
    }

    public function test_the_same_event_is_never_posted_twice(): void
    {
        $manager = $this->user('accounts-manager');
        $applicant = $this->applicant();

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

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

        // Replaying the posting must not double the receivable.
        app(BusinessEventPoster::class)
            ->invoiceIssued($invoice->fresh(), $manager);

        $this->assertSame(40000, $this->balanceOf('1400'));
        $this->assertSame(1, JournalEntry::where('source', 'invoice_issued')->count());
    }

    public function test_a_manual_journal_can_be_posted_through_the_api(): void
    {
        $this->actingAs($this->user('accounts-manager'))
            ->postJson('/api/v1/ledger/journals', [
                'description' => 'Bank charges for August',
                'lines' => [
                    ['account_code' => '6650', 'debit' => 1500],
                    ['account_code' => '1200', 'credit' => 1500],
                ],
            ])
            ->assertCreated()
            ->assertJsonPath('data.is_balanced', true)
            ->assertJsonPath('data.status', JournalStatus::Posted->value);
    }

    public function test_an_unbalanced_manual_journal_is_refused_by_the_api(): void
    {
        $this->actingAs($this->user('accounts-manager'))
            ->postJson('/api/v1/ledger/journals', [
                'description' => 'Wishful thinking',
                'lines' => [
                    ['account_code' => '1100', 'debit' => 100000],
                    ['account_code' => '4200', 'credit' => 1],
                ],
            ])
            ->assertStatus(422);
    }

    public function test_the_income_statement_reads_from_the_journal(): void
    {
        $manager = $this->user('accounts-manager');

        app(LedgerPoster::class)->post($this->company->id, 'Service revenue', [
            ['account' => '1100', 'debit' => 200000],
            ['account' => '4200', 'credit' => 200000],
        ], $manager);

        app(LedgerPoster::class)->post($this->company->id, 'Medical cost', [
            ['account' => '5100', 'debit' => 50000],
            ['account' => '1100', 'credit' => 50000],
        ], $manager);

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

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

        $this->assertSame(200000, $report['revenue_total']);
        $this->assertSame(150000, $report['gross_profit']);
        $this->assertSame(120000, $report['net_profit']);
    }

    public function test_the_applicant_statement_runs_a_balance(): void
    {
        $manager = $this->user('accounts-manager');
        $applicant = $this->applicant();

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

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

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

        $statement = $this->actingAs($manager)
            ->getJson("/api/v1/applicants/{$applicant->uuid}/statement")
            ->assertOk()
            ->json('data');

        $this->assertCount(2, $statement['rows']);
        $this->assertSame(50000, $statement['rows'][0]['debit']);
        $this->assertSame(30000, $statement['rows'][1]['credit']);
        $this->assertSame(20000, $statement['closing_balance']);
    }

    public function test_the_cash_position_totals_the_cash_accounts(): void
    {
        $manager = $this->user('accounts-manager');

        app(LedgerPoster::class)->post($this->company->id, 'Opening cash', [
            ['account' => '1100', 'debit' => 100000],
            ['account' => '3100', 'credit' => 100000],
        ], $manager);

        app(LedgerPoster::class)->post($this->company->id, 'Cash to bank', [
            ['account' => '1200', 'debit' => 60000],
            ['account' => '1100', 'credit' => 60000],
        ], $manager);

        $position = $this->actingAs($manager)
            ->getJson('/api/v1/ledger/cash-position')
            ->assertOk()
            ->json('data');

        $this->assertSame(100000, $position['total'], 'Moving money between accounts does not create any.');
    }

    public function test_accounting_needs_the_permission(): void
    {
        $this->actingAs($this->user('data-entry-officer'))
            ->getJson('/api/v1/ledger/trial-balance')
            ->assertForbidden();

        $this->actingAs($this->user('cashier'))
            ->postJson('/api/v1/ledger/journals', [
                'description' => 'Not my job',
                'lines' => [
                    ['account_code' => '1100', 'debit' => 100],
                    ['account_code' => '4200', 'credit' => 100],
                ],
            ])
            ->assertForbidden();
    }
}
