<?php

namespace Tests\Feature;

use App\Livewire\Commands\Index;
use App\Models\Runbook;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\User;
use App\Services\CommandRunner;
use App\Support\Confirm\ConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

class CommandsComponentTest extends TestCase
{
    use RefreshDatabase;

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

    private function admin(): User
    {
        return User::factory()->create(['must_change_password' => false]);
    }

    private function operator(): User
    {
        return User::factory()->operator()->create(['must_change_password' => false]);
    }

    private function viewer(): User
    {
        return User::factory()->viewer()->create(['must_change_password' => false]);
    }

    private function server(string $name = 'box', string $ip = '10.0.0.1'): Server
    {
        $s = Server::create(['name' => $name, 'ip' => $ip, 'ssh_port' => 22, 'status' => 'online']);
        $s->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);

        return $s;
    }

    // ── route gating (operate) ──────────────────────────────────────────────────

    public function test_viewer_cannot_open_commands(): void
    {
        $this->actingAs($this->viewer())->get('/commands')->assertForbidden();
    }

    public function test_operator_can_open_commands(): void
    {
        $this->actingAs($this->operator())->get('/commands')->assertOk();
    }

    // ── ad-hoc run through the confirm token ────────────────────────────────────

    public function test_run_opens_a_confirm_for_the_selected_targets(): void
    {
        $this->actingAs($this->operator());
        $this->server();

        Livewire::test(Index::class)
            ->set('command', 'uptime')
            ->set('scopeType', 'all')
            ->call('run')
            ->assertHasNoErrors()
            ->assertDispatched('openModal');
    }

    public function test_run_requires_a_command(): void
    {
        $this->actingAs($this->operator());
        $this->server();

        Livewire::test(Index::class)->set('command', '  ')->call('run')->assertHasErrors('command');
    }

    public function test_run_requires_at_least_one_target(): void
    {
        $this->actingAs($this->operator()); // no servers created → empty target

        Livewire::test(Index::class)->set('command', 'uptime')->set('scopeType', 'all')->call('run')->assertHasErrors('command');
    }

    public function test_execute_runs_the_command_over_the_sealed_targets(): void
    {
        $this->actingAs($this->operator());
        $server = $this->server();

        $runner = Mockery::mock(CommandRunner::class);
        $runner->shouldReceive('run')->once()
            ->with('uptime', Mockery::on(fn ($servers) => $servers->pluck('id')->all() === [$server->id]))
            ->andReturn([['server' => 'box', 'ok' => true, 'output' => 'up 3 days']]);
        app()->instance(CommandRunner::class, $runner);

        $token = ConfirmToken::issue('commandRun', ['command' => 'uptime', 'serverIds' => [$server->id]], 'command.run', 'uptime · 1', null);
        ConfirmToken::confirm($token);

        Livewire::test(Index::class)
            ->call('execute', $token)
            ->assertOk()
            ->assertSet('results.0.output', 'up 3 days');
    }

    public function test_a_forged_run_token_executes_nothing(): void
    {
        $this->actingAs($this->operator());
        $runner = Mockery::mock(CommandRunner::class);
        $runner->shouldReceive('run')->never();
        app()->instance(CommandRunner::class, $runner);

        Livewire::test(Index::class)->call('execute', 'not-a-token')->assertSet('results', []);
    }

    // (A viewer cannot even mount the component — the route + mount operate-gate blocks the whole
    //  page, proven by test_viewer_cannot_open_commands; every mutating method re-gates as defence.)

    // ── runbooks ────────────────────────────────────────────────────────────────

    public function test_operator_creates_a_runbook_and_it_is_audited(): void
    {
        $this->actingAs($this->operator());

        Livewire::test(Index::class)
            ->set('runbookName', 'Restart nginx')
            ->set('runbookCommand', 'systemctl restart nginx')
            ->call('createRunbook')
            ->assertHasNoErrors();

        $this->assertDatabaseHas('runbooks', ['name' => 'Restart nginx']);
        $this->assertDatabaseHas('audit_events', ['action' => 'runbook.create', 'target' => 'Restart nginx']);
    }

    public function test_runbook_run_opens_a_confirm_with_the_saved_command(): void
    {
        $this->actingAs($this->operator());
        $this->server();
        $rb = Runbook::create(['name' => 'up', 'command' => 'uptime']);

        Livewire::test(Index::class)
            ->set('scopeType', 'all')
            ->call('runRunbook', $rb->uuid)
            ->assertDispatched('openModal');
    }

    public function test_runbook_delete_via_a_confirmed_token(): void
    {
        $this->actingAs($this->operator());
        $rb = Runbook::create(['name' => 'gone', 'command' => 'x']);
        $token = ConfirmToken::issue('runbookDeleted', ['uuid' => $rb->uuid], 'runbook.delete', $rb->name, null);
        ConfirmToken::confirm($token);

        Livewire::test(Index::class)->call('deleteRunbook', $token)->assertOk();

        $this->assertDatabaseMissing('runbooks', ['id' => $rb->id]);
    }

    public function test_group_scope_only_targets_group_members(): void
    {
        $this->actingAs($this->operator());
        $group = ServerGroup::create(['name' => 'web', 'color' => 'cyan']);
        $member = $this->server('web1', '10.0.0.1');
        $this->server('db1', '10.0.0.2'); // not in the group
        $group->servers()->attach($member->id);

        $runner = Mockery::mock(CommandRunner::class);
        $runner->shouldReceive('run')->once()
            ->with('hostname', Mockery::on(fn ($servers) => $servers->pluck('id')->all() === [$member->id]))
            ->andReturn([]);
        app()->instance(CommandRunner::class, $runner);

        // Issue a token as the component would for a group scope, then execute.
        $token = ConfirmToken::issue('commandRun', ['command' => 'hostname', 'serverIds' => [$member->id]], 'command.run', 'x', null);
        ConfirmToken::confirm($token);

        Livewire::test(Index::class)->call('execute', $token)->assertOk();
    }
}
