<?php

use App\Enums\PayrollAdjustmentType;
use App\Enums\PayrollStatus;
use App\Jobs\CheckAttendancePenaltiesJob;
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\Payroll;
use App\Models\PayrollAdjustment;
use App\Models\PayrollPeriod;
use App\Models\User;
use App\Settings\HRSettings;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;

uses(RefreshDatabase::class);

/*
|--------------------------------------------------------------------------
| HELPER
|--------------------------------------------------------------------------
*/

function createActiveEmployeeWithPayroll(int $year, int $month, PayrollStatus|string $status = PayrollStatus::UNPAID): array
{
    $period = PayrollPeriod::firstOrCreate(
        ['year' => $year, 'month' => $month],
        ['status' => 'open']
    );

    $user = User::factory()->create(['is_active' => true]);
    $employee = Employee::factory()->create([
        'user_id' => $user->id,
        'join_date' => Carbon::createFromDate($year, $month, 1)->subMonth()->toDateString(),
        'resign_date' => null,
        'employment_status' => 'full_time',
        'base_salary' => 5000000,
    ]);

    $payroll = Payroll::factory()->create([
        'payroll_period_id' => $period->id,
        'employee_id' => $employee->id,
        'base_salary' => $employee->base_salary,
        'bonus_amount' => 0,
        'deduction_amount' => 0,
        'total_amount' => $employee->base_salary,
        'status' => $status,
    ]);

    return compact('period', 'user', 'employee', 'payroll');
}

function setHrSettings(int $latePenalty, int $absentPenalty, string $checkInTime = '08:00'): void
{
    $settings = app(HRSettings::class);
    $settings->scheduled_check_in_time = $checkInTime;
    $settings->scheduled_check_out_time = '17:00';
    $settings->late_penalty_amount = $latePenalty;
    $settings->absent_penalty_amount = $absentPenalty;
    $settings->save();
}

/*
|--------------------------------------------------------------------------
| SKIP CONDITIONS
|--------------------------------------------------------------------------
*/

test('job skips when yesterday is sunday', function () {
    $sunday = Carbon::parse('2024-07-07'); // Sunday
    Carbon::setTestNow($sunday->copy()->addDay());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($sunday->year, $sunday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $sunday->toDateString(),
        'check_in_at' => $sunday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when yesterday is saturday', function () {
    $saturday = Carbon::parse('2024-07-06'); // Saturday
    Carbon::setTestNow($saturday->copy()->addDay());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($saturday->year, $saturday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $saturday->toDateString(),
        'check_in_at' => $saturday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when both penalties are zero', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(0, 0);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when no payroll period exists', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);

    $user = User::factory()->create(['is_active' => true]);
    $employee = Employee::factory()->create([
        'user_id' => $user->id,
        'join_date' => $yesterday->copy()->subMonth()->toDateString(),
        'resign_date' => null,
        'employment_status' => 'full_time',
        'base_salary' => 5000000,
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when no system user exists', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    User::query()->delete();

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips inactive employees', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    $data['user']->update(['is_active' => false]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips resigned employees', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    $data['employee']->update(['resign_date' => $yesterday->copy()->subDays(5)->toDateString()]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when payroll is already paid', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month, PayrollStatus::PAID);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job skips when payroll is cancelled', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month, PayrollStatus::CANCELLED);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

/*
|--------------------------------------------------------------------------
| LATE PENALTY
|--------------------------------------------------------------------------
*/

test('job creates late penalty when employee checks in late', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(8)->setMinute(30),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $data['payroll']->id,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 50000,
    ]);
});

test('job does not create late penalty when employee checks in on time', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setTime(8, 0, 0),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job does not create late penalty when employee checks in early', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setTime(7, 30, 0),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('job does not create late penalty when late_penalty_amount is zero', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(0, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('late penalty description includes date and minutes', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(8)->setMinute(30),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $adjustment = PayrollAdjustment::where('payroll_id', $data['payroll']->id)->first();
    expect($adjustment->description)->toContain('Denda keterlambatan');
    expect($adjustment->description)->toContain($yesterday->format('d/m/Y'));
    expect($adjustment->description)->toContain('30 menit');
});

test('late penalty links to attendance record', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    $attendance = Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $adjustment = PayrollAdjustment::where('payroll_id', $data['payroll']->id)->first();
    expect($adjustment->attendance_id)->toBe($attendance->id);
});

/*
|--------------------------------------------------------------------------
| ABSENT PENALTY
|--------------------------------------------------------------------------
*/

test('job creates absent penalty when employee has no attendance', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $data['payroll']->id,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 100000,
        'attendance_id' => null,
    ]);
});

test('job does not create absent penalty when absent_penalty_amount is zero', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 0);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 0);
});

test('absent penalty description includes date', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $adjustment = PayrollAdjustment::where('payroll_id', $data['payroll']->id)->first();
    expect($adjustment->description)->toContain('Denda ketidakhadiran');
    expect($adjustment->description)->toContain($yesterday->format('d/m/Y'));
});

test('absent penalty has null attendance_id', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $adjustment = PayrollAdjustment::where('payroll_id', $data['payroll']->id)->first();
    expect($adjustment->attendance_id)->toBeNull();
});

/*
|--------------------------------------------------------------------------
| DEDUPLICATION
|--------------------------------------------------------------------------
*/

test('job does not create duplicate late penalty', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    $attendance = Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    PayrollAdjustment::create([
        'payroll_id' => $data['payroll']->id,
        'created_by_id' => User::first()->id,
        'attendance_id' => $attendance->id,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 50000,
        'description' => 'Denda keterlambatan manual',
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 1);
});

test('job does not create duplicate absent penalty', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    PayrollAdjustment::create([
        'payroll_id' => $data['payroll']->id,
        'created_by_id' => User::first()->id,
        'attendance_id' => null,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 100000,
        'description' => 'Denda ketidakhadiran '.$yesterday->format('d/m/Y'),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseCount('payroll_adjustments', 1);
});

/*
|--------------------------------------------------------------------------
| PAYROLL RECALCULATION
|--------------------------------------------------------------------------
*/

test('job recalculates payroll total_amount after late penalty', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $data['payroll']->refresh();
    expect($data['payroll']->deduction_amount)->toBe(50000);
    expect($data['payroll']->total_amount)->toBe($data['payroll']->base_salary - 50000);
});

test('job recalculates payroll total_amount after absent penalty', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $data['payroll']->refresh();
    expect($data['payroll']->deduction_amount)->toBe(100000);
    expect($data['payroll']->total_amount)->toBe($data['payroll']->base_salary - 100000);
});

test('job recalculates payroll with existing bonus adjustments', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    PayrollAdjustment::create([
        'payroll_id' => $data['payroll']->id,
        'created_by_id' => User::first()->id,
        'attendance_id' => null,
        'type' => PayrollAdjustmentType::BONUS,
        'amount' => 200000,
        'description' => 'Bonus test',
    ]);

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $data['payroll']->refresh();
    expect($data['payroll']->bonus_amount)->toBe(200000);
    expect($data['payroll']->deduction_amount)->toBe(50000);
    expect($data['payroll']->total_amount)->toBe($data['payroll']->base_salary + 200000 - 50000);
});

/*
|--------------------------------------------------------------------------
| MULTIPLE EMPLOYEES
|--------------------------------------------------------------------------
*/

test('job processes multiple employees', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');

    $data1 = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);
    $data2 = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $data1['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $data1['payroll']->id,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 50000,
    ]);

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $data2['payroll']->id,
        'type' => PayrollAdjustmentType::DEDUCTION,
        'amount' => 100000,
    ]);
});

test('job handles mix of late and absent employees', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000, '08:00');

    $lateEmp = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);
    $absentEmp = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    Attendance::factory()->create([
        'employee_id' => $lateEmp['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $lateEmp['payroll']->id,
        'amount' => 50000,
    ]);

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $absentEmp['payroll']->id,
        'amount' => 100000,
    ]);
});

/*
|--------------------------------------------------------------------------
| EDGE CASES
|--------------------------------------------------------------------------
*/

test('job handles employee with no user gracefully', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    (new CheckAttendancePenaltiesJob)->handle();

    $this->assertDatabaseHas('payroll_adjustments', [
        'payroll_id' => $data['payroll']->id,
    ]);
});

test('job uses system user as created_by for adjustments', function () {
    $yesterday = now()->subDay();
    Carbon::setTestNow(now());

    setHrSettings(50000, 100000);
    $data = createActiveEmployeeWithPayroll($yesterday->year, $yesterday->month);

    $systemUser = User::first();

    Attendance::factory()->create([
        'employee_id' => $data['employee']->id,
        'attendance_date' => $yesterday->toDateString(),
        'check_in_at' => $yesterday->copy()->setHour(9),
    ]);

    (new CheckAttendancePenaltiesJob)->handle();

    $adjustment = PayrollAdjustment::where('payroll_id', $data['payroll']->id)->first();
    expect($adjustment->created_by_id)->toBe($systemUser->id);
});

test('job can be dispatched to queue', function () {
    Queue::fake();

    CheckAttendancePenaltiesJob::dispatch();

    Queue::assertPushed(CheckAttendancePenaltiesJob::class);
});

test('job has correct tries property', function () {
    $job = new CheckAttendancePenaltiesJob;
    expect($job->tries)->toBe(3);
});
