<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Enums\AssetCondition;
use App\Enums\AssetStatus;
use App\Enums\AssignmentStatus;
use App\Enums\AuditEvent;
use App\Enums\EmployeeStatus;
use App\Enums\HasBadge;
use App\Enums\ImportStatus;
use App\Enums\WarrantyType;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class EnumTest extends TestCase
{
    /**
     * @return array<string, array{0: class-string}>
     */
    public static function badgeEnums(): array
    {
        return [
            'AssetStatus' => [AssetStatus::class],
            'AssetCondition' => [AssetCondition::class],
            'AssignmentStatus' => [AssignmentStatus::class],
            'EmployeeStatus' => [EmployeeStatus::class],
            'WarrantyType' => [WarrantyType::class],
            'AuditEvent' => [AuditEvent::class],
            'ImportStatus' => [ImportStatus::class],
        ];
    }

    /**
     * @param  class-string  $enum
     */
    #[Test]
    #[DataProvider('badgeEnums')]
    public function every_case_has_a_label_a_colour_and_an_icon(string $enum): void
    {
        foreach ($enum::cases() as $case) {
            $this->assertInstanceOf(HasBadge::class, $case);
            $this->assertNotSame('', $case->label(), "{$enum}::{$case->name} has no label.");
            $this->assertNotSame('', $case->color(), "{$enum}::{$case->name} has no colour.");
            $this->assertNotSame('', $case->icon(), "{$enum}::{$case->name} has no icon.");
        }
    }

    /**
     * @param  class-string  $enum
     */
    #[Test]
    #[DataProvider('badgeEnums')]
    public function options_covers_every_case(string $enum): void
    {
        $this->assertCount(count($enum::cases()), $enum::options());
    }

    #[Test]
    public function only_an_in_stock_asset_can_be_assigned(): void
    {
        $this->assertTrue(AssetStatus::InStock->canBeAssigned());

        foreach (AssetStatus::cases() as $status) {
            if ($status !== AssetStatus::InStock) {
                $this->assertFalse(
                    $status->canBeAssigned(),
                    "{$status->value} must not be assignable.",
                );
            }
        }
    }

    #[Test]
    public function a_disposed_asset_is_terminal(): void
    {
        $this->assertTrue(AssetStatus::Disposed->isTerminal());
        $this->assertSame([], AssetStatus::Disposed->allowedTransitions());
    }

    #[Test]
    public function an_asset_can_never_be_moved_directly_into_the_assigned_state(): void
    {
        // Assignment happens only through AssignmentService, inside a
        // transaction that also writes the custody record.
        foreach (AssetStatus::cases() as $status) {
            $this->assertNotContains(
                AssetStatus::Assigned,
                $status->allowedTransitions(),
                "{$status->value} must not offer a direct move to assigned.",
            );
        }
    }

    #[Test]
    public function a_disposed_asset_cannot_return_to_stock(): void
    {
        $this->assertFalse(AssetStatus::Disposed->canTransitionTo(AssetStatus::InStock));
    }

    #[Test]
    public function a_repaired_asset_can_return_to_stock(): void
    {
        $this->assertTrue(AssetStatus::UnderRepair->canTransitionTo(AssetStatus::InStock));
    }

    #[Test]
    public function no_transition_target_is_listed_twice(): void
    {
        foreach (AssetStatus::cases() as $status) {
            $targets = $status->allowedTransitions();

            $this->assertSame(
                count($targets),
                count(array_unique(array_column($targets, 'value'))),
                "{$status->value} lists a duplicate transition target.",
            );
        }
    }

    #[Test]
    public function return_condition_decides_where_the_asset_lands(): void
    {
        $this->assertSame(AssetStatus::InStock, AssetCondition::Good->statusOnReturn());
        $this->assertSame(AssetStatus::InStock, AssetCondition::Fair->statusOnReturn());

        // A damaged laptop must never silently re-enter the assignable pool.
        $this->assertSame(AssetStatus::UnderRepair, AssetCondition::Poor->statusOnReturn());
        $this->assertSame(AssetStatus::Damaged, AssetCondition::Unusable->statusOnReturn());
    }

    #[Test]
    public function an_exited_employee_cannot_receive_assets(): void
    {
        $this->assertTrue(EmployeeStatus::Active->canReceiveAssets());
        $this->assertTrue(EmployeeStatus::OnNotice->canReceiveAssets());
        $this->assertFalse(EmployeeStatus::Exited->canReceiveAssets());
        $this->assertFalse(EmployeeStatus::Inactive->canReceiveAssets());
    }

    #[Test]
    public function employees_on_notice_or_exited_require_clearance(): void
    {
        $this->assertTrue(EmployeeStatus::OnNotice->requiresClearance());
        $this->assertTrue(EmployeeStatus::Exited->requiresClearance());
        $this->assertFalse(EmployeeStatus::Active->requiresClearance());
    }

    #[Test]
    public function import_status_reports_completion_correctly(): void
    {
        $this->assertFalse(ImportStatus::Pending->isFinished());
        $this->assertFalse(ImportStatus::Processing->isFinished());
        $this->assertTrue(ImportStatus::Completed->isFinished());
        $this->assertTrue(ImportStatus::CompletedWithErrors->isFinished());
        $this->assertTrue(ImportStatus::Failed->isFinished());
    }
}
