<?php

declare(strict_types=1);

namespace Tests\Feature\Audit;

use App\Enums\AuditEvent;
use App\Models\Asset;
use App\Models\AuditLog;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Facades\Excel;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class AuditLogTest extends TestCase
{
    use RefreshDatabase;

    #[Test]
    public function a_guest_is_sent_to_the_sign_in_screen(): void
    {
        $this->get(route('audit.index'))->assertRedirect(route('login'));
    }

    #[Test]
    public function any_signed_in_user_can_reach_it_no_master_flag_required(): void
    {
        $this->actingAs(User::factory()->create())
            ->get(route('audit.index'))
            ->assertOk();
    }

    #[Test]
    public function the_dashboard_cards_reflect_current_counts(): void
    {
        // Both User::factory() calls below are themselves audited (User is
        // Auditable) — the assertions compare against a baseline taken after
        // both accounts exist, rather than against literal counts, so the
        // test does not have to know or care how many incidental entries
        // creating a user produces.
        $user = User::factory()->create();
        $viewer = User::factory()->create();
        $baseline = AuditLog::query()->count();
        $baselineToday = AuditLog::query()->whereDate('created_at', today())->count();

        $this->makeLog(['user_id' => $user->id, 'event' => AuditEvent::Login, 'description' => 'A'], now());
        $this->makeLog(['user_id' => $user->id, 'event' => AuditEvent::Login, 'description' => 'B'], now()->subDays(2));
        $this->makeLog(['user_id' => $user->id, 'event' => AuditEvent::Login, 'description' => 'C'], now()->subDays(40));

        $response = $this->actingAs($viewer)->get(route('audit.index'));

        $response->assertOk();
        $this->assertSame($baseline + 3, $response->viewData('cards')['total']);
        $this->assertSame($baselineToday + 1, $response->viewData('cards')['today']);
    }

    #[Test]
    public function the_listing_can_be_searched(): void
    {
        AuditLog::create(['event' => AuditEvent::Updated, 'description' => 'Renamed the finance printer']);
        AuditLog::create(['event' => AuditEvent::Updated, 'description' => 'Unrelated entry']);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.index', ['search' => 'finance printer']))
            ->assertOk()
            ->assertSee('Renamed the finance printer')
            ->assertDontSee('Unrelated entry');
    }

    #[Test]
    public function the_listing_can_be_filtered_by_user(): void
    {
        $alice = User::factory()->create(['name' => 'Alice Admin']);
        $bob = User::factory()->create(['name' => 'Bob Builder']);

        AuditLog::create(['user_id' => $alice->id, 'event' => AuditEvent::Login, 'description' => 'Alice event']);
        AuditLog::create(['user_id' => $bob->id, 'event' => AuditEvent::Login, 'description' => 'Bob event']);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.index', ['user_id' => $alice->id]))
            ->assertOk()
            ->assertSee('Alice event')
            ->assertDontSee('Bob event');
    }

    #[Test]
    public function the_listing_can_be_filtered_by_module(): void
    {
        $asset = Asset::factory()->create();
        $employee = Employee::factory()->create();

        AuditLog::create(['auditable_type' => Asset::class, 'auditable_id' => $asset->id, 'event' => AuditEvent::Updated, 'description' => 'Asset event marker']);
        AuditLog::create(['auditable_type' => Employee::class, 'auditable_id' => $employee->id, 'event' => AuditEvent::Updated, 'description' => 'Employee event marker']);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.index', ['module' => Asset::class]))
            ->assertOk()
            ->assertSee('Asset event marker')
            ->assertDontSee('Employee event marker');
    }

    #[Test]
    public function the_listing_can_be_filtered_by_action(): void
    {
        AuditLog::create(['event' => AuditEvent::Login, 'description' => 'Login marker']);
        AuditLog::create(['event' => AuditEvent::Logout, 'description' => 'Logout marker']);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.index', ['event' => AuditEvent::Login->value]))
            ->assertOk()
            ->assertSee('Login marker')
            ->assertDontSee('Logout marker');
    }

    #[Test]
    public function the_listing_can_be_filtered_by_date_range(): void
    {
        $this->makeLog(['event' => AuditEvent::Login, 'description' => 'Recent marker'], now());
        $this->makeLog(['event' => AuditEvent::Login, 'description' => 'Old marker'], now()->subYear());

        $this->actingAs(User::factory()->create())
            ->get(route('audit.index', ['date_from' => now()->subDay()->toDateString()]))
            ->assertOk()
            ->assertSee('Recent marker')
            ->assertDontSee('Old marker');
    }

    #[Test]
    public function viewing_details_shows_the_before_and_after_of_a_change(): void
    {
        $log = AuditLog::create([
            'event' => AuditEvent::Updated,
            'description' => 'Updated Asset AUM-LAP-0001',
            'old_values' => ['status' => 'in_stock'],
            'new_values' => ['status' => 'assigned'],
        ]);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.show', $log))
            ->assertOk()
            ->assertSee('in_stock')
            ->assertSee('assigned');
    }

    #[Test]
    public function an_entry_with_no_diff_shows_no_changes_panel(): void
    {
        $log = AuditLog::create(['event' => AuditEvent::Login, 'description' => 'A sign-in has no diff']);

        $this->actingAs(User::factory()->create())
            ->get(route('audit.show', $log))
            ->assertOk()
            ->assertDontSee('Before');
    }

    #[Test]
    public function an_unknown_log_id_404s(): void
    {
        $this->actingAs(User::factory()->create())
            ->get(route('audit.show', 999999))
            ->assertNotFound();
    }

    #[Test]
    public function exporting_honours_the_current_filters(): void
    {
        Excel::fake();
        $this->travelTo($frozen = now());

        $this->actingAs(User::factory()->create())
            ->get(route('audit.export', ['event' => AuditEvent::Login->value]))
            ->assertOk();

        Excel::assertDownloaded(
            'aumni-ams-audit-logs-'.$frozen->format('Y-m-d-His').'.xlsx',
        );
    }

    /**
     * `created_at` is deliberately not fillable — AuditLog::create() writes
     * it itself, matching a real event's timestamp. Tests that need a
     * specific age set it by direct property assignment afterward, which
     * bypasses the mass-assignment guard the way a single trusted field
     * always can.
     *
     * @param  array<string, mixed>  $attributes
     */
    private function makeLog(array $attributes, ?Carbon $createdAt = null): AuditLog
    {
        $log = AuditLog::create($attributes);

        if ($createdAt !== null) {
            $log->created_at = $createdAt;
            $log->save();
        }

        return $log;
    }
}
