<?php

use App\Enums\PayrollAdjustmentType;
use App\Enums\PayrollPeriodStatus;
use App\Enums\PayrollStatus;
use App\Enums\Permission as PermissionEnum;
use App\Models\Employee;
use App\Models\Payroll;
use App\Models\PayrollAdjustment;
use App\Models\PayrollPeriod;
use App\Models\User;
use Database\Seeders\CashAccountSeeder;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;

uses(RefreshDatabase::class);

beforeEach(function () {
    $this->seed(RolePermissionSeeder::class);
    User::factory()->create(); // Required by CashAccountSeeder
    $this->seed(CashAccountSeeder::class);
    Storage::fake('public');
});

// ─── Helper ───────────────────────────────────────────────

function createPayrollUserWithPermission(PermissionEnum ...$permissions): User
{
    $user = User::factory()->create();

    $user->givePermissionTo(
        array_merge(
            [PermissionEnum::DASHBOARD_VIEW->value],
            array_map(fn (PermissionEnum $p) => $p->value, $permissions)
        )
    );

    $user->forgetCachedPermissions();

    return $user;
}

function createPayrollWithPeriod(?Employee $employee = null): Payroll
{
    $period = PayrollPeriod::factory()->create([
        'status' => PayrollPeriodStatus::OPEN->value,
    ]);

    $employee ??= Employee::factory()->create();

    return Payroll::factory()->create([
        'payroll_period_id' => $period->id,
        'employee_id' => $employee->id,
        'status' => PayrollStatus::UNPAID->value,
    ]);
}

function adjustmentPayload(?string $type = null, ?int $amount = null, ?string $description = null): array
{
    return [
        'type' => $type ?? PayrollAdjustmentType::BONUS->value,
        'amount' => $amount ?? 500000,
        'description' => $description ?? 'Tunjangan transport',
    ];
}

// ─── Index ────────────────────────────────────────────────

describe('Payroll Index', function () {
    test('authenticated user with permission can view payroll index', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $this->actingAs($user)
            ->get(route('admin.finance.payroll.index'))
            ->assertOk();
    });

    test('guest is redirected to login', function () {
        $this->get(route('admin.finance.payroll.index'))
            ->assertRedirect(route('login'));
    });

    test('user without permission is forbidden', function () {
        $user = User::factory()->create();

        $this->actingAs($user)
            ->get(route('admin.finance.payroll.index'))
            ->assertForbidden();
    });

    test('index displays payrolls for a period', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $period = PayrollPeriod::factory()->create();
        Payroll::factory()->count(3)->create(['payroll_period_id' => $period->id]);

        $this->actingAs($user)
            ->get(route('admin.finance.payroll.index', ['period_id' => $period->id]))
            ->assertOk();
    });

    test('index can search by employee name', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $period = PayrollPeriod::factory()->create();
        Payroll::factory()->create(['payroll_period_id' => $period->id]);

        $this->actingAs($user)
            ->get(route('admin.finance.payroll.index', [
                'period_id' => $period->id,
                'search' => 'test',
            ]))
            ->assertOk();
    });
});

// ─── Store Adjustment ─────────────────────────────────────

describe('Payroll Store Adjustment', function () {
    test('authenticated user with permission can add adjustment', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), adjustmentPayload())
            ->assertRedirect();

        $this->assertDatabaseHas('payroll_adjustments', [
            'payroll_id' => $payroll->id,
            'type' => PayrollAdjustmentType::BONUS->value,
            'amount' => 500000,
            'description' => 'Tunjangan transport',
        ]);
    });

    test('guest cannot add adjustment', function () {
        $payroll = createPayrollWithPeriod();

        $this->post(route('admin.finance.payroll.adjustments.store', $payroll), adjustmentPayload())
            ->assertRedirect(route('login'));
    });

    test('user without adjust permission is forbidden', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), adjustmentPayload())
            ->assertForbidden();
    });

    test('type is required', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), [
                'type' => '',
                'amount' => 500000,
                'description' => 'Test',
            ])
            ->assertSessionHasErrors('type');
    });

    test('type must be valid enum value', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), [
                'type' => 'invalid',
                'amount' => 500000,
                'description' => 'Test',
            ])
            ->assertSessionHasErrors('type');
    });

    test('amount is required', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), [
                'type' => PayrollAdjustmentType::BONUS->value,
                'amount' => '',
                'description' => 'Test',
            ])
            ->assertSessionHasErrors('amount');
    });

    test('amount must be at least 1', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), [
                'type' => PayrollAdjustmentType::BONUS->value,
                'amount' => 0,
                'description' => 'Test',
            ])
            ->assertSessionHasErrors('amount');
    });

    test('description is required', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), [
                'type' => PayrollAdjustmentType::BONUS->value,
                'amount' => 500000,
                'description' => '',
            ])
            ->assertSessionHasErrors('description');
    });

    test('adding bonus adjustment recalculates payroll amounts', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 0,
            'deduction_amount' => 0,
            'total_amount' => 3000000,
            'status' => PayrollStatus::UNPAID->value,
        ]);

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), adjustmentPayload(
                type: PayrollAdjustmentType::BONUS->value,
                amount: 500000,
            ));

        expect($payroll->fresh()->bonus_amount)->toBe(500000);
        expect($payroll->fresh()->total_amount)->toBe(3500000);
    });

    test('adding deduction adjustment recalculates payroll amounts', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 0,
            'deduction_amount' => 0,
            'total_amount' => 3000000,
            'status' => PayrollStatus::UNPAID->value,
        ]);

        $this->actingAs($user)
            ->post(route('admin.finance.payroll.adjustments.store', $payroll), adjustmentPayload(
                type: PayrollAdjustmentType::DEDUCTION->value,
                amount: 200000,
            ));

        expect($payroll->fresh()->deduction_amount)->toBe(200000);
        expect($payroll->fresh()->total_amount)->toBe(2800000);
    });
});

// ─── Update Adjustment ────────────────────────────────────

describe('Payroll Update Adjustment', function () {
    test('authenticated user with permission can update adjustment', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();
        $adjustment = PayrollAdjustment::factory()->bonus()->create([
            'payroll_id' => $payroll->id,
            'amount' => 500000,
        ]);

        $this->actingAs($user)
            ->put(route('admin.finance.payroll.adjustments.update', $adjustment), adjustmentPayload(
                type: PayrollAdjustmentType::BONUS->value,
                amount: 700000,
                description: 'Deskripsi baru',
            ))
            ->assertRedirect();

        expect($adjustment->fresh()->amount)->toBe(700000);
        expect($adjustment->fresh()->description)->toBe('Deskripsi baru');
    });

    test('guest cannot update adjustment', function () {
        $adjustment = PayrollAdjustment::factory()->create();

        $this->put(route('admin.finance.payroll.adjustments.update', $adjustment), adjustmentPayload())
            ->assertRedirect(route('login'));
    });

    test('user without adjust permission is forbidden', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $adjustment = PayrollAdjustment::factory()->create();

        $this->actingAs($user)
            ->put(route('admin.finance.payroll.adjustments.update', $adjustment), adjustmentPayload())
            ->assertForbidden();
    });

    test('updating adjustment recalculates payroll amounts', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 500000,
            'deduction_amount' => 0,
            'total_amount' => 3500000,
            'status' => PayrollStatus::UNPAID->value,
        ]);

        $adjustment = PayrollAdjustment::factory()->bonus()->create([
            'payroll_id' => $payroll->id,
            'amount' => 500000,
        ]);

        $this->actingAs($user)
            ->put(route('admin.finance.payroll.adjustments.update', $adjustment), adjustmentPayload(
                type: PayrollAdjustmentType::BONUS->value,
                amount: 800000,
            ));

        expect($payroll->fresh()->bonus_amount)->toBe(800000);
        expect($payroll->fresh()->total_amount)->toBe(3800000);
    });
});

// ─── Destroy Adjustment ───────────────────────────────────

describe('Payroll Destroy Adjustment', function () {
    test('authenticated user with permission can delete adjustment', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = createPayrollWithPeriod();
        $adjustment = PayrollAdjustment::factory()->create(['payroll_id' => $payroll->id]);

        $this->actingAs($user)
            ->delete(route('admin.finance.payroll.adjustments.destroy', $adjustment))
            ->assertRedirect();

        $this->assertDatabaseMissing('payroll_adjustments', ['id' => $adjustment->id]);
    });

    test('guest cannot delete adjustment', function () {
        $adjustment = PayrollAdjustment::factory()->create();

        $this->delete(route('admin.finance.payroll.adjustments.destroy', $adjustment))
            ->assertRedirect(route('login'));
    });

    test('user without adjust permission is forbidden', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW);

        $adjustment = PayrollAdjustment::factory()->create();

        $this->actingAs($user)
            ->delete(route('admin.finance.payroll.adjustments.destroy', $adjustment))
            ->assertForbidden();
    });

    test('deleting adjustment recalculates payroll amounts', function () {
        $user = createPayrollUserWithPermission(PermissionEnum::PAYROLL_VIEW, PermissionEnum::PAYROLL_ADJUST);

        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 500000,
            'deduction_amount' => 0,
            'total_amount' => 3500000,
            'status' => PayrollStatus::UNPAID->value,
        ]);

        $adjustment = PayrollAdjustment::factory()->bonus()->create([
            'payroll_id' => $payroll->id,
            'amount' => 500000,
        ]);

        $this->actingAs($user)
            ->delete(route('admin.finance.payroll.adjustments.destroy', $adjustment));

        expect($payroll->fresh()->bonus_amount)->toBe(0);
        expect($payroll->fresh()->total_amount)->toBe(3000000);
    });
});

// ─── Payroll Model ────────────────────────────────────────

describe('Payroll Model', function () {
    test('payroll has amounts cast to integer', function () {
        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 500000,
            'deduction_amount' => 200000,
            'total_amount' => 3300000,
        ]);

        expect($payroll->base_salary)->toBeInt();
        expect($payroll->bonus_amount)->toBeInt();
        expect($payroll->deduction_amount)->toBeInt();
        expect($payroll->total_amount)->toBeInt();
    });

    test('payroll has status cast to enum', function () {
        $payroll = Payroll::factory()->create([
            'status' => PayrollStatus::UNPAID->value,
        ]);

        expect($payroll->status)->toBe(PayrollStatus::UNPAID);
    });

    test('payroll has formatted amount accessors', function () {
        $payroll = Payroll::factory()->create([
            'base_salary' => 3000000,
            'bonus_amount' => 500000,
            'deduction_amount' => 200000,
            'total_amount' => 3300000,
        ]);

        expect($payroll->base_salary_formatted)->toBe('Rp 3.000.000');
        expect($payroll->bonus_amount_formatted)->toBe('Rp 500.000');
        expect($payroll->deduction_amount_formatted)->toBe('Rp 200.000');
        expect($payroll->total_amount_formatted)->toBe('Rp 3.300.000');
    });

    test('payroll has status label accessor', function () {
        $payroll = Payroll::factory()->create([
            'status' => PayrollStatus::UNPAID->value,
        ]);

        expect($payroll->status_label)->toBe('Belum Dibayar');
    });

    test('payroll belongs to employee', function () {
        $payroll = Payroll::factory()->create();

        expect($payroll->employee)->not->toBeNull();
    });

    test('payroll belongs to payroll period', function () {
        $payroll = Payroll::factory()->create();

        expect($payroll->payrollPeriod)->not->toBeNull();
    });

    test('payroll can have adjustments', function () {
        $payroll = Payroll::factory()->create();
        PayrollAdjustment::factory()->count(3)->create(['payroll_id' => $payroll->id]);

        expect($payroll->fresh()->adjustments)->toHaveCount(3);
    });
});

// ─── Payroll Period Model ─────────────────────────────────

describe('Payroll Period Model', function () {
    test('payroll period has status cast to enum', function () {
        $period = PayrollPeriod::factory()->create([
            'status' => PayrollPeriodStatus::OPEN->value,
        ]);

        expect($period->status)->toBe(PayrollPeriodStatus::OPEN);
    });

    test('payroll period has period label accessor', function () {
        $period = PayrollPeriod::factory()->create([
            'year' => 2026,
            'month' => 6,
        ]);

        expect($period->period_label)->not->toBeEmpty();
    });

    test('payroll period has status label accessor', function () {
        $period = PayrollPeriod::factory()->create([
            'status' => PayrollPeriodStatus::OPEN->value,
        ]);

        expect($period->status_label)->toBe('Dibuka');
    });

    test('payroll period isOpen method', function () {
        $openPeriod = PayrollPeriod::factory()->create([
            'status' => PayrollPeriodStatus::OPEN->value,
        ]);

        $closedPeriod = PayrollPeriod::factory()->closed()->create();

        expect($openPeriod->isOpen())->toBeTrue();
        expect($closedPeriod->isOpen())->toBeFalse();
    });

    test('payroll period uses soft deletes', function () {
        $period = PayrollPeriod::factory()->create();

        $period->delete();

        expect($period->trashed())->toBeTrue();
    });
});

// ─── Payroll Adjustment Model ─────────────────────────────

describe('Payroll Adjustment Model', function () {
    test('payroll adjustment has type cast to enum', function () {
        $adjustment = PayrollAdjustment::factory()->create([
            'type' => PayrollAdjustmentType::BONUS->value,
        ]);

        expect($adjustment->type)->toBe(PayrollAdjustmentType::BONUS);
    });

    test('payroll adjustment has amount cast to integer', function () {
        $adjustment = PayrollAdjustment::factory()->create(['amount' => 500000]);

        expect($adjustment->amount)->toBeInt();
        expect($adjustment->amount)->toBe(500000);
    });

    test('payroll adjustment has formatted accessors', function () {
        $adjustment = PayrollAdjustment::factory()->create(['amount' => 750000]);

        expect($adjustment->amount_formatted)->toBe('Rp 750.000');
    });

    test('payroll adjustment has type label accessor', function () {
        $adjustment = PayrollAdjustment::factory()->bonus()->create();

        expect($adjustment->type_label)->toBe('Tunjangan');
    });

    test('payroll adjustment belongs to payroll', function () {
        $adjustment = PayrollAdjustment::factory()->create();

        expect($adjustment->payroll)->not->toBeNull();
    });

    test('bonus scope returns only bonus adjustments', function () {
        PayrollAdjustment::factory()->count(2)->bonus()->create();
        PayrollAdjustment::factory()->deduction()->create();

        $bonuses = PayrollAdjustment::query()->bonus()->get();

        expect($bonuses)->toHaveCount(2);
    });

    test('deduction scope returns only deduction adjustments', function () {
        PayrollAdjustment::factory()->count(3)->deduction()->create();
        PayrollAdjustment::factory()->bonus()->create();

        $deductions = PayrollAdjustment::query()->deduction()->get();

        expect($deductions)->toHaveCount(3);
    });
});
