<?php

namespace Tests\Feature;

use App\Livewire\Modals\HostShell;
use App\Livewire\Terminal\Index;
use App\Models\HostCredential;
use App\Models\Server;
use App\Models\SshCredential;
use App\Models\TerminalSession;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class TerminalTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        config()->set('clusev.terminal_secret', 'test-secret');
        $this->actingAs(User::factory()->create(['must_change_password' => false]));
    }

    private function server(): Server
    {
        $s = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 2222, 'status' => 'online']);
        SshCredential::create(['server_id' => $s->id, 'username' => 'deploy', 'auth_type' => 'password', 'secret' => 's3cret-pw']);

        return $s;
    }

    private function hostCredential(): HostCredential
    {
        return HostCredential::create([
            'host' => 'host.docker.internal', 'port' => 2200, 'username' => 'root',
            'auth_type' => 'password', 'secret' => 'r00t-pw',
        ]);
    }

    private function token(string $kind, ?int $serverId = null, ?\DateTimeInterface $expires = null): string
    {
        $token = 'tok-'.bin2hex(random_bytes(6));
        TerminalSession::create([
            'token' => $token,
            'user_id' => auth()->id(),
            'kind' => $kind,
            'server_id' => $serverId,
            'expires_at' => $expires ?? now()->addMinute(),
            'created_at' => now(),
        ]);

        return $token;
    }

    private function resolve(string $token, string $secret = 'test-secret')
    {
        return $this->withHeader('X-Sidecar-Secret', $secret)
            ->postJson(route('internal.terminal.resolve'), ['token' => $token]);
    }

    // ── token minting (Livewire) ──────────────────────────────────────────────

    public function test_open_host_mints_a_single_use_host_session_when_configured(): void
    {
        $this->hostCredential();

        Livewire::test(Index::class)->call('open', 'host')->assertSet('activeKey', 'host');

        $s = TerminalSession::first();
        $this->assertNotNull($s);
        $this->assertSame('host', $s->kind);
        $this->assertNull($s->server_id);
        $this->assertNull($s->used_at);
        $this->assertTrue($s->expires_at->isFuture());
    }

    public function test_open_host_without_a_login_opens_the_setup_modal_and_mints_nothing(): void
    {
        Livewire::test(Index::class)
            ->call('open', 'host')
            ->assertDispatched('openModal', component: 'modals.host-shell');

        $this->assertSame(0, TerminalSession::count());
    }

    public function test_open_server_mints_a_session_for_that_server(): void
    {
        $server = $this->server();
        Livewire::test(Index::class)->call('open', 'server', $server->uuid)->assertSet('activeKey', $server->uuid);

        $this->assertSame($server->id, TerminalSession::first()->server_id);
    }

    public function test_open_ignores_an_unknown_server(): void
    {
        Livewire::test(Index::class)->call('open', 'server', 'nope');
        $this->assertSame(0, TerminalSession::count());
    }

    // ── resolve endpoint ──────────────────────────────────────────────────────

    public function test_resolve_requires_the_sidecar_secret(): void
    {
        $token = $this->token('host');
        $this->postJson(route('internal.terminal.resolve'), ['token' => $token])->assertForbidden();
        $this->resolve($token, 'wrong-secret')->assertForbidden();
    }

    public function test_resolve_returns_the_host_ssh_spec_and_burns_the_token(): void
    {
        $this->hostCredential();
        $token = $this->token('host');

        $this->resolve($token)->assertOk()->assertExactJson([
            'kind' => 'server', // the sidecar SSHes into the host exactly like a fleet server
            'host' => 'host.docker.internal',
            'port' => 2200,
            'username' => 'root',
            'auth_type' => 'password',
            'secret' => 'r00t-pw', // decrypted by the model cast, only over the internal net
            'passphrase' => null,
            'host_key' => null, // the host has no fleet TOFU pin
        ]);
        // single use → second resolve fails
        $this->resolve($token)->assertNotFound();
    }

    public function test_resolve_host_404s_when_no_host_login_is_configured(): void
    {
        $token = $this->token('host');

        $this->resolve($token)->assertNotFound();
    }

    public function test_resolve_returns_the_server_spec_with_decrypted_credentials(): void
    {
        $server = $this->server();
        $token = $this->token('server', $server->id);

        $this->resolve($token)->assertOk()->assertExactJson([
            'kind' => 'server',
            'host' => '10.0.0.9',
            'port' => 2222,
            'username' => 'deploy',
            'auth_type' => 'password',
            'secret' => 's3cret-pw', // decrypted by the model cast, only over the internal net
            'passphrase' => null,
            'host_key' => null, // no TOFU pin stored for this test server
        ]);
    }

    public function test_resolve_includes_the_pinned_host_key_when_the_server_has_one(): void
    {
        $server = $this->server();
        $server->update(['ssh_host_key' => 'ssh-ed25519 AAAAC3NzaPINNED']);
        $token = $this->token('server', $server->id);

        $this->resolve($token)->assertOk()->assertJsonPath('host_key', 'ssh-ed25519 AAAAC3NzaPINNED');
    }

    public function test_resolve_rejects_an_expired_token(): void
    {
        $token = $this->token('host', null, now()->subMinute());
        $this->resolve($token)->assertNotFound();
    }

    public function test_resolve_rejects_a_server_token_whose_credential_is_locked(): void
    {
        $server = $this->server();
        $server->credential->update(['disabled_at' => now()]);
        $token = $this->token('server', $server->id);

        $this->resolve($token)->assertNotFound();
    }

    // ── page ──────────────────────────────────────────────────────────────────

    public function test_terminal_page_renders_with_the_rail(): void
    {
        $this->server();
        $this->get(route('terminal'))->assertOk()->assertSee(__('terminal.heading'));
    }

    // ── host-login modal + server search ───────────────────────────────────────

    public function test_host_shell_modal_saves_the_singleton(): void
    {
        Livewire::test(HostShell::class)
            ->set('port', 22)
            ->set('username', 'root')
            ->set('authType', 'password')
            ->set('secret', 'hunter2pw')
            ->call('save')
            ->assertDispatched('hostShellSaved');

        $hc = HostCredential::current();
        $this->assertNotNull($hc);
        $this->assertSame('host.docker.internal', $hc->host); // fixed to the local machine, not operator-set
        $this->assertSame('root', $hc->username);
        $this->assertSame('hunter2pw', $hc->secret); // round-trips through the encrypted cast
    }

    public function test_host_shell_edit_keeps_the_stored_secret_when_left_blank(): void
    {
        $this->hostCredential(); // secret 'r00t-pw'

        Livewire::test(HostShell::class)
            ->assertSet('configured', true)
            ->set('username', 'admin') // change something, leave secret blank
            ->call('save');

        $hc = HostCredential::current();
        $this->assertSame('admin', $hc->username);
        $this->assertSame('r00t-pw', $hc->secret); // unchanged
    }

    public function test_host_shell_requires_a_new_secret_when_switching_auth_method(): void
    {
        $this->hostCredential(); // auth_type 'password', secret 'r00t-pw'

        Livewire::test(HostShell::class)
            ->set('authType', 'key') // switch method but leave the secret blank
            ->set('secret', '')
            ->call('save')
            ->assertHasErrors('secret');

        // The stored login is untouched — no password silently reused as a key.
        $hc = HostCredential::current();
        $this->assertSame('password', $hc->auth_type);
        $this->assertSame('r00t-pw', $hc->secret);
    }

    public function test_search_filters_the_server_rail(): void
    {
        Server::create(['name' => 'alpha-web', 'ip' => '10.0.0.1', 'status' => 'online']);
        Server::create(['name' => 'delta-db', 'ip' => '10.0.0.2', 'status' => 'online']);

        Livewire::test(Index::class)
            ->set('search', 'alpha')
            ->assertSee('alpha-web')
            ->assertDontSee('delta-db');
    }

    public function test_search_box_appears_once_there_is_more_than_one_server(): void
    {
        Server::create(['name' => 'only-one', 'ip' => '10.0.0.1', 'status' => 'online']);
        $this->get(route('terminal'))->assertDontSee(__('terminal.search_placeholder')); // 1 server → no search

        Server::create(['name' => 'second', 'ip' => '10.0.0.2', 'status' => 'online']);
        $this->get(route('terminal'))->assertSee(__('terminal.search_placeholder')); // 2 servers → search shows
    }
}
