<?php

declare(strict_types=1);

use App\Models\Artwork;
use App\Models\Category;
use App\Models\Collection;
use App\Models\ContentType;
use App\Models\NovaCard;
use App\Models\NovaCardCategory;
use App\Models\NovaCardTemplate;
use App\Models\Page;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

beforeEach(function (): void {
    config([
        'app.url' => 'http://skinbase26.test',
        'sitemaps.pre_generated.enabled' => false,
        'sitemaps.pre_generated.prefer' => false,
        'sitemaps.delivery.prefer_published_release' => true,
        'sitemaps.delivery.fallback_to_live_build' => true,
    ]);
    Cache::flush();

    ensureNewsSchema();
    ensureForumSchema();
});

it('renders the sitemap index and every child sitemap endpoint', function (): void {
    seedSitemapFixtures();

    $indexResponse = $this->get('/sitemap.xml')
        ->assertOk()
        ->assertHeader('Content-Type', 'application/xml; charset=UTF-8');

    $indexXml = simplexml_load_string($indexResponse->getContent());

    expect($indexXml)->not->toBeFalse();
    expect($indexResponse->getContent())
        ->toContain(url('/sitemaps/artworks.xml'))
        ->toContain(url('/sitemaps/static-pages.xml'))
        ->toContain(url('/sitemaps/forum-threads.xml'))
        ->toContain(url('/sitemaps/news-google.xml'));

    foreach ([
        'artworks',
        'users',
        'tags',
        'categories',
        'collections',
        'cards',
        'stories',
        'news',
        'news-google',
        'forum-index',
        'forum-categories',
        'forum-threads',
        'static-pages',
    ] as $name) {
        $response = $this->get('/sitemaps/' . $name . '.xml')
            ->assertOk()
            ->assertHeader('Content-Type', 'application/xml; charset=UTF-8');

        expect(simplexml_load_string($response->getContent()))->not->toBeFalse();
    }
});

it('includes only public canonical urls and exposes the sitemap in robots txt', function (): void {
    $fixtures = seedSitemapFixtures();

    $artworksXml = $this->get('/sitemaps/artworks.xml')->assertOk()->getContent();
    expect($artworksXml)
        ->toContain($fixtures['artwork_url'])
        ->toContain('<image:image>')
        ->not->toContain('/art/' . $fixtures['private_artwork_id'] . '/');

    $usersXml = $this->get('/sitemaps/users.xml')->assertOk()->getContent();
    expect($usersXml)
        ->toContain($fixtures['profile_url'])
        ->not->toContain('/@inactivecreator');

    $tagsXml = $this->get('/sitemaps/tags.xml')->assertOk()->getContent();
    expect($tagsXml)
        ->toContain('/tag/featured-tag')
        ->not->toContain('/tag/inactive-tag');

    $categoriesXml = $this->get('/sitemaps/categories.xml')->assertOk()->getContent();
    expect($categoriesXml)
        ->toContain('/categories')
        ->toContain('/photography')
        ->toContain('/photography/portraits');

    $collectionsXml = $this->get('/sitemaps/collections.xml')->assertOk()->getContent();
    expect($collectionsXml)->toContain($fixtures['collection_url']);

    $cardsXml = $this->get('/sitemaps/cards.xml')->assertOk()->getContent();
    expect($cardsXml)
        ->toContain($fixtures['card_url'])
        ->toContain('<image:image>');

    $storiesXml = $this->get('/sitemaps/stories.xml')->assertOk()->getContent();
    expect($storiesXml)
        ->toContain('/stories/creator-spotlight')
        ->toContain('<image:image>');

    $newsXml = $this->get('/sitemaps/news.xml')->assertOk()->getContent();
    expect($newsXml)
        ->toContain('/news/platform-update')
        ->toContain('<image:image>');

    $googleNewsXml = $this->get('/sitemaps/news-google.xml')->assertOk()->getContent();
    expect($googleNewsXml)
        ->toContain('<news:news>')
        ->toContain('<news:title>Platform Update</news:title>');

    $forumIndexXml = $this->get('/sitemaps/forum-index.xml')->assertOk()->getContent();
    expect($forumIndexXml)->toContain('/forum');

    $forumCategoriesXml = $this->get('/sitemaps/forum-categories.xml')->assertOk()->getContent();
    expect($forumCategoriesXml)
        ->toContain('/forum/category/general')
        ->toContain('/forum/general-board');

    $forumThreadsXml = $this->get('/sitemaps/forum-threads.xml')->assertOk()->getContent();
    expect($forumThreadsXml)
        ->toContain('/forum/topic/welcome-thread')
        ->not->toContain('#reply-content');

    $staticPagesXml = $this->get('/sitemaps/static-pages.xml')->assertOk()->getContent();
    expect($staticPagesXml)
        ->toContain('http://skinbase26.test/')
        ->toContain('/about')
        ->toContain('/pages/community-handbook')
        ->not->toContain('/pages/about');

    $robots = $this->get('/robots.txt')
        ->assertOk()
        ->assertHeader('Content-Type', 'text/plain; charset=UTF-8')
        ->getContent();

    expect($robots)->toContain('Sitemap: http://skinbase26.test/sitemap.xml');
});

it('returns 404 for unknown sitemap names', function (): void {
    $this->get('/sitemaps/not-a-real-sitemap.xml')->assertNotFound();
});

it('lists shard urls in the sitemap index and serves shard compatibility indexes', function (): void {
    config([
        'sitemaps.shards.enabled' => true,
        'sitemaps.shards.artworks.size' => 1,
        'sitemaps.shards.users.size' => 1,
    ]);

    $fixtures = seedSitemapFixtures();
    $secondCreator = User::factory()->create([
        'username' => 'sitemapuser2',
        'is_active' => true,
    ]);

    $secondArtwork = Artwork::factory()->create([
        'user_id' => $secondCreator->id,
        'title' => 'Second Sitemap Artwork',
        'slug' => 'second-sitemap-artwork',
        'hash' => str_repeat('d', 32),
        'file_path' => 'artworks/original/dd/dd/' . str_repeat('d', 32) . '.jpg',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subHours(12),
    ]);

    $indexXml = $this->get('/sitemap.xml')->assertOk()->getContent();
    expect($indexXml)
        ->toContain('/sitemaps/artworks-index.xml')
        ->toContain('/sitemaps/users-index.xml');

    $artworksCompatibilityIndex = $this->get('/sitemaps/artworks.xml')->assertOk()->getContent();
    expect($artworksCompatibilityIndex)
        ->toContain('/sitemaps/artworks-0001.xml')
        ->toContain('/sitemaps/artworks-0002.xml');

    $artworksShardOne = $this->get('/sitemaps/artworks-1.xml')->assertOk()->getContent();
    $artworksShardTwo = $this->get('/sitemaps/artworks-2.xml')->assertOk()->getContent();

    expect($artworksShardOne)
        ->toContain($fixtures['artwork_url'])
        ->not->toContain('/art/' . $secondArtwork->id . '/');
    expect($artworksShardTwo)->toContain('/art/' . $secondArtwork->id . '/');

    $allArtworkLocs = array_merge(extractUrlLocs($artworksShardOne), extractUrlLocs($artworksShardTwo));
    expect(array_unique($allArtworkLocs))->toHaveCount(count($allArtworkLocs));

    $this->get('/sitemaps/artworks-99.xml')->assertNotFound();
});

it('build command creates a validated sitemap release artifact set', function (): void {
    Storage::fake('local');

    config([
        'sitemaps.shards.enabled' => true,
        'sitemaps.shards.artworks.size' => 1,
        'sitemaps.shards.users.size' => 1,
        'sitemaps.releases.disk' => 'local',
        'sitemaps.releases.path' => 'sitemaps',
    ]);

    seedSitemapFixtures();
    $secondCreator = User::factory()->create([
        'username' => 'warmbuilder',
        'is_active' => true,
    ]);
    Artwork::factory()->create([
        'user_id' => $secondCreator->id,
        'title' => 'Warm Builder Artwork',
        'slug' => 'warm-builder-artwork',
        'hash' => str_repeat('e', 32),
        'file_path' => 'artworks/original/ee/ee/' . str_repeat('e', 32) . '.jpg',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subHours(6),
    ]);

    $code = Artisan::call('skinbase:sitemaps:build', [
        '--force' => true,
        '--shards' => true,
    ]);

    expect($code)->toBe(0)
        ->and(Artisan::output())
        ->toContain('Family [artworks] complete')
        ->toContain('Family [users] complete')
        ->toContain('Sitemap release [')
        ->toContain('status=validated');

    $releases = app(\App\Services\Sitemaps\SitemapReleaseManager::class)->listReleases();

    expect($releases)->toHaveCount(1)
        ->and($releases[0]['status'])->toBe('validated');

    $releaseId = $releases[0]['release_id'];

    Storage::disk('local')->assertExists('sitemaps/releases/' . $releaseId . '/sitemap.xml');
    Storage::disk('local')->assertExists('sitemaps/releases/' . $releaseId . '/sitemaps/artworks-index.xml');
    Storage::disk('local')->assertExists('sitemaps/releases/' . $releaseId . '/sitemaps/artworks-0001.xml');
    Storage::disk('local')->assertExists('sitemaps/releases/' . $releaseId . '/manifest.json');
});

it('publish and validate commands serve the active sitemap release', function (): void {
    Storage::fake('local');

    config([
        'sitemaps.shards.enabled' => true,
        'sitemaps.shards.artworks.size' => 1,
        'sitemaps.shards.users.size' => 1,
        'sitemaps.releases.disk' => 'local',
        'sitemaps.releases.path' => 'sitemaps',
    ]);

    seedSitemapFixtures();
    $secondCreator = User::factory()->create([
        'username' => 'validatoruser',
        'is_active' => true,
    ]);
    Artwork::factory()->create([
        'user_id' => $secondCreator->id,
        'title' => 'Validator Artwork',
        'slug' => 'validator-artwork',
        'hash' => str_repeat('f', 32),
        'file_path' => 'artworks/original/ff/ff/' . str_repeat('f', 32) . '.jpg',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subHours(5),
    ]);

    $publishCode = Artisan::call('skinbase:sitemaps:publish');

    expect($publishCode)->toBe(0)
        ->and(Artisan::output())->toContain('Published sitemap release');

    $activeRelease = app(\App\Services\Sitemaps\SitemapReleaseManager::class)->activeReleaseId();
    expect($activeRelease)->not->toBeNull();

    $indexXml = $this->get('/sitemap.xml')->assertOk()->getContent();
    expect($indexXml)
        ->toContain('/sitemaps/artworks-index.xml')
        ->toContain('/sitemaps/users-index.xml');

    $artworksXml = $this->get('/sitemaps/artworks.xml')->assertOk()->getContent();
    expect($artworksXml)->toContain('/sitemaps/artworks-0001.xml');

    $code = Artisan::call('skinbase:sitemaps:validate', ['--active' => true]);

    expect($code)->toBe(0)
        ->and(Artisan::output())
        ->toContain('Family [artworks]')
        ->toContain('Family [users]')
        ->toContain('Sitemap validation passed.');
});

it('can rollback sitemap delivery to the previous published release', function (): void {
    Storage::fake('local');

    config([
        'sitemaps.shards.enabled' => true,
        'sitemaps.shards.artworks.size' => 1,
        'sitemaps.shards.users.size' => 1,
        'sitemaps.releases.disk' => 'local',
        'sitemaps.releases.path' => 'sitemaps',
    ]);

    $fixtures = seedSitemapFixtures();

    expect(Artisan::call('skinbase:sitemaps:publish'))->toBe(0);
    $firstRelease = app(\App\Services\Sitemaps\SitemapReleaseManager::class)->activeReleaseId();

    $secondCreator = User::factory()->create([
        'username' => 'rollbackuser',
        'is_active' => true,
    ]);

    $newArtwork = Artwork::factory()->create([
        'user_id' => $secondCreator->id,
        'title' => 'Rollback Artwork',
        'slug' => 'rollback-artwork',
        'hash' => str_repeat('9', 32),
        'file_path' => 'artworks/original/99/99/' . str_repeat('9', 32) . '.jpg',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subHours(2),
    ]);

    expect(Artisan::call('skinbase:sitemaps:publish'))->toBe(0);
    $secondRelease = app(\App\Services\Sitemaps\SitemapReleaseManager::class)->activeReleaseId();

    expect($secondRelease)->not->toBe($firstRelease);

    $currentIndex = $this->get('/sitemaps/artworks.xml')->assertOk()->getContent();
    expect($currentIndex)->toContain('/sitemaps/artworks-0002.xml');

    expect(Artisan::call('skinbase:sitemaps:rollback', ['release' => $firstRelease]))->toBe(0);

    $rolledBackIndex = $this->get('/sitemaps/artworks.xml')->assertOk()->getContent();
    expect($rolledBackIndex)
        ->toContain($fixtures['artwork_url'])
        ->not->toContain('/art/' . $newArtwork->id . '/');
});

/**
 * @return array<string, int|string>
 */
function seedSitemapFixtures(): array
{
    $creator = User::factory()->create([
        'username' => 'sitemapuser',
        'is_active' => true,
    ]);

    $inactiveCreator = User::factory()->create([
        'username' => 'inactivecreator',
        'is_active' => false,
    ]);

    $contentType = ContentType::query()->create([
        'name' => 'Photography',
        'slug' => 'photography',
        'description' => 'Photography on Skinbase',
        'order' => 1,
    ]);

    $category = Category::query()->create([
        'content_type_id' => $contentType->id,
        'parent_id' => null,
        'name' => 'Portraits',
        'slug' => 'portraits',
        'description' => 'Portrait photography',
        'is_active' => true,
        'sort_order' => 1,
    ]);

    $publicTag = Tag::factory()->create([
        'name' => 'Featured Tag',
        'slug' => 'featured-tag',
        'usage_count' => 10,
        'is_active' => true,
    ]);

    $inactiveTag = Tag::factory()->inactive()->create([
        'name' => 'Inactive Tag',
        'slug' => 'inactive-tag',
        'usage_count' => 10,
    ]);

    $artwork = Artwork::factory()->create([
        'user_id' => $creator->id,
        'title' => 'Sitemap Artwork',
        'slug' => 'sitemap-artwork',
        'hash' => str_repeat('a', 32),
        'file_path' => 'artworks/original/aa/aa/' . str_repeat('a', 32) . '.jpg',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subDay(),
    ]);
    $artwork->categories()->attach($category->id);
    $artwork->tags()->attach($publicTag->id, ['source' => 'user']);

    $privateArtwork = Artwork::factory()->create([
        'user_id' => $inactiveCreator->id,
        'title' => 'Private Artwork',
        'slug' => 'private-artwork',
        'is_public' => false,
        'is_approved' => true,
        'published_at' => now()->subDay(),
    ]);
    $privateArtwork->tags()->attach($inactiveTag->id, ['source' => 'user']);

    $collection = Collection::factory()->create([
        'user_id' => $creator->id,
        'title' => 'Showcase Set',
        'slug' => 'showcase-set',
        'canonical_collection_id' => null,
    ]);

    $cardCategory = NovaCardCategory::query()->create([
        'slug' => 'mindset',
        'name' => 'Mindset',
        'description' => 'Mindset cards',
        'active' => true,
        'order_num' => 0,
    ]);

    $cardTemplate = NovaCardTemplate::query()->create([
        'slug' => 'sitemap-template',
        'name' => 'Sitemap Template',
        'description' => 'Sitemap test template',
        'config_json' => ['font_preset' => 'modern-sans'],
        'supported_formats' => ['square'],
        'active' => true,
        'official' => true,
        'order_num' => 0,
    ]);

    $card = NovaCard::query()->create([
        'user_id' => $creator->id,
        'category_id' => $cardCategory->id,
        'template_id' => $cardTemplate->id,
        'title' => 'Clarity Card',
        'slug' => 'clarity-card',
        'quote_text' => 'Clarity is a public card.',
        'description' => 'A public card used for the sitemap test.',
        'format' => NovaCard::FORMAT_SQUARE,
        'project_json' => ['layout' => ['layout' => 'quote_heavy']],
        'render_version' => 1,
        'background_type' => 'gradient',
        'preview_path' => 'cards/previews/clarity-card.webp',
        'visibility' => NovaCard::VISIBILITY_PUBLIC,
        'status' => NovaCard::STATUS_PUBLISHED,
        'moderation_status' => NovaCard::MOD_APPROVED,
        'published_at' => now()->subDay(),
    ]);

    insertFiltered('stories', [
        'slug' => 'creator-spotlight',
        'title' => 'Creator Spotlight',
        'excerpt' => 'A public story for the sitemap test.',
        'content' => 'Story body',
        'cover_image' => '/images/stories/creator-spotlight.webp',
        'creator_id' => $creator->id,
        'author_id' => null,
        'featured' => true,
        'status' => 'published',
        'published_at' => now()->subDay(),
    ]);

    Page::factory()->create([
        'slug' => 'about',
        'title' => 'About Skinbase',
        'is_published' => true,
        'published_at' => now()->subMonth(),
    ]);

    Page::factory()->create([
        'slug' => 'community-handbook',
        'title' => 'Community Handbook',
        'is_published' => true,
        'published_at' => now()->subWeek(),
    ]);

    $newsCategoryId = insertFiltered('news_categories', [
        'name' => 'Updates',
        'slug' => 'updates',
        'is_active' => true,
    ]);

    insertFiltered('news_articles', [
        'title' => 'Platform Update',
        'slug' => 'platform-update',
        'excerpt' => 'A news article for the sitemap test.',
        'content' => 'News body',
        'cover_image' => 'news/platform-update.webp',
        'author_id' => $creator->id,
        'category_id' => $newsCategoryId,
        'status' => 'published',
        'published_at' => now()->subDay(),
        'is_featured' => true,
        'views' => 10,
    ]);

    $forumCategoryId = insertFiltered('forum_categories', [
        'name' => 'General',
        'title' => 'General',
        'slug' => 'general',
        'description' => 'General forum category',
        'is_active' => true,
        'parent_id' => null,
        'position' => 1,
    ]);

    $forumBoardId = insertFiltered('forum_boards', [
        'category_id' => $forumCategoryId,
        'legacy_category_id' => null,
        'title' => 'General Board',
        'slug' => 'general-board',
        'description' => 'General board',
        'position' => 1,
        'is_active' => true,
        'is_read_only' => false,
    ]);

    insertFiltered('forum_topics', [
        'board_id' => $forumBoardId,
        'user_id' => $creator->id,
        'title' => 'Welcome Thread',
        'slug' => 'welcome-thread',
        'views' => 1,
        'replies_count' => 0,
        'is_pinned' => false,
        'is_locked' => false,
        'is_deleted' => false,
        'last_post_at' => now()->subHours(2),
    ]);

    return [
        'artwork_url' => route('art.show', [
            'id' => $artwork->id,
            'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id,
        ]),
        'private_artwork_id' => $privateArtwork->id,
        'profile_url' => route('profile.show', ['username' => 'sitemapuser']),
        'collection_url' => route('profile.collections.show', ['username' => 'sitemapuser', 'slug' => 'showcase-set']),
        'card_url' => route('cards.show', ['slug' => 'clarity-card', 'id' => $card->id]),
    ];
}

function ensureNewsSchema(): void
{
    if (! Schema::hasTable('news_categories')) {
        Schema::create('news_categories', function (Blueprint $table): void {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    if (! Schema::hasTable('news_articles')) {
        Schema::create('news_articles', function (Blueprint $table): void {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('excerpt')->nullable();
            $table->longText('content')->nullable();
            $table->string('cover_image')->nullable();
            $table->foreignId('author_id')->nullable();
            $table->foreignId('category_id')->nullable();
            $table->string('status')->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->boolean('is_featured')->default(false);
            $table->unsignedInteger('views')->default(0);
            $table->softDeletes();
            $table->timestamps();
        });
    }
}

function ensureForumSchema(): void
{
    if (! Schema::hasTable('forum_categories')) {
        Schema::create('forum_categories', function (Blueprint $table): void {
            $table->id();
            $table->string('name');
            $table->string('title')->nullable();
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->boolean('is_active')->default(true);
            $table->foreignId('parent_id')->nullable();
            $table->integer('position')->default(0);
            $table->timestamps();
        });
    } else {
        ensureColumn('forum_categories', 'title', fn (Blueprint $table) => $table->string('title')->nullable());
        ensureColumn('forum_categories', 'description', fn (Blueprint $table) => $table->text('description')->nullable());
        ensureColumn('forum_categories', 'is_active', fn (Blueprint $table) => $table->boolean('is_active')->default(true));
    }

    if (! Schema::hasTable('forum_boards')) {
        Schema::create('forum_boards', function (Blueprint $table): void {
            $table->id();
            $table->foreignId('category_id');
            $table->foreignId('legacy_category_id')->nullable();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->integer('position')->default(0);
            $table->boolean('is_active')->default(true);
            $table->boolean('is_read_only')->default(false);
            $table->timestamps();
        });
    }

    if (! Schema::hasTable('forum_topics')) {
        Schema::create('forum_topics', function (Blueprint $table): void {
            $table->id();
            $table->foreignId('board_id');
            $table->foreignId('user_id')->nullable();
            $table->foreignId('artwork_id')->nullable();
            $table->unsignedBigInteger('legacy_thread_id')->nullable();
            $table->string('title');
            $table->string('slug')->unique();
            $table->unsignedInteger('views')->default(0);
            $table->unsignedInteger('replies_count')->default(0);
            $table->boolean('is_pinned')->default(false);
            $table->boolean('is_locked')->default(false);
            $table->boolean('is_deleted')->default(false);
            $table->unsignedBigInteger('last_post_id')->nullable();
            $table->timestamp('last_post_at')->nullable();
            $table->timestamps();
        });
    }
}

function ensureColumn(string $table, string $column, Closure $callback): void
{
    if (! Schema::hasColumn($table, $column)) {
        Schema::table($table, $callback);
    }
}

/**
 * @param  array<string, mixed>  $attributes
 */
function insertFiltered(string $table, array $attributes): int
{
    $columns = array_flip(Schema::getColumnListing($table));
    $payload = [];

    foreach ($attributes as $column => $value) {
        if (isset($columns[$column])) {
            $payload[$column] = $value;
        }
    }

    if (isset($columns['created_at']) && ! array_key_exists('created_at', $payload)) {
        $payload['created_at'] = now();
    }

    if (isset($columns['updated_at']) && ! array_key_exists('updated_at', $payload)) {
        $payload['updated_at'] = now();
    }

    return (int) DB::table($table)->insertGetId($payload);
}

/**
 * @return list<string>
 */
function extractUrlLocs(string $xml): array
{
    $document = simplexml_load_string($xml);

    if ($document === false) {
        return [];
    }

    $nodes = $document->xpath('//*[local-name()="url"]/*[local-name()="loc"]') ?: [];

    return array_values(array_filter(array_map(
        static fn ($node): string => trim((string) $node),
        $nodes,
    )));
}