<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Models\User;
use App\Services\FleetService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

/**
 * Defense-in-depth: EnsureSecurityOnboarded must be re-applied to Livewire component
 * updates, not just the initial page GET. Livewire only re-runs route middleware that is
 * registered as "persistent"; otherwise a component mounted while onboarded keeps running
 * destructive actions on the /livewire/update endpoint even after the account is reverted
 * to must_change_password mid-session (Codex finding on the SSH-key-provision feature).
 *
 * This exercises the REAL update endpoint — Livewire skips persistent middleware for
 * Livewire::test() fake requests, so a faithful test has to replay a page-mounted snapshot
 * through a genuine POST /livewire/update.
 */
class PersistentSecurityGateTest extends TestCase
{
    use RefreshDatabase;

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    public function test_reverted_user_cannot_run_destructive_livewire_action_on_a_mounted_component(): void
    {
        $user = User::factory()->create(['must_change_password' => false]);
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        // The destructive SSH action must never be reached once the gate re-runs.
        $fleet = Mockery::mock(FleetService::class)->shouldIgnoreMissing();
        $fleet->shouldReceive('removeAuthorizedKey')->never();
        app()->instance(FleetService::class, $fleet);

        // 1. Mount the panel page while fully onboarded — the rendered snapshot carries
        //    memo.path = servers/{uuid}, the route that bears EnsureSecurityOnboarded.
        $html = $this->actingAs($user)
            ->get(route('servers.show', $server))
            ->assertOk()
            ->getContent();

        $snapshot = $this->extractSnapshot($html, 'servers.show');

        // 2. The account is reverted to must_change_password AFTER the component mounted.
        $user->forceFill(['must_change_password' => true])->save();

        // 3. Replay a destructive call against the still-valid snapshot, exactly as the
        //    browser would on the next interaction.
        $response = $this->actingAs($user)->postJson('/livewire/update', [
            'components' => [[
                'snapshot' => $snapshot,
                'updates' => [],
                'calls' => [[
                    'path' => '',
                    'method' => 'removeKey',
                    'params' => ['SHA256:'.str_repeat('a', 43)],
                ]],
            ]],
        ], ['X-Livewire' => 'true']);

        // The persistent gate redirects to the password change before removeKey runs;
        // the ->never() expectation above proves the destructive call was blocked.
        $response->assertRedirect(route('password.change'));
    }

    /** Pull the wire:snapshot JSON for a specific component out of rendered page HTML. */
    private function extractSnapshot(string $html, string $name): string
    {
        preg_match_all('/wire:snapshot="([^"]*)"/', $html, $matches);

        foreach ($matches[1] as $encoded) {
            $json = htmlspecialchars_decode($encoded, ENT_QUOTES | ENT_SUBSTITUTE);

            if ((json_decode($json, true)['memo']['name'] ?? null) === $name) {
                return $json;
            }
        }

        $this->fail("No wire:snapshot found for component [{$name}] in the rendered page.");
    }
}
