<?php

namespace Tests\Feature;

use App\Livewire\Versions\Index;
use App\Models\User;
use App\Services\DeploymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Tests\TestCase;

/**
 * The changelog browser: releases are grouped into major.minor series (a left rail / pill row)
 * and the active series is paginated so a long history never becomes an endless scroll. These
 * tests drive the real CHANGELOG.md, asserting structural invariants (ordering, paging, defaults)
 * rather than specific version numbers, so they survive new releases.
 */
class VersionsChangelogTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Cache::flush();
        app(DeploymentService::class)->clearUpdateRequest();
        $this->actingAs(User::factory()->create(['must_change_password' => false]));
    }

    protected function tearDown(): void
    {
        app(DeploymentService::class)->clearUpdateRequest();
        parent::tearDown();
    }

    public function test_releases_are_grouped_into_series_ordered_newest_first(): void
    {
        Livewire::test(Index::class)->assertViewHas('seriesList', function ($series): bool {
            if (! is_array($series) || count($series) < 2) {
                return false;
            }
            // Newest series first: each key sorts >= the next (compare on "X.Y.0").
            for ($i = 0; $i < count($series) - 1; $i++) {
                if (version_compare($series[$i]['key'].'.0', $series[$i + 1]['key'].'.0', '<')) {
                    return false;
                }
            }

            return true;
        });
    }

    public function test_default_active_series_is_the_newest_one(): void
    {
        $firstKey = null;

        $component = Livewire::test(Index::class)
            ->assertViewHas('seriesList', function ($series) use (&$firstKey): bool {
                $firstKey = $series[0]['key'] ?? null;

                return true;
            });

        $component->assertViewHas('activeKey', fn ($key): bool => $key === $firstKey);
    }

    public function test_an_unknown_selected_series_falls_back_to_the_newest(): void
    {
        Livewire::test(Index::class)
            ->call('selectSeries', '99.99')
            ->assertViewHas('activeKey', fn ($key): bool => $key !== '99.99' && $key !== '');
    }

    public function test_selecting_a_series_switches_the_visible_releases_and_resets_the_page(): void
    {
        // Default view shows the newest series, so the very first release (v0.1.0) is NOT visible…
        Livewire::test(Index::class)->assertDontSee('v0.1.0');

        // …selecting the 0.1 series brings it into view and jumps back to page 1.
        Livewire::test(Index::class)
            ->set('changelogPage', 3)
            ->call('selectSeries', 'v0.1')
            ->assertSet('series', 'v0.1')
            ->assertSet('changelogPage', 1)
            ->assertSee('v0.1.0');
    }

    public function test_a_long_series_is_paginated_to_the_per_page_limit(): void
    {
        Livewire::test(Index::class)
            ->call('selectSeries', 'v0.9')
            ->assertViewHas('totalPages', fn ($total): bool => $total > 1)
            ->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) <= 8);
    }

    public function test_paging_shows_a_different_slice_of_the_series(): void
    {
        $pageOne = null;

        Livewire::test(Index::class)
            ->call('selectSeries', 'v0.9')
            ->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
                $pageOne = array_column($releases, 'version');

                return true;
            });

        Livewire::test(Index::class)
            ->call('selectSeries', 'v0.9')
            ->call('gotoChangelogPage', 2)
            ->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
                $pageTwo = array_column($releases, 'version');

                // Page 2 is non-empty and disjoint from page 1.
                return $pageTwo !== [] && array_intersect($pageTwo, $pageOne) === [];
            });
    }

    public function test_an_out_of_range_page_is_clamped(): void
    {
        Livewire::test(Index::class)
            ->call('selectSeries', 'v0.9')
            ->call('gotoChangelogPage', 9999)
            ->assertViewHas('changelogPage', fn ($page): bool => $page >= 1)
            ->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) >= 1)
            ->assertSee(__('versions.changelog_title'));
    }

    public function test_the_installed_series_is_resolved_from_the_running_version(): void
    {
        config()->set('clusev.version', '0.7.3');

        Livewire::test(Index::class)->assertViewHas('currentSeries', 'v0.7');
    }

    public function test_total_release_count_is_passed_for_the_panel_subtitle(): void
    {
        Livewire::test(Index::class)->assertViewHas('totalReleases', fn ($count): bool => is_int($count) && $count >= 1);
    }

    public function test_series_and_page_are_deep_linkable_via_the_query_string(): void
    {
        // ?series= + ?page= drive the view so a series/page is shareable and survives reload.
        Livewire::withQueryParams(['series' => 'v0.9', 'page' => 2])
            ->test(Index::class)
            ->assertSet('series', 'v0.9')
            ->assertSet('changelogPage', 2);
    }

    public function test_a_trailing_zero_series_like_0_10_survives_a_deep_link(): void
    {
        // The 'v' prefix keeps the ?series= value non-numeric, so "0.10" is NOT type-juggled to
        // "0.1" on reload (which used to silently switch the selected series).
        Livewire::withQueryParams(['series' => 'v0.10'])
            ->test(Index::class)
            ->assertSet('series', 'v0.10')
            ->assertViewHas('activeKey', 'v0.10');
    }

    public function test_chevron_right_icon_renders_a_real_path(): void
    {
        // The accordion disclosure caret and the pagination "Next" button both use x-icon
        // name="chevron-right"; a missing map entry renders an empty <svg> (invisible affordance).
        $html = Blade::render('<x-icon name="chevron-right" />');

        $this->assertStringContainsString('<path', $html, 'chevron-right must resolve to a real Lucide path, not an empty svg');
    }
}
