<?php

namespace Tests\Feature;

use App\Models\Attendance;
use App\Models\AttendancePunch;
use App\Models\BiometricDevice;
use App\Models\Company;
use App\Models\SubscriptionPlan;
use App\Models\User;
use App\Models\WorkDuration;
use App\Services\TenantRoleService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Tests\TestCase;

class BiometricFirstInLastOutTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @return array{company: Company, device: BiometricDevice, password: string, employee: User, sn: string, empCode: string}
     */
    private function seedBothDevice(): array
    {
        $plan = SubscriptionPlan::query()->create([
            'name' => 'Filo Plan '.Str::random(6),
            'base_price_monthly' => 0,
            'active' => true,
            'modules' => ['attendance', 'employees', 'dashboard'],
        ]);

        $company = Company::query()->create([
            'name' => 'Filo Co '.Str::random(4),
            'short_name' => 'F'.Str::upper(Str::random(3)),
            'email' => 'filo-'.Str::lower(Str::random(8)).'@example.com',
            'status' => 'active',
            'subscription_plan_id' => $plan->id,
        ]);
        TenantRoleService::syncAllDefaultRolesForCompany((int) $company->id);

        $password = 'test-device-secret-password-123';
        $sn = 'SN-'.Str::upper(Str::random(10));
        $empCode = 'E'.Str::upper(Str::random(6));

        $device = BiometricDevice::withoutGlobalScopes()->create([
            'company_id' => $company->id,
            'terminal_sn' => $sn,
            'terminal_alias' => 'Both Gate',
            'api_username' => 'dev_'.Str::lower(Str::random(10)),
            'api_password_hash' => Hash::make($password),
            'is_active' => true,
            'punch_mode' => BiometricDevice::PUNCH_MODE_BOTH,
        ]);

        $employee = User::factory()->create([
            'company_id' => $company->id,
            'user_type' => User::USER_TYPE_STAFF,
            'status' => User::STATUS_ACTIVE,
            'employee_number' => $empCode,
            'allow_login' => true,
        ]);

        return compact('company', 'device', 'password', 'employee', 'sn', 'empCode');
    }

    private function postPunches(string $username, string $password, array $payload)
    {
        return $this->withHeaders([
            'Authorization' => 'Basic '.base64_encode($username.':'.$password),
            'Accept' => 'application/json',
            'Content-Type' => 'application/json',
        ])->postJson('/api/integrations/zkteco/attendance', $payload);
    }

    public function test_single_punch_is_first_in_only(): void
    {
        $ctx = $this->seedBothDevice();

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [[
            'emp_code' => $ctx['empCode'],
            'punch_datetime' => '2026-09-28 09:00:00',
            'punch_state' => 'Check In',
            'terminal_sn' => $ctx['sn'],
        ]])->assertStatus(202)->assertJson(['accepted' => 1]);

        $punch = AttendancePunch::withoutGlobalScopes()->where('company_id', $ctx['company']->id)->first();
        $this->assertSame(AttendancePunch::TYPE_FIRST_IN, $punch->processed_type);
        $this->assertFalse((bool) $punch->is_duplicate);

        $att = Attendance::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->where('user_id', $ctx['employee']->id)
            ->first();
        $this->assertNotNull($att);
        $this->assertSame('09:00:00', substr((string) $att->clock_in_time, 0, 8));
        $this->assertNull($att->clock_out_date_time);
        $this->assertNull($att->total_duration);
    }

    public function test_two_punches_set_first_in_and_last_out(): void
    {
        $ctx = $this->seedBothDevice();

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 18:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
        ])->assertJson(['accepted' => 2]);

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();
        $this->assertSame([
            AttendancePunch::TYPE_FIRST_IN,
            AttendancePunch::TYPE_LAST_OUT,
        ], $types);

        $att = Attendance::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->where('user_id', $ctx['employee']->id)
            ->first();
        $this->assertSame('09:00:00', substr((string) $att->clock_in_time, 0, 8));
        $this->assertSame('18:00:00', substr((string) $att->clock_out_time, 0, 8));
        $this->assertSame(9 * 3600, (int) $att->total_duration);
    }

    public function test_multiple_punches_mark_intermediates(): void
    {
        $ctx = $this->seedBothDevice();

        $times = ['09:00:00', '12:00:00', '14:00:00', '18:00:00'];
        $rows = [];
        foreach ($times as $t) {
            $rows[] = [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 '.$t,
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ];
        }
        $this->postPunches($ctx['device']->api_username, $ctx['password'], $rows)->assertJson(['accepted' => 4]);

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();
        $this->assertSame([
            AttendancePunch::TYPE_FIRST_IN,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_LAST_OUT,
        ], $types);

        $att = Attendance::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->where('user_id', $ctx['employee']->id)
            ->first();
        $this->assertSame('09:00:00', substr((string) $att->clock_in_time, 0, 8));
        $this->assertSame('18:00:00', substr((string) $att->clock_out_time, 0, 8));
    }

    public function test_rapid_repeats_are_duplicates(): void
    {
        $ctx = $this->seedBothDevice();

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:01',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:03',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:05',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
        ])->assertJson(['accepted' => 3]);

        $punches = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->get();
        $this->assertSame(AttendancePunch::TYPE_FIRST_IN, $punches[0]->processed_type);
        $this->assertSame(AttendancePunch::TYPE_DUPLICATE, $punches[1]->processed_type);
        $this->assertSame(AttendancePunch::TYPE_DUPLICATE, $punches[2]->processed_type);
        $this->assertTrue((bool) $punches[1]->is_duplicate);

        $att = Attendance::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->where('user_id', $ctx['employee']->id)
            ->first();
        $this->assertNull($att->clock_out_date_time);
    }

    public function test_new_punch_demotes_previous_last_out(): void
    {
        $ctx = $this->seedBothDevice();

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 18:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
        ])->assertJson(['accepted' => 2]);

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [[
            'emp_code' => $ctx['empCode'],
            'punch_datetime' => '2026-09-28 19:00:00',
            'punch_state' => 'Check In',
            'terminal_sn' => $ctx['sn'],
        ]])->assertJson(['accepted' => 1]);

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();
        $this->assertSame([
            AttendancePunch::TYPE_FIRST_IN,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_LAST_OUT,
        ], $types);

        $att = Attendance::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->where('user_id', $ctx['employee']->id)
            ->first();
        $this->assertSame('19:00:00', substr((string) $att->clock_out_time, 0, 8));
    }

    public function test_manual_time_lock_preserves_admin_times(): void
    {
        $ctx = $this->seedBothDevice();

        $att = Attendance::withoutGlobalScopes()->create([
            'company_id' => $ctx['company']->id,
            'user_id' => $ctx['employee']->id,
            'date' => '2026-09-28',
            'status' => 'present',
            'clock_in_date_time' => '2026-09-28 08:00:00',
            'clock_in_time' => '08:00:00',
            'clock_out_date_time' => '2026-09-28 17:00:00',
            'clock_out_time' => '17:00:00',
            'total_duration' => 9 * 3600,
            'manual_time_lock' => true,
        ]);

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 18:00:00',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
            ],
        ])->assertJson(['accepted' => 2]);

        $att->refresh();
        $this->assertSame('08:00:00', substr((string) $att->clock_in_time, 0, 8));
        $this->assertSame('17:00:00', substr((string) $att->clock_out_time, 0, 8));

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();
        $this->assertSame([
            AttendancePunch::TYPE_FIRST_IN,
            AttendancePunch::TYPE_LAST_OUT,
        ], $types);
    }

    public function test_device_transaction_id_is_idempotent(): void
    {
        $ctx = $this->seedBothDevice();

        $payload = [[
            'emp_code' => $ctx['empCode'],
            'punch_datetime' => '2026-09-28 09:15:00',
            'punch_state' => 'Check In',
            'terminal_sn' => $ctx['sn'],
            'device_transaction_id' => 'TXN-ABC-1',
        ]];

        $this->postPunches($ctx['device']->api_username, $ctx['password'], $payload)
            ->assertJson(['accepted' => 1]);
        $this->postPunches($ctx['device']->api_username, $ctx['password'], $payload)
            ->assertJson(['accepted' => 0, 'duplicates' => 1]);

        $this->assertSame(
            1,
            AttendancePunch::withoutGlobalScopes()->where('company_id', $ctx['company']->id)->count()
        );
    }

    public function test_login_mode_does_not_use_filo_classification(): void
    {
        $ctx = $this->seedBothDevice();
        $ctx['device']->update(['punch_mode' => BiometricDevice::PUNCH_MODE_LOGIN]);

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 09:00:00',
                'punch_state' => 'Check Out',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 12:00:00',
                'punch_state' => 'Check Out',
                'terminal_sn' => $ctx['sn'],
            ],
            [
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => '2026-09-28 18:00:00',
                'punch_state' => 'Check Out',
                'terminal_sn' => $ctx['sn'],
            ],
        ])->assertJson(['accepted' => 3]);

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();

        $this->assertNotContains(AttendancePunch::TYPE_INTERMEDIATE, $types);
        $this->assertNotContains(AttendancePunch::TYPE_LAST_OUT, $types);
        $this->assertNotContains(AttendancePunch::TYPE_FIRST_IN, $types);
        $this->assertNotContains(AttendancePunch::TYPE_DUPLICATE, $types);

        $statuses = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->pluck('status')
            ->unique()
            ->sort()
            ->values()
            ->all();
        $this->assertContains(AttendancePunch::STATUS_APPLIED, $statuses);
        foreach ($statuses as $status) {
            $this->assertContains($status, [
                AttendancePunch::STATUS_APPLIED,
                AttendancePunch::STATUS_IGNORED,
            ]);
        }
    }

    public function test_filo_reclassifies_legacy_punches_and_clears_toggle_sessions(): void
    {
        $ctx = $this->seedBothDevice();

        $att = Attendance::withoutGlobalScopes()->create([
            'company_id' => $ctx['company']->id,
            'user_id' => $ctx['employee']->id,
            'date' => '2026-09-28',
            'status' => 'present',
            'clock_in_date_time' => '2026-09-28 09:48:30',
            'clock_in_time' => '09:48:30',
            'clock_in_method' => 'biometric',
        ]);

        $legacyTimes = [
            '2026-09-28 09:48:30',
            '2026-09-28 09:50:26',
            '2026-09-28 09:52:42',
            '2026-09-28 10:05:31',
        ];
        foreach ($legacyTimes as $i => $dt) {
            $punch = AttendancePunch::withoutGlobalScopes()->create([
                'company_id' => $ctx['company']->id,
                'biometric_device_id' => $ctx['device']->id,
                'user_id' => $ctx['employee']->id,
                'emp_code' => $ctx['empCode'],
                'punch_datetime' => $dt,
                'punch_date' => '2026-09-28',
                'punch_state' => 'Check In',
                'terminal_sn' => $ctx['sn'],
                'status' => AttendancePunch::STATUS_APPLIED,
                'attendance_id' => $att->id,
                'attendance_date' => null,
                'processed_type' => null,
            ]);
            if ($i % 2 === 0) {
                WorkDuration::withoutGlobalScopes()->create([
                    'company_id' => $ctx['company']->id,
                    'attendance_id' => $att->id,
                    'start_time' => substr($dt, 11),
                    'end_time' => $i + 1 < count($legacyTimes) ? substr($legacyTimes[$i + 1], 11) : null,
                    'duration' => 120,
                    'status' => 'closed',
                    'notes' => 'zkteco:'.$punch->id,
                ]);
            }
        }

        $this->postPunches($ctx['device']->api_username, $ctx['password'], [[
            'emp_code' => $ctx['empCode'],
            'punch_datetime' => '2026-09-28 11:36:03',
            'punch_state' => 'Check In',
            'terminal_sn' => $ctx['sn'],
        ]])->assertJson(['accepted' => 1]);

        $types = AttendancePunch::withoutGlobalScopes()
            ->where('company_id', $ctx['company']->id)
            ->orderBy('punch_datetime')
            ->pluck('processed_type')
            ->all();
        $this->assertSame([
            AttendancePunch::TYPE_FIRST_IN,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_INTERMEDIATE,
            AttendancePunch::TYPE_LAST_OUT,
        ], $types);

        $sessions = WorkDuration::withoutGlobalScopes()
            ->where('attendance_id', $att->id)
            ->get();
        $this->assertCount(1, $sessions);
        $this->assertSame('zkteco-filo:'.$att->id, $sessions->first()->notes);
        $this->assertSame('09:48:30', substr((string) $sessions->first()->start_time, 0, 8));
        $this->assertSame('11:36:03', substr((string) $sessions->first()->end_time, 0, 8));

        $att->refresh();
        $this->assertSame('09:48:30', substr((string) $att->clock_in_time, 0, 8));
        $this->assertSame('11:36:03', substr((string) $att->clock_out_time, 0, 8));
    }
}
