<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\EmployeeCompensation;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\PayrollEntry;
use App\Models\PayrollPeriod;
use App\Models\Role;
use App\Models\User;
use Carbon\CarbonImmutable;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PayrollWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        CarbonImmutable::setTestNow('2026-08-12 12:45:00');
        $this->seed(DatabaseSeeder::class);
    }

    protected function tearDown(): void
    {
        CarbonImmutable::setTestNow();
        parent::tearDown();
    }

    public function test_compensation_period_generation_formula_and_snapshot_are_persistent(): void
    {
        [$branch,, $hr] = $this->fixture('formula');
        $employee = $this->employee($branch, 'payroll_formula_employee');

        $this->actingAs($hr)->put(route('payroll.compensations.update', $employee), $this->components())->assertRedirect();
        $compensation = EmployeeCompensation::where('user_id', $employee->id)->firstOrFail();
        $this->assertSame('1000.00', $compensation->basic_salary);
        $this->assertDatabaseHas('audit_logs', ['action' => 'payroll.compensation.updated', 'auditable_id' => $compensation->id]);

        $this->actingAs($hr)->post(route('payroll.store'), $this->periodPayload($branch))->assertRedirect();
        $period = PayrollPeriod::firstOrFail();
        $entry = PayrollEntry::where('payroll_period_id', $period->id)->where('user_id', $employee->id)->firstOrFail();

        $this->assertSame('1250.00', $entry->gross);
        $this->assertSame('60.00', $entry->total_deductions);
        $this->assertSame('1190.00', $entry->net);
        $this->assertSame('1250.00', $period->total_gross);
        $this->assertSame('60.00', $period->total_deductions);
        $this->assertSame('1190.00', $period->total_net);
        $this->assertStringStartsWith('PSL-', $entry->payslip_number);

        $changed = $this->components();
        $changed['basic_salary'] = '2000.00';
        $this->actingAs($hr)->put(route('payroll.compensations.update', $employee), $changed)->assertRedirect();
        $this->assertSame('1000.00', $entry->fresh()->basic_salary);

        $edited = $this->components();
        $edited['basic_salary'] = '1200.00';
        $this->actingAs($hr)->put(route('payroll.entries.update', [$period, $entry]), $edited)->assertRedirect();
        $this->assertSame('1450.00', $entry->fresh()->gross);
        $this->assertSame('1390.00', $entry->fresh()->net);
        $this->assertSame('1390.00', $period->fresh()->total_net);
    }

    public function test_maker_checker_approval_locks_period_and_rejection_never_posts_money(): void
    {
        [$branch, $account, , $manager] = $this->fixture('approval');
        $branch->business->update(['maker_checker_enabled' => true]);
        $admin = $this->staff('branch-administrator', $branch, 'payroll_branch_admin');
        $employee = $this->employee($branch, 'payroll_approval_employee');
        $this->actingAs($admin)->put(route('payroll.compensations.update', $employee), $this->components())->assertRedirect();
        $this->actingAs($admin)->post(route('payroll.store'), $this->periodPayload($branch))->assertRedirect();
        $period = PayrollPeriod::firstOrFail();
        $entry = PayrollEntry::where('user_id', $employee->id)->firstOrFail();

        $this->actingAs($admin)->post(route('payroll.decision', $period), ['decision' => 'approve'])->assertSessionHasErrors('payroll');
        $this->assertSame('draft', $period->fresh()->status);
        $this->actingAs($manager)->post(route('payroll.decision', $period), ['decision' => 'approve', 'comments' => 'Payroll checked.'])->assertRedirect();
        $this->assertSame('approved', $period->fresh()->status);

        $this->actingAs($admin)->put(route('payroll.entries.update', [$period, $entry]), $this->components())->assertSessionHasErrors('payroll');
        $this->assertSame('1190.00', $entry->fresh()->net);
        $this->assertSame('100000.00', $account->fresh()->current_balance);

        [$branch2, $account2, $hr2, $manager2] = $this->fixture('rejection');
        $employee2 = $this->employee($branch2, 'payroll_rejection_employee');
        $this->actingAs($hr2)->put(route('payroll.compensations.update', $employee2), $this->components())->assertRedirect();
        $payload = $this->periodPayload($branch2);
        $payload['start_date'] = '2026-09-01'; $payload['end_date'] = '2026-09-30'; $payload['pay_date'] = '2026-09-30';
        $this->actingAs($hr2)->post(route('payroll.store'), $payload)->assertRedirect();
        $rejected = PayrollPeriod::where('branch_id', $branch2->id)->latest('id')->firstOrFail();
        $this->actingAs($manager2)->post(route('payroll.decision', $rejected), ['decision' => 'reject', 'comments' => 'Attendance data incomplete.'])->assertRedirect();
        $this->assertSame('rejected', $rejected->fresh()->status);
        $this->assertSame('100000.00', $account2->fresh()->current_balance);
        $this->assertSame(0, FinancialAccountTransaction::where('type', 'payroll')->count());
    }

    public function test_approved_payroll_payment_posts_account_and_balanced_payroll_ledger_and_prevents_double_payment(): void
    {
        [$branch, $account, $hr, $manager, $accountant] = $this->fixture('payment');
        $employee = $this->employee($branch, 'payroll_payment_employee');
        $this->actingAs($hr)->put(route('payroll.compensations.update', $employee), $this->components())->assertRedirect();
        $this->actingAs($hr)->post(route('payroll.store'), $this->periodPayload($branch))->assertRedirect();
        $period = PayrollPeriod::firstOrFail();
        $entry = PayrollEntry::where('user_id', $employee->id)->firstOrFail();
        $this->actingAs($manager)->post(route('payroll.decision', $period), ['decision' => 'approve'])->assertRedirect();

        $this->actingAs($accountant)->post(route('payroll.pay', $period), ['financial_account_id' => $account->id])->assertRedirect();
        $period->refresh(); $entry->refresh();
        $this->assertSame('paid', $period->status);
        $this->assertSame('paid', $entry->status);
        $this->assertSame('98810.00', $account->fresh()->current_balance);
        $this->assertDatabaseHas('financial_account_transactions', [
            'related_type' => PayrollEntry::class, 'related_id' => $entry->id, 'type' => 'payroll', 'money_out' => 1190, 'money_in' => 0,
        ]);
        $this->assertDatabaseHas('accounting_entries', [
            'related_type' => PayrollEntry::class, 'related_id' => $entry->id, 'category' => 'payroll', 'debit' => 1190, 'credit' => 0,
        ]);
        $this->assertDatabaseHas('accounting_entries', [
            'related_type' => PayrollEntry::class, 'related_id' => $entry->id, 'category' => 'assets', 'debit' => 0, 'credit' => 1190,
        ]);
        $this->assertSame(
            AccountingEntry::where('related_type', PayrollEntry::class)->where('related_id', $entry->id)->sum('debit'),
            AccountingEntry::where('related_type', PayrollEntry::class)->where('related_id', $entry->id)->sum('credit')
        );
        $this->assertNotNull($entry->financial_account_transaction_id);
        $this->assertNotNull($entry->accounting_journal_number);
        $transactionCount = FinancialAccountTransaction::where('type', 'payroll')->count();

        $this->actingAs($accountant)->post(route('payroll.pay', $period), ['financial_account_id' => $account->id])->assertSessionHasErrors('payroll');
        $this->assertSame('98810.00', $account->fresh()->current_balance);
        $this->assertSame($transactionCount, FinancialAccountTransaction::where('type', 'payroll')->count());
    }

    public function test_insufficient_funds_aborts_entire_payroll_without_partial_employee_payment(): void
    {
        [$branch, $account, $hr, $manager, $accountant] = $this->fixture('insufficient');
        $employee = $this->employee($branch, 'payroll_insufficient_employee');
        $this->actingAs($hr)->put(route('payroll.compensations.update', $employee), $this->components())->assertRedirect();
        $this->actingAs($hr)->post(route('payroll.store'), $this->periodPayload($branch))->assertRedirect();
        $period = PayrollPeriod::firstOrFail();
        $this->actingAs($manager)->post(route('payroll.decision', $period), ['decision' => 'approve'])->assertRedirect();
        $account->update(['current_balance' => '100.00']);

        $this->actingAs($accountant)->post(route('payroll.pay', $period), ['financial_account_id' => $account->id])->assertSessionHasErrors('financial_account_id');
        $this->assertSame('approved', $period->fresh()->status);
        $this->assertSame('100.00', $account->fresh()->current_balance);
        $this->assertSame(0, FinancialAccountTransaction::where('type', 'payroll')->count());
        $this->assertSame(0, PayrollEntry::where('payroll_period_id', $period->id)->where('status', 'paid')->count());
        $this->assertSame(0, AccountingEntry::where('event_type', 'payroll_payment')->count());
    }

    public function test_payroll_permissions_branch_isolation_and_duty_separation_are_backend_enforced(): void
    {
        [$branchA,, $hrA] = $this->fixture('branch-a');
        [$branchB,, $hrB] = $this->fixture('branch-b');
        $employeeB = $this->employee($branchB, 'payroll_branch_b_employee');
        $this->actingAs($hrB)->put(route('payroll.compensations.update', $employeeB), $this->components())->assertRedirect();
        $this->actingAs($hrB)->post(route('payroll.store'), $this->periodPayload($branchB))->assertRedirect();
        $periodB = PayrollPeriod::where('branch_id', $branchB->id)->firstOrFail();

        $auditorA = $this->staff('auditor', $branchA, 'payroll_auditor_a');
        $accountantA = $this->staff('accountant', $branchA, 'payroll_accountant_a_extra');
        $this->actingAs($auditorA)->get(route('payroll.index'))->assertOk()->assertDontSee($periodB->period_number);
        $this->actingAs($auditorA)->get(route('payroll.show', $periodB))->assertForbidden();
        $this->actingAs($accountantA)->get(route('payroll.compensations'))->assertForbidden();
        $this->actingAs($hrA)->post(route('payroll.pay', $periodB), ['financial_account_id' => 1])->assertForbidden();
        $this->actingAs($hrA)->get(route('payroll.create'))->assertOk()->assertSee('data-ui="form-shell"', false);
    }

    public function test_paid_payslip_is_printable_and_downloadable_as_pdf(): void
    {
        [$branch, $account, $hr, $manager, $accountant] = $this->fixture('payslip');
        $employee = $this->employee($branch, 'payroll_payslip_employee');
        $this->actingAs($hr)->put(route('payroll.compensations.update', $employee), $this->components())->assertRedirect();
        $this->actingAs($hr)->post(route('payroll.store'), $this->periodPayload($branch))->assertRedirect();
        $period = PayrollPeriod::firstOrFail();
        $entry = PayrollEntry::where('user_id', $employee->id)->firstOrFail();

        $this->actingAs($hr)->get(route('payroll.payslip', [$period, $entry]))->assertStatus(422);
        $this->actingAs($manager)->post(route('payroll.decision', $period), ['decision' => 'approve'])->assertRedirect();
        $this->actingAs($accountant)->post(route('payroll.pay', $period), ['financial_account_id' => $account->id])->assertRedirect();
        $entry->refresh();

        $this->actingAs($accountant)->get(route('payroll.payslip', [$period, $entry]))
            ->assertOk()->assertSee('Employee Payslip')->assertSee($entry->payslip_number)->assertSee('1,190.00');
        $response = $this->actingAs($accountant)->get(route('payroll.payslip.pdf', [$period, $entry]));
        $response->assertOk()->assertHeader('content-type', 'application/pdf');
        $this->assertStringStartsWith('%PDF-1.4', $response->getContent());
    }

    private function components(): array
    {
        return [
            'basic_salary' => '1000.00', 'allowances' => '100.00', 'bonus' => '50.00', 'commission' => '25.00',
            'overtime' => '75.00', 'deductions' => '30.00', 'advances' => '20.00', 'other_deductions' => '10.00',
            'effective_from' => '2026-08-01', 'status' => 'active', 'notes' => 'Standard compensation package.',
        ];
    }

    private function periodPayload(Branch $branch): array
    {
        return [
            'branch_id' => $branch->id, 'name' => 'August 2026 Payroll', 'start_date' => '2026-08-01',
            'end_date' => '2026-08-31', 'pay_date' => '2026-08-31',
        ];
    }

    private function fixture(string $tag): array
    {
        $business = Business::firstOrFail();
        $branch = Branch::create(['business_id' => $business->id, 'name' => 'Payroll '.str($tag)->headline(), 'status' => 'active']);
        $account = FinancialAccount::create([
            'branch_id' => $branch->id, 'name' => 'Payroll Cash '.str($tag)->headline(), 'account_type' => 'cash',
            'opening_balance' => '100000.00', 'current_balance' => '100000.00', 'status' => 'active',
        ]);
        $hr = $this->staff('hr-payroll-officer', $branch, 'payroll_hr_'.str_replace('-', '_', $tag));
        $manager = $this->staff('branch-manager', $branch, 'payroll_manager_'.str_replace('-', '_', $tag));
        $accountant = $this->staff('accountant', $branch, 'payroll_accountant_'.str_replace('-', '_', $tag));
        return [$branch, $account, $hr, $manager, $accountant];
    }

    private function employee(Branch $branch, string $username): User
    {
        $employee = User::factory()->create([
            'name' => str($username)->replace('_', ' ')->title()->toString(), 'username' => $username,
            'position' => 'Operations Officer', 'status' => 'active', 'primary_branch_id' => $branch->id,
        ]);
        $employee->branches()->attach($branch->id, ['is_primary' => true]);
        return $employee;
    }

    private function staff(string $role, Branch $branch, string $username): User
    {
        $user = User::factory()->create([
            'name' => str($username)->replace('_', ' ')->title()->toString(), 'username' => $username,
            'status' => 'active', 'primary_branch_id' => $branch->id,
        ]);
        $user->roles()->attach(Role::where('name', $role)->firstOrFail());
        $user->branches()->attach($branch->id, ['is_primary' => true]);
        return $user;
    }
}
