<?php

namespace Tests\Unit\Support\Confirm;

use App\Models\User;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Tests\TestCase;

class ConfirmTokenTest extends TestCase
{
    use RefreshDatabase;

    private function login(): User
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        return $user;
    }

    public function test_issue_confirm_consume_is_the_happy_path(): void
    {
        $this->login();
        $token = ConfirmToken::issue('keyRemoved', ['fingerprint' => 'ab:cd'], 'ssh_key.remove', 'box · key', 7);

        // The modal confirms (pending → confirmed)…
        $this->assertSame('keyRemoved', ConfirmToken::confirm($token)['event']);

        // …then the handler consumes (confirmed → burned).
        $payload = ConfirmToken::consume($token, 'keyRemoved');
        $this->assertSame(['fingerprint' => 'ab:cd'], $payload['params']);
        $this->assertSame('ssh_key.remove', $payload['auditAction']);
        $this->assertSame('box · key', $payload['auditTarget']);
        $this->assertSame(7, $payload['serverId']);
    }

    public function test_a_freshly_issued_token_cannot_be_consumed_without_confirmation(): void
    {
        // The modal-bypass guard: issuing a token (opener) does NOT authorize apply.
        $this->login();
        $token = ConfirmToken::issue('twoFactorDisabled');

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::consume($token, 'twoFactorDisabled');
    }

    public function test_issue_rejects_an_event_outside_the_allowlist(): void
    {
        $this->login();

        $this->expectException(\InvalidArgumentException::class);
        ConfirmToken::issue('evilEvent', ['userId' => 1]);
    }

    public function test_a_token_can_be_confirmed_only_once(): void
    {
        $this->login();
        $token = ConfirmToken::issue('twoFactorDisabled');

        ConfirmToken::confirm($token);

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::confirm($token);
    }

    public function test_a_confirmed_token_is_consumed_only_once(): void
    {
        $this->login();
        $token = ConfirmToken::issue('twoFactorDisabled');
        ConfirmToken::confirm($token);

        ConfirmToken::consume($token, 'twoFactorDisabled');

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::consume($token, 'twoFactorDisabled');
    }

    public function test_consume_rejects_a_token_minted_for_a_different_action(): void
    {
        $this->login();
        $token = ConfirmToken::issue('fileConfirmed', ['name' => 'x']);
        ConfirmToken::confirm($token);

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::consume($token, 'keyRemoved');
    }

    public function test_confirm_rejects_a_garbage_token(): void
    {
        $this->login();

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::confirm('not-a-real-token');
    }

    public function test_consume_rejects_a_garbage_token(): void
    {
        $this->login();

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::consume('not-a-real-token', 'keyRemoved');
    }

    public function test_peek_returns_null_on_a_garbage_token_and_never_throws(): void
    {
        $this->login();

        $this->assertNull(ConfirmToken::peek('not-a-real-token'));
    }

    public function test_peek_exposes_the_audit_target_for_display(): void
    {
        $this->login();
        $token = ConfirmToken::issue('keyRemoved', [], 'ssh_key.remove', 'box · key', 7);

        $this->assertSame('box · key', ConfirmToken::peek($token)['auditTarget']);
    }

    public function test_a_token_is_bound_to_the_issuing_user(): void
    {
        $this->login();
        $token = ConfirmToken::issue('twoFactorDisabled');

        // A different operator must not be able to confirm Alice's token.
        Auth::login(User::factory()->create());

        $this->expectException(InvalidConfirmToken::class);
        ConfirmToken::confirm($token);
    }
}
