<?php

namespace Tests\Feature;

use App\Models\Attendance;
use App\Models\AttendanceImport;
use App\Models\AttendanceImportRow;
use App\Models\Company;
use App\Models\Holiday;
use App\Models\Shift;
use App\Models\User;
use App\Services\TenantRoleService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;

class AttendanceImportTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->withoutMiddleware([
            \App\Http\Middleware\CheckTenantPermission::class,
            \App\Http\Middleware\CheckSubscriptionModule::class,
            \App\Http\Middleware\EnsureCompanySubscriptionAccess::class,
            \App\Http\Middleware\EnsureCompanyTwoFactorEnrolled::class,
            \App\Http\Middleware\TouchUserSession::class,
            \App\Http\Middleware\TenantMiddleware::class,
            \App\Http\Middleware\EnsureTenantUser::class,
            \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
        ]);
    }

    public function test_download_template_sample_aligns_with_headers(): void
    {
        [$actor] = $this->createActor();

        $service = app(\App\Services\AttendanceImportService::class);
        $this->assertSame(
            count($service->templateHeaders()),
            count($service->templateSampleRow())
        );

        $response = $this->actingAs($actor)->get(route('attendance.import.template'));
        $response->assertOk();

        $lines = array_values(array_filter(preg_split("/\r\n|\n|\r/", trim($response->streamedContent())) ?: []));
        $this->assertGreaterThanOrEqual(2, count($lines));

        $headers = str_getcsv($lines[0]);
        $sample = str_getcsv($lines[1]);
        $this->assertSame($service->templateHeaders(), $headers);
        $this->assertSame(count($headers), count($sample));
        $this->assertSame('present', $sample[array_search('attendance_status', $headers, true)]);
    }

    public function test_upload_builds_preview_with_valid_invalid_and_duplicate_rows(): void
    {
        Storage::fake('local');
        [$actor, $employee] = $this->createActorWithEmployee();

        $date = Carbon::yesterday()->toDateString();
        Attendance::query()->create([
            'company_id' => $actor->company_id,
            'user_id' => $employee->id,
            'date' => $date,
            'clock_in_date_time' => $date.' 09:00:00',
            'clock_in_time' => '09:00:00',
            'status' => 'present',
        ]);

        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            [$employee->employee_number, $date, '09:00', '18:00', 'present'],
            ['MISSING-CODE', $date, '09:00', '18:00', 'present'],
            ['', $date, '09:00', '18:00', 'present'],
        ]);

        $response = $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ]);

        $response->assertSessionHasNoErrors();
        $import = AttendanceImport::query()->latest('id')->firstOrFail();
        $response->assertRedirect(route('attendance.import.preview', $import));

        $this->assertSame(AttendanceImport::STATUS_PREVIEW, $import->status);
        $this->assertSame(1, (int) $import->duplicate_rows);
        $this->assertSame(2, (int) $import->invalid_rows);
        $this->assertSame(0, (int) $import->valid_rows);
    }

    public function test_confirm_imports_present_and_computes_late_from_shift(): void
    {
        Storage::fake('local');
        [$actor, $employee, $shift] = $this->createActorWithEmployee(withShift: true);

        $date = Carbon::yesterday()->toDateString();
        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            [$employee->employee_number, $date, '10:00', '18:00', 'present'],
        ]);

        $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ])->assertSessionHasNoErrors();

        $import = AttendanceImport::query()->latest('id')->firstOrFail();
        $this->assertSame(1, (int) $import->valid_rows);

        $this->actingAs($actor)
            ->post(route('attendance.import.confirm', $import))
            ->assertRedirect(route('attendance.import.show', $import));

        $import->refresh();
        $this->assertSame(AttendanceImport::STATUS_COMPLETED, $import->status);
        $this->assertSame(1, (int) $import->success_rows);

        $attendance = Attendance::query()
            ->where('company_id', $actor->company_id)
            ->where('user_id', $employee->id)
            ->where('date', $date)
            ->firstOrFail();

        $this->assertSame('present', $attendance->status);
        $this->assertTrue((bool) $attendance->is_late);
        $this->assertSame('10:00:00', $attendance->clock_in_time);
        $this->assertSame('18:00:00', $attendance->clock_out_time);
        $this->assertNotNull($shift->id);
    }

    public function test_confirm_imports_leave_and_absent_status(): void
    {
        Storage::fake('local');
        [$actor, $employee] = $this->createActorWithEmployee();

        $leaveDate = Carbon::yesterday()->toDateString();
        $absentDate = Carbon::yesterday()->subDay()->toDateString();

        Attendance::query()->create([
            'company_id' => $actor->company_id,
            'user_id' => $employee->id,
            'date' => $absentDate,
            'clock_in_date_time' => null,
            'status' => 'leave',
            'is_leave' => true,
        ]);

        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            [$employee->employee_number, $leaveDate, '', '', 'leave'],
            [$employee->employee_number, $absentDate, '', '', 'absent'],
        ]);

        $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ])->assertSessionHasNoErrors();

        $import = AttendanceImport::query()->latest('id')->firstOrFail();
        $this->assertSame(2, (int) $import->valid_rows);

        $this->actingAs($actor)->post(route('attendance.import.confirm', $import))->assertRedirect();

        $this->assertDatabaseHas('attendances', [
            'company_id' => $actor->company_id,
            'user_id' => $employee->id,
            'date' => $leaveDate,
            'status' => 'leave',
            'is_leave' => 1,
        ]);
        $this->assertDatabaseMissing('attendances', [
            'company_id' => $actor->company_id,
            'user_id' => $employee->id,
            'date' => $absentDate,
        ]);
    }

    public function test_in_file_duplicate_and_holiday_are_rejected(): void
    {
        Storage::fake('local');
        [$actor, $employee] = $this->createActorWithEmployee();

        $date = Carbon::yesterday()->toDateString();
        Holiday::query()->create([
            'company_id' => $actor->company_id,
            'name' => 'Test Holiday',
            'date' => $date,
        ]);

        $otherDate = Carbon::yesterday()->subDay()->toDateString();
        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            [$employee->employee_number, $otherDate, '09:00', '18:00', 'present'],
            [$employee->employee_number, $otherDate, '09:30', '18:00', 'present'],
            [$employee->employee_number, $date, '09:00', '18:00', 'present'],
        ]);

        $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ])->assertSessionHasNoErrors();

        $import = AttendanceImport::query()->latest('id')->firstOrFail();
        $this->assertSame(1, (int) $import->valid_rows);
        $this->assertSame(1, (int) $import->duplicate_rows);
        $this->assertSame(1, (int) $import->invalid_rows);

        $holidayRow = AttendanceImportRow::query()
            ->where('attendance_import_id', $import->id)
            ->where('attendance_date', $date)
            ->firstOrFail();
        $this->assertSame(AttendanceImportRow::STATUS_INVALID, $holidayRow->status);
        $this->assertStringContainsString('Holiday', (string) $holidayRow->error_message);
    }

    public function test_error_report_download_contains_failed_rows(): void
    {
        Storage::fake('local');
        [$actor, $employee] = $this->createActorWithEmployee();

        $date = Carbon::yesterday()->toDateString();
        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            ['NOPE', $date, '09:00', '18:00', 'present'],
            [$employee->employee_number, $date, '09:00', '18:00', 'present'],
        ]);

        $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ]);

        $import = AttendanceImport::query()->latest('id')->firstOrFail();
        $this->actingAs($actor)->post(route('attendance.import.confirm', $import));

        $response = $this->actingAs($actor)->get(route('attendance.import.errors', $import));
        $response->assertOk();
        $content = $response->streamedContent();
        $this->assertStringContainsString('NOPE', $content);
        $this->assertStringContainsString('invalid', $content);
    }

    public function test_staff_cannot_access_import(): void
    {
        [$actor, $employee] = $this->createActorWithEmployee();

        $this->actingAs($employee)->get(route('attendance.import.index'))->assertForbidden();
        $this->actingAs($employee)->get(route('attendance.import.template'))->assertForbidden();
    }

    public function test_company_isolation_on_show(): void
    {
        Storage::fake('local');
        [$actor, $employee] = $this->createActorWithEmployee();

        $date = Carbon::yesterday()->toDateString();
        $csv = $this->makeCsv([
            ['employee_code', 'attendance_date', 'check_in_time', 'check_out_time', 'attendance_status'],
            [$employee->employee_number, $date, '09:00', '18:00', 'present'],
        ]);
        $this->actingAs($actor)->post(route('attendance.import.store'), [
            'file' => UploadedFile::fake()->createWithContent('attendance.csv', $csv),
        ]);
        $import = AttendanceImport::query()->latest('id')->firstOrFail();

        $otherCompany = Company::query()->create([
            'name' => 'Other Co',
            'short_name' => 'OTH',
            'email' => 'other-'.Str::uuid().'@demo.local',
            'status' => 'active',
        ]);
        $roles = TenantRoleService::syncAllDefaultRolesForCompany((int) $otherCompany->id);
        $otherAdmin = User::factory()->create([
            'company_id' => $otherCompany->id,
            'role_id' => $roles['admin']->id,
            'user_type' => User::USER_TYPE_ADMIN,
            'status' => User::STATUS_ACTIVE,
            'is_superadmin' => false,
        ]);

        $this->actingAs($otherAdmin)->get(route('attendance.import.show', $import))->assertNotFound();
    }

    /**
     * @return array{0: User}
     */
    private function createActor(): array
    {
        [$actor] = $this->createActorWithEmployee(createEmployee: false);

        return [$actor];
    }

    /**
     * @return array{0: User, 1: User, 2?: Shift}
     */
    private function createActorWithEmployee(bool $withShift = false, bool $createEmployee = true): array
    {
        $suffix = strtoupper(Str::random(4));
        $company = Company::query()->create([
            'name' => 'Attendance Import Co '.$suffix,
            'short_name' => 'A'.$suffix,
            'email' => 'att-import-'.Str::uuid().'@demo.local',
            'status' => 'active',
        ]);
        $roles = TenantRoleService::syncAllDefaultRolesForCompany((int) $company->id);

        $actor = User::factory()->create([
            'company_id' => $company->id,
            'role_id' => $roles['admin']->id,
            'user_type' => User::USER_TYPE_ADMIN,
            'status' => User::STATUS_ACTIVE,
            'is_superadmin' => false,
        ]);

        if (! $createEmployee) {
            return [$actor, $actor];
        }

        $shift = null;
        if ($withShift) {
            $shift = Shift::query()->create([
                'company_id' => $company->id,
                'name' => 'General',
                'clock_in_time' => '09:00:00',
                'clock_out_time' => '18:00:00',
                'grace_period_minutes' => 15,
                'early_going_grace_minutes' => 15,
                'is_active' => true,
            ]);
        }

        $employee = User::factory()->create([
            'company_id' => $company->id,
            'role_id' => $roles['employee']->id,
            'user_type' => User::USER_TYPE_STAFF,
            'status' => User::STATUS_ACTIVE,
            'employee_number' => 'EMP'.strtoupper(Str::random(5)),
            'shift_id' => $shift?->id,
            'allow_login' => true,
        ]);

        return $withShift ? [$actor, $employee, $shift] : [$actor, $employee];
    }

    /**
     * @param  array<int, array<int, string>>  $rows
     */
    private function makeCsv(array $rows): string
    {
        $handle = fopen('php://temp', 'r+');
        foreach ($rows as $row) {
            fputcsv($handle, $row);
        }
        rewind($handle);
        $csv = stream_get_contents($handle) ?: '';
        fclose($handle);

        return $csv;
    }
}
