<?php

namespace Tests\Feature;

use App\Livewire\Wireguard\Index;
use App\Models\AuditEvent;
use App\Models\User;
use App\Models\WgTrafficSample;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class WireguardPageTest extends TestCase
{
    use RefreshDatabase;

    private string $file;

    protected function setUp(): void
    {
        parent::setUp();
        $dir = storage_path('app/restart-signal');
        @mkdir($dir, 0775, true);
        $this->file = $dir.'/wg-status.json';
        @unlink($this->file);
        $this->actingAs(User::factory()->create(['must_change_password' => false]));
    }

    protected function tearDown(): void
    {
        @unlink($this->file);
        parent::tearDown();
    }

    public function test_renders_unconfigured_state(): void
    {
        Livewire::test(Index::class)
            ->assertOk()
            ->assertViewHas('status', fn ($s) => $s['configured'] === false)
            ->assertSee(__('wireguard.setup_title'));
    }

    public function test_renders_peers_when_configured(): void
    {
        file_put_contents($this->file, json_encode([
            'at' => time(), 'configured' => true,
            'gate' => ['marker' => true, 'chain' => true], 'wg_quick' => 'active',
            'server' => ['subnet' => '10.99.0.0/24', 'server_ip' => '10.99.0.1', 'port' => 51820, 'endpoint' => '1.2.3.4:51820', 'pubkey' => 'srvpub'],
            'peers' => [['name' => 'laptop', 'pubkey' => 'p1', 'endpoint' => '5.6.7.8:41820', 'latest_handshake' => time(), 'rx' => 100, 'tx' => 200]],
        ]));

        Livewire::test(Index::class)
            ->assertOk()
            ->assertViewHas('status', fn ($s) => $s['configured'] === true && count($s['peers']) === 1)
            ->assertSee('laptop')                     // peers tab (default)
            ->set('tab', 'server')->assertSee('10.99.0.0/24');   // server tab shows the subnet
    }

    public function test_shows_ssh_lock_active_state_when_the_host_reports_it(): void
    {
        file_put_contents($this->file, json_encode([
            'at' => time(), 'configured' => true,
            'gate' => ['marker' => true, 'chain' => true, 'ssh' => true], 'wg_quick' => 'active',
            'server' => ['subnet' => '10.99.0.0/24', 'server_ip' => '10.99.0.1', 'port' => 51820, 'endpoint' => '1.2.3.4:51820', 'pubkey' => 'srvpub'],
            'peers' => [],
        ]));

        Livewire::test(Index::class)
            ->assertViewHas('status', fn ($s) => $s['gate']['ssh'] === true)
            ->set('tab', 'server')   // SSH lock lives in the server-configuration tab
            ->assertSee(__('wireguard.ssh_lock'))
            ->assertSee(__('wireguard.ssh_lock_turn_off'));
    }

    public function test_traffic_header_shows_live_cumulative_peer_totals(): void
    {
        // The header must match the per-peer rows (sum of live counters), not the sampled throughput.
        file_put_contents($this->file, json_encode([
            'at' => time(), 'configured' => true,
            'gate' => ['marker' => false, 'chain' => false, 'ssh' => false], 'wg_quick' => 'active',
            'server' => ['subnet' => '10.99.0.0/24', 'server_ip' => '10.99.0.1', 'port' => 51820, 'endpoint' => '1.2.3.4:51820', 'dns' => '1.1.1.1', 'pubkey' => 'srvpub'],
            'peers' => [
                ['name' => 'a', 'pubkey' => 'p1', 'endpoint' => '', 'latest_handshake' => time(), 'rx' => 244, 'tx' => 92],
                ['name' => 'b', 'pubkey' => 'p2', 'endpoint' => '', 'latest_handshake' => 0, 'rx' => 1000, 'tx' => 0],
            ],
        ]));

        Livewire::test(Index::class)
            ->assertSee('1.2 KB');   // received = 244 + 1000 = 1244 B (only the summed header shows this)
    }

    public function test_window_selector_switches_and_passes_traffic(): void
    {
        WgTrafficSample::create(['peer_pubkey' => 'p1', 'peer_name' => 'a', 'rx' => 0, 'tx' => 0, 'sampled_at' => now()->subMinutes(10)]);
        WgTrafficSample::create(['peer_pubkey' => 'p1', 'peer_name' => 'a', 'rx' => 500, 'tx' => 100, 'sampled_at' => now()->subMinutes(1)]);

        Livewire::test(Index::class)
            ->assertViewHas('traffic', fn ($t) => $t['total_down'] === 500)
            ->call('setWindow', 86400)
            ->assertSet('window', 86400)
            ->assertViewHas('traffic', fn ($t) => $t['window'] === 86400);
    }

    public function test_window_clamps_to_allowed_values(): void
    {
        Livewire::test(Index::class)
            ->call('setWindow', 999)
            ->assertSet('window', 3600);
    }

    public function test_add_peer_writes_a_request_and_audits(): void
    {
        Livewire::test(Index::class)
            ->set('newPeer', 'laptop')
            ->call('addPeer')
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');

        $this->assertTrue(AuditEvent::where('action', 'wg.add-peer')->exists());
        $this->assertFileExists(storage_path('app/restart-signal/wg-request.json'));
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_add_peer_rejects_an_invalid_name(): void
    {
        Livewire::test(Index::class)
            ->set('newPeer', 'bad name')
            ->call('addPeer')
            ->assertHasErrors('newPeer');
        $this->assertSame(0, AuditEvent::where('action', 'wg.add-peer')->count());
    }

    public function test_remove_peer_writes_a_request_and_audits(): void
    {
        Livewire::test(Index::class)
            ->call('removePeer', 'laptop')
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');
        $this->assertTrue(AuditEvent::where('action', 'wg.remove-peer')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_set_endpoint_writes_a_request_and_audits(): void
    {
        Livewire::test(Index::class)
            ->set('newEndpoint', 'vpn.example.com:51820')
            ->call('setEndpoint')
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');
        $this->assertTrue(AuditEvent::where('action', 'wg.set-endpoint')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_set_endpoint_rejects_a_bad_value(): void
    {
        Livewire::test(Index::class)
            ->set('newEndpoint', 'has spaces!!')
            ->call('setEndpoint')
            ->assertHasErrors('newEndpoint');
    }

    public function test_run_gate_writes_the_right_action_and_audits(): void
    {
        Livewire::test(Index::class)
            ->call('runGate', false)
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');
        $this->assertTrue(AuditEvent::where('action', 'wg.gate-down')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_run_ssh_gate_lock_writes_the_right_action_and_audits(): void
    {
        Livewire::test(Index::class)
            ->call('runSshGate', true)
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '')
            ->assertSet('pendingAction', 'gate-ssh-on');
        $this->assertTrue(AuditEvent::where('action', 'wg.ssh-lock')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_run_ssh_gate_unlock_writes_the_right_action_and_audits(): void
    {
        Livewire::test(Index::class)
            ->call('runSshGate', false)
            ->assertSet('pendingAction', 'gate-ssh-off');
        $this->assertTrue(AuditEvent::where('action', 'wg.ssh-unlock')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_confirm_ssh_gate_opens_the_modal_without_auditing(): void
    {
        Livewire::test(Index::class)
            ->call('confirmSshGate', true)
            ->assertDispatched('openModal');
        // The confirm step must NOT audit — only the apply path (runSshGate) does.
        $this->assertSame(0, AuditEvent::where('action', 'wg.ssh-lock')->count());
    }

    public function test_poll_result_surfaces_the_config_once(): void
    {
        $c = Livewire::test(Index::class)->set('newPeer', 'laptop')->call('addPeer');
        $id = $c->get('pendingId');
        file_put_contents(storage_path("app/restart-signal/wg-result-{$id}.json"), json_encode(['id' => $id, 'ok' => true, 'message' => 'ok', 'config' => "[Interface]\nPrivateKey = x", 'at' => time()]));

        $c->call('pollResult')
            ->assertSet('pendingId', null)
            ->assertSet('resultConfig', fn ($v) => str_contains((string) $v, 'PrivateKey'));
        @unlink(storage_path("app/restart-signal/wg-result-{$id}.json"));
    }

    public function test_set_dns_writes_a_request_and_audits(): void
    {
        Livewire::test(Index::class)
            ->set('newDns', '10.0.0.1, 1.1.1.1')
            ->call('setDns')
            ->assertSet('pendingAction', 'set-dns')
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');
        $this->assertTrue(AuditEvent::where('action', 'wg.set-dns')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_set_dns_rejects_a_bad_value(): void
    {
        Livewire::test(Index::class)
            ->set('newDns', 'not-an-ip')
            ->call('setDns')
            ->assertHasErrors('newDns');
        $this->assertSame(0, AuditEvent::where('action', 'wg.set-dns')->count());
    }

    public function test_set_tab_switches_and_clamps_unknown_values(): void
    {
        Livewire::test(Index::class)
            ->assertSet('tab', 'peers')
            ->call('setTab', 'server')->assertSet('tab', 'server')
            ->call('setTab', 'bogus')->assertSet('tab', 'peers');
    }

    public function test_status_exposes_the_server_dns(): void
    {
        file_put_contents($this->file, json_encode([
            'at' => time(), 'configured' => true,
            'gate' => ['marker' => true, 'chain' => true, 'ssh' => false], 'wg_quick' => 'active',
            'server' => ['subnet' => '10.99.0.0/24', 'server_ip' => '10.99.0.1', 'port' => 51820, 'endpoint' => '1.2.3.4:51820', 'dns' => '10.0.0.1', 'pubkey' => 'srvpub'],
            'peers' => [],
        ]));

        Livewire::test(Index::class)
            ->set('tab', 'server')
            ->assertViewHas('status', fn ($s) => $s['server']['dns'] === '10.0.0.1')
            ->assertSee('10.0.0.1');
    }

    public function test_setup_writes_a_request_and_audits(): void
    {
        Livewire::test(Index::class)
            ->set('setupSubnet', '10.99.0.0/24')->set('setupPort', '51820')->set('setupEndpoint', '1.2.3.4:51820')->set('setupPeer', 'client-1')
            ->call('setupWg')
            ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== '');
        $this->assertTrue(AuditEvent::where('action', 'wg.setup')->exists());
        @unlink(storage_path('app/restart-signal/wg-request.json'));
    }

    public function test_setup_rejects_a_bad_subnet(): void
    {
        Livewire::test(Index::class)
            ->set('setupSubnet', 'nope')->set('setupPort', '51820')->set('setupPeer', 'c')
            ->call('setupWg')
            ->assertHasErrors('setupSubnet');
    }

    public function test_download_config_filename_uses_endpoint_host_and_peer_name(): void
    {
        // The .conf filename becomes the tunnel name on import — build it from endpoint host + peer.
        Livewire::test(Index::class)
            ->set('resultConfig', "[Interface]\nPrivateKey = x\n\n[Peer]\nEndpoint = 203.0.113.10:51820")
            ->set('resultName', 'laptop')
            ->call('downloadConfig')
            ->assertFileDownloaded('203.0.113.10-laptop.conf');
    }

    public function test_download_config_falls_back_to_a_default_filename(): void
    {
        Livewire::test(Index::class)
            ->set('resultConfig', "[Interface]\nPrivateKey = x")
            ->call('downloadConfig')
            ->assertFileDownloaded('clusev.conf');
    }

    public function test_poll_result_audits_a_failure_so_it_is_readable_later(): void
    {
        $c = Livewire::test(Index::class)->set('newEndpoint', 'vpn.example.com')->call('setEndpoint');
        $id = $c->get('pendingId');
        file_put_contents(storage_path("app/restart-signal/wg-result-{$id}.json"), json_encode(['id' => $id, 'ok' => false, 'message' => 'set-endpoint-failed', 'config' => null, 'at' => time()]));

        $c->call('pollResult');

        $e = AuditEvent::where('action', 'wg.action-failed')->first();
        $this->assertNotNull($e);
        $this->assertStringContainsString('set-endpoint-failed', (string) $e->target);
        @unlink(storage_path("app/restart-signal/wg-result-{$id}.json"));
    }

    public function test_poll_result_times_out_when_the_host_does_not_respond(): void
    {
        // A pending request with no host result for >30s must stop spinning, not poll forever.
        Livewire::test(Index::class)
            ->set('pendingId', 'AAAAAAAAAAAAAAAAbbbbcccc')
            ->set('pendingSince', time() - 31)
            ->call('pollResult')
            ->assertSet('pendingId', null)
            ->assertSet('pendingSince', null);
    }
}
