<?php

namespace Tests\Feature\CatalogLandingPages;

use App\Enums\LandingPageLayoutMode;
use App\Enums\LandingPageVersion;
use App\Jobs\AnalyzeCatalogWebsiteJob;
use App\Jobs\DiscoverCatalogProductsJob;
use App\Jobs\GenerateCatalogLandingPagesJob;
use App\Livewire\CatalogLandingPages\ChatWizard;
use App\Models\LandingPage;
use App\Models\LandingPageGeneration;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Laravel\Ai\StructuredAnonymousAgent;
use Livewire\Livewire;
use Tests\TestCase;

class ChatWizardTest extends TestCase
{
    use RefreshDatabase;

    public function test_the_route_renders_the_wizard(): void
    {
        $this->actingAs(User::factory()->create())
            ->get(route('catalog-landing-pages.create'))
            ->assertOk()
            ->assertSeeLivewire(ChatWizard::class)
            ->assertSee('brand name for this catalog')
            ->assertDontSee('Product URLs');
    }

    public function test_guests_are_redirected_from_the_wizard(): void
    {
        $this->get(route('catalog-landing-pages.create'))
            ->assertRedirect(route('login'));
    }

    public function test_create_page_resumes_template_selection_for_in_progress_generation(): void
    {
        $user = User::factory()->create();
        $generation = LandingPageGeneration::factory()->catalog()->create([
            'user_id' => $user->id,
            'status' => 'awaiting_template',
            'brand_name' => 'Office Depot',
            'brand_url' => 'https://shop.example.com/listing',
            'product_urls' => array_fill(0, 5, 'https://shop.example.com/a/products/1/tent-1'),
            'catalog' => [
                'research' => [
                    'source' => 'listing',
                    'listing_url' => 'https://shop.example.com/listing',
                    'category' => 'Office',
                    'subcategory' => 'Chairs',
                    'products' => $this->productRows(5),
                ],
            ],
        ]);

        $this->actingAs($user)
            ->get(route('catalog-landing-pages.create', ['generation' => $generation->id]))
            ->assertOk()
            ->assertSeeLivewire(ChatWizard::class)
            ->assertSee('Which layout should we use for every page?')
            ->assertSee('Template 1')
            ->assertSee('Template 2')
            ->assertSee('Tent 1');
    }

    public function test_create_page_resumes_discovering_step_for_in_progress_generation(): void
    {
        $user = User::factory()->create();
        $generation = LandingPageGeneration::factory()->catalog()->create([
            'user_id' => $user->id,
            'status' => 'discovering',
            'brand_name' => 'Office Depot',
            'brand_url' => 'https://shop.example.com/listing',
            'product_urls' => [],
            'catalog' => [
                'research' => [
                    'source' => 'listing',
                    'listing_url' => 'https://shop.example.com/listing',
                ],
            ],
        ]);

        Livewire::actingAs($user)
            ->withQueryParams(['generation' => $generation->id])
            ->test(ChatWizard::class)
            ->assertSet('step', 'discovering')
            ->assertSet('brandName', 'Office Depot')
            ->assertSet('brandUrl', 'https://shop.example.com/listing')
            ->assertSet('researchSource', 'listing');
    }

    public function test_wizard_ignores_generation_owned_by_another_user(): void
    {
        $owner = User::factory()->create();
        $other = User::factory()->create();
        $generation = LandingPageGeneration::factory()->catalog()->create([
            'user_id' => $owner->id,
            'status' => 'awaiting_template',
            'brand_name' => 'Office Depot',
            'brand_url' => 'https://shop.example.com/listing',
            'product_urls' => array_fill(0, 5, 'https://shop.example.com/a/products/1/tent-1'),
            'catalog' => [
                'research' => [
                    'source' => 'listing',
                    'products' => $this->productRows(5),
                ],
            ],
        ]);

        Livewire::actingAs($other)
            ->withQueryParams(['generation' => $generation->id])
            ->test(ChatWizard::class)
            ->assertSet('generationId', null)
            ->assertSet('step', 'brand');
    }

    public function test_listing_url_is_required_before_discovery(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->assertSet('step', 'products')
            ->call('submitProducts')
            ->assertHasErrors(['listingUrl']);

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

    public function test_a_homepage_url_is_rejected_as_the_listing_url(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('listingUrl', 'https://www.kohls.com/')
            ->call('submitListingProducts')
            ->assertHasErrors(['listingUrl'])
            ->assertSee('not the store homepage')
            ->assertSet('step', 'products');

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

    public function test_a_website_homepage_url_is_accepted_via_legacy_website_flow(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://www.officedepot.com/')
            ->call('submitWebsite')
            ->assertHasNoErrors()
            ->assertSet('step', 'analyzing');

        $this->assertSame(1, LandingPageGeneration::query()->count());
        Bus::assertDispatched(AnalyzeCatalogWebsiteJob::class);
    }

    public function test_a_website_url_is_rejected_as_the_brand_name(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'https://www.officedepot.com/')
            ->call('submitBrand')
            ->assertHasErrors(['brandName'])
            ->assertSet('step', 'brand')
            ->assertSee('Enter the brand name, not the website URL.');
    }

    public function test_typing_the_website_url_does_not_overwrite_the_brand_name(): void
    {
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Office Depot')
            ->call('submitBrand')
            ->assertSet('confirmedBrandName', 'Office Depot')
            ->set('brandName', 'https://www.officedepot.com/')
            ->assertSet('brandName', 'Office Depot')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->assertSet('brandName', 'Office Depot')
            ->call('submitWebsite')
            ->assertSee('Office Depot');

        $this->assertSame('Office Depot', LandingPageGeneration::query()->sole()->brand_name);
    }

    public function test_it_creates_a_researching_generation_and_lists_categories(): void
    {
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'category')
            ->assertSet('categories', [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]);

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('Hoe', $generation->brand_name);
        $this->assertSame('https://shop.example.com', $generation->brand_url);
        $this->assertSame([], $generation->product_urls);
        $this->assertSame(LandingPageLayoutMode::Catalog, $generation->layout_mode);
        $this->assertSame('awaiting_category', $generation->status);
        $this->assertSame(0, $generation->landingPages()->count());
    }

    public function test_products_step_offers_listing_and_json_choices(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->assertSet('step', 'products')
            ->assertSee('Product listing URL')
            ->assertSee('Upload JSON')
            ->call('chooseProductSource', 'json')
            ->assertSet('productSource', 'json')
            ->assertSee('Required JSON format')
            ->assertSee('data-copy-target="catalog-products-json-source"', false)
            ->assertSee('title="Copy template"', false)
            ->assertSee('title="Download template"', false)
            ->assertSee('Or paste JSON');
    }

    public function test_a_listing_url_skips_categories_and_discovers_products(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('listingUrl', 'https://shop.example.com/fresh-seedlings')
            ->call('submitListingProducts')
            ->assertSet('step', 'discovering')
            ->assertSee('from this listing');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('discovering', $generation->status);
        $this->assertSame('listing', $generation->catalog['research']['source']);
        $this->assertSame('https://shop.example.com/fresh-seedlings', $generation->catalog['research']['listing_url']);
        $this->assertSame('https://shop.example.com/fresh-seedlings', $generation->brand_url);

        Bus::assertDispatched(
            DiscoverCatalogProductsJob::class,
            fn (DiscoverCatalogProductsJob $job): bool => $job->generationId === $generation->id,
        );
        Bus::assertNotDispatched(AnalyzeCatalogWebsiteJob::class);
    }

    public function test_listing_scrape_failure_offers_manual_json_import(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $this->actingAs(User::factory()->create());

        $generation = LandingPageGeneration::factory()->catalog()->create([
            'user_id' => auth()->id(),
            'status' => 'awaiting_products',
            'brand_name' => 'Kohl\'s',
            'brand_url' => 'https://www.kohls.com/catalog/mens-teens.jsp',
            'catalog' => [
                'research' => [
                    'source' => 'listing',
                    'listing_url' => 'https://www.kohls.com/catalog/mens-teens.jsp',
                    'products' => [],
                    'empty_reason' => 'blocked',
                ],
            ],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('generationId', $generation->id)
            ->call('syncFromGeneration')
            ->assertSet('step', 'empty')
            ->assertSee('Add products manually')
            ->assertSee('Add products via JSON')
            ->call('backToProducts', 'json')
            ->assertSet('step', 'products')
            ->assertSet('productSource', 'json')
            ->assertSee('Or paste JSON');

        $generation->refresh();
        $this->assertTrue($generation->catalog['research']['manual_input']);
        $this->assertSame('json', $generation->catalog['research']['manual_input_source']);
    }

    public function test_going_back_from_discovering_keeps_json_upload_available(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $user = User::factory()->create();
        $generation = LandingPageGeneration::factory()->catalog()->create([
            'user_id' => $user->id,
            'status' => 'discovering',
            'brand_name' => 'Kohl\'s',
            'brand_url' => 'https://www.kohls.com/catalog/mens-teens.jsp',
            'catalog' => [
                'research' => [
                    'source' => 'listing',
                    'listing_url' => 'https://www.kohls.com/catalog/mens-teens.jsp',
                ],
            ],
        ]);

        Livewire::actingAs($user)
            ->test(ChatWizard::class)
            ->set('generationId', $generation->id)
            ->call('syncFromGeneration')
            ->assertSet('step', 'discovering')
            ->assertSee('Add products via JSON instead')
            ->call('useJsonImportInstead')
            ->assertSet('step', 'products')
            ->assertSet('productSource', 'json')
            ->assertSee('Upload JSON')
            ->call('syncFromGeneration')
            ->assertSet('step', 'products')
            ->assertSet('productSource', 'json');

        $generation->refresh();
        $this->assertSame('awaiting_products', $generation->status);
        $this->assertTrue($generation->catalog['research']['manual_input']);
    }

    public function test_valid_json_moves_to_edit_step_before_discovery(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Kohl\'s')
            ->call('submitBrand')
            ->call('chooseProductSource', 'json')
            ->set('productsJson', $this->importableProductsJson(2))
            ->call('submitProducts')
            ->assertHasNoErrors()
            ->assertSet('step', 'json_edit')
            ->assertSee('Review product JSON')
            ->assertSee('Minimum 2 valid products');

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

    public function test_json_edit_submission_dispatches_product_discovery(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Kohl\'s')
            ->call('submitBrand')
            ->call('chooseProductSource', 'json')
            ->set('productsJson', $this->importableProductsJson(2))
            ->call('submitProducts')
            ->assertSet('step', 'json_edit')
            ->call('submitProducts')
            ->assertSet('step', 'discovering')
            ->assertSee('uploaded products');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('json', $generation->catalog['research']['source']);
        $this->assertCount(2, $generation->catalog['research']['uploaded_products']);
        Bus::assertDispatched(DiscoverCatalogProductsJob::class);
    }

    public function test_invalid_json_shows_validation_errors(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Kohl\'s')
            ->call('submitBrand')
            ->call('chooseProductSource', 'json')
            ->set('productsJson', '{"products":[]}')
            ->call('submitProducts')
            ->assertHasErrors(['productsJson'])
            ->assertSet('step', 'products');
    }

    public function test_json_with_one_product_is_rejected(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Kohl\'s')
            ->call('submitBrand')
            ->call('chooseProductSource', 'json')
            ->set('productsJson', $this->importableProductsJson(1))
            ->call('submitProducts')
            ->assertHasErrors(['productsJson'])
            ->assertSee('at least 2')
            ->assertSet('step', 'products');
    }

    public function test_listing_retry_redispatches_product_discovery(): void
    {
        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);
        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('listingUrl', 'https://shop.example.com/fresh-seedlings')
            ->call('submitListingProducts');

        $component->call('retryAnalysis')
            ->assertSet('step', 'discovering');

        Bus::assertDispatchedTimes(DiscoverCatalogProductsJob::class, 2);
        Bus::assertNotDispatched(AnalyzeCatalogWebsiteJob::class);
        $this->assertSame('listing', LandingPageGeneration::query()->sole()->catalog['research']['source']);
    }

    public function test_it_logs_category_subcategory_and_product_url_fetches(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
            ]],
            ['products' => $this->productRows(6)],
        ]);

        $logged = [];
        Log::listen(function (MessageLogged $entry) use (&$logged): void {
            $logged[] = $entry;
        });

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->call('selectSubcategory', 'Tents');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('awaiting_template', $generation->status);
        $messages = array_map(fn (MessageLogged $entry): string => $entry->message, $logged);

        $this->assertContains('Catalog categories fetched', $messages);
        $this->assertContains('Catalog subcategories fetched', $messages);
        $this->assertContains('Catalog product listing URLs', $messages);
        $this->assertContains('Catalog product candidates validated', $messages);
        $this->assertContains('Catalog product URLs ranked', $messages);

        foreach ($logged as $entry) {
            if ($entry->message === 'Catalog categories fetched') {
                $this->assertSame($generation->id, $entry->context['generation_id']);
                $this->assertSame('https://shop.example.com', $entry->context['website']);
            }

            if ($entry->message === 'Catalog product listing URLs') {
                $this->assertSame(['https://shop.example.com/outdoor/tents'], $entry->context['listing_urls']);
            }
        }
    }

    public function test_choosing_a_template_after_products_are_found_starts_generation(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
            ]],
            ['products' => $this->productRows(6)],
        ]);

        $component = Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->assertSet('step', 'subcategory')
            ->call('selectSubcategory', 'Tents')
            ->assertSet('step', 'template')
            ->assertSee('Which layout should we use for every page?')
            ->assertSee('Template 1')
            ->assertSee('Template 2')
            ->assertSee('Tent 1')
            ->assertSee('Preview Template 1 full page')
            ->assertSee('Preview Template 2 full page')
            ->assertSee('overflow: auto', false)
            ->assertDontSee('pointer-events: none', false);

        $this->assertMatchesRegularExpression(
            '/id="catalog-template-preview-modal"[^>]*\bhidden\b/',
            $component->html(),
        );

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('awaiting_template', $generation->status);
        $this->assertCount(6, $generation->product_urls);
        $this->assertSame($this->productUrl(1), $generation->product_urls[0]);
        $this->assertSame('Outdoor', $generation->catalog['research']['category']);
        $this->assertSame('Tents', $generation->catalog['research']['subcategory']);
        $this->assertSame(
            [['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents']],
            $generation->catalog['research']['selected_subcategories'],
        );
        $this->assertSame(0, $generation->landingPages()->count());
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);

        $component
            ->call('selectTemplate', 'nope')
            ->assertSet('step', 'template');

        $this->assertSame('awaiting_template', $generation->fresh()->status);
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);

        $component
            ->call('selectTemplate', 'template-1')
            ->assertSet('step', 'theme');

        $generation->refresh();
        $this->assertSame('awaiting_theme', $generation->status);
        $this->assertSame('template-1', $generation->catalog['research']['template']);
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);

        $component
            ->call('selectTheme', 'dermstore')
            ->assertRedirect(route('catalog-landing-pages.show', $generation));

        $generation->refresh()->load('landingPages');
        $this->assertSame('queued', $generation->status);
        $this->assertSame('dermstore', $generation->catalog['research']['theme']);
        $this->assertCount(6, $generation->landingPages);
        $this->assertTrue($generation->landingPages->every(fn (LandingPage $page) => $page->template === 'template-1'));
        $this->assertTrue($generation->landingPages->every(fn (LandingPage $page) => $page->theme === 'dermstore'));
        $this->assertEqualsCanonicalizing(
            array_map(fn (LandingPageVersion $version) => $version->value, LandingPageVersion::cases()),
            $generation->landingPages->pluck('version')->map->value->all(),
        );

        Bus::assertDispatched(
            GenerateCatalogLandingPagesJob::class,
            fn (GenerateCatalogLandingPagesJob $job) => $job->generationId === $generation->id,
        );
    }

    public function test_fewer_than_five_products_does_not_dispatch_the_catalog_job(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        Http::preventStrayRequests();
        $responses = [
            'https://shop.example.com/robots.txt' => Http::response("User-agent: *\nDisallow:\n"),
            'https://shop.example.com' => Http::response('<html><nav>Outdoor Camping</nav></html>'),
            'https://shop.example.com/sitemap.xml' => Http::response('<urlset></urlset>'),
            'https://shop.example.com/outdoor' => Http::response(
                '<html><a href="/outdoor/tents">Tents</a></html>'
            ),
            'https://shop.example.com/outdoor/tents' => Http::response($this->listingHtmlWithProducts(4)),
        ];

        for ($i = 1; $i <= 4; $i++) {
            $responses[$this->productUrl($i)] = Http::response($this->productPageHtml($i));
        }

        Http::fake($responses);
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
            ]],
            ['products' => $this->productRows(4)],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->call('selectSubcategory', 'Tents')
            ->assertSet('step', 'empty')
            ->assertSet('emptyReason', 'no_products')
            ->assertSee('No products found');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame([], $generation->product_urls);
        $this->assertSame('awaiting_subcategory', $generation->status);
        $this->assertSame(0, $generation->landingPages()->count());
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_zero_products_does_not_dispatch_the_catalog_job(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        Http::preventStrayRequests();
        Http::fake([
            'https://shop.example.com/robots.txt' => Http::response("User-agent: *\nDisallow: /admin\n"),
            'https://shop.example.com' => Http::response('<html><nav>Outdoor Camping</nav></html>'),
            'https://shop.example.com/sitemap.xml' => Http::response('<urlset></urlset>'),
            'https://shop.example.com/outdoor' => Http::response('<html>Tents Sleeping bags</html>'),
            'https://shop.example.com/outdoor/tents' => Http::response('<html>Top rated tents</html>'),
        ]);
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
            ]],
            ['products' => []],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->call('selectSubcategory', 'Tents')
            ->assertSet('step', 'empty')
            ->assertSet('emptyReason', 'no_products');

        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_off_host_product_urls_are_dropped_instead_of_padded(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        Http::preventStrayRequests();
        Http::fake([
            'https://shop.example.com/robots.txt' => Http::response("User-agent: *\nDisallow:\n"),
            'https://shop.example.com' => Http::response('<html><nav>Outdoor Camping</nav></html>'),
            'https://shop.example.com/sitemap.xml' => Http::response('<urlset></urlset>'),
            'https://shop.example.com/outdoor' => Http::response(
                '<html><a href="/outdoor/tents">Tents</a></html>'
            ),
            'https://shop.example.com/outdoor/tents' => Http::response($this->listingHtmlWithProducts(1)),
            $this->productUrl(1) => Http::response($this->productPageHtml(1)),
        ]);
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
            ]],
            ['products' => [
                ['name' => 'Tent', 'url' => 'https://amazon.example.com/tent', 'popularity_note' => 'Bestseller'],
                ['name' => 'Dome', 'url' => $this->productUrl(1), 'popularity_note' => '4.8 stars'],
            ]],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->call('selectSubcategory', 'Tents')
            ->assertSet('step', 'empty');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame([], $generation->product_urls);
        $this->assertCount(1, $generation->catalogResearch()['products']);
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_category_cannot_be_set_directly_from_the_client(): void
    {
        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class);

        try {
            $component->set('category', 'Outdoor');
            $this->fail('Expected setting locked property [category] from the client to be rejected.');
        } catch (\Throwable $e) {
            $this->assertStringContainsStringIgnoringCase('locked', $e->getMessage());
        }
    }

    public function test_empty_categories_skip_to_product_fetching_and_show_no_products_when_none_exist(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => []],
            ['products' => []],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'empty')
            ->assertSet('emptyReason', 'no_products')
            ->assertSee('No products found')
            ->assertDontSee('I found these categories');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame([], $generation->product_urls);
        Bus::assertNotDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_empty_website_research_offers_a_listing_url_fallback(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => []],
            ['products' => []],
        ]);

        $component = Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'empty')
            ->assertSee('Listing URL');

        Bus::fake([
            AnalyzeCatalogWebsiteJob::class,
            DiscoverCatalogProductsJob::class,
            GenerateCatalogLandingPagesJob::class,
        ]);

        $component
            ->set('listingUrl', 'https://shop.example.com/fresh-seedlings')
            ->call('submitListingFallback')
            ->assertSet('step', 'discovering')
            ->assertSet('researchSource', 'listing')
            ->assertSee('from this listing');

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('discovering', $generation->status);
        $this->assertSame('listing', $generation->catalog['research']['source']);
        $this->assertSame('https://shop.example.com/fresh-seedlings', $generation->catalog['research']['listing_url']);
        $this->assertSame('https://shop.example.com/fresh-seedlings', $generation->brand_url);

        Bus::assertDispatched(
            DiscoverCatalogProductsJob::class,
            fn (DiscoverCatalogProductsJob $job): bool => $job->generationId === $generation->id,
        );
        Bus::assertNotDispatched(AnalyzeCatalogWebsiteJob::class);
    }

    public function test_listing_fallback_requires_a_listing_url(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => []],
            ['products' => []],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'empty')
            ->call('submitListingFallback')
            ->assertHasErrors(['listingUrl'])
            ->assertSet('step', 'empty');

        $this->assertSame('website', LandingPageGeneration::query()->sole()->catalog['research']['source']);
    }

    public function test_empty_subcategories_skip_to_product_fetching(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => []],
            ['products' => $this->productRows(6)],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSee('I found these categories')
            ->call('selectCategory', 'Outdoor')
            ->assertSet('step', 'template')
            ->call('selectTemplate', 'template-2')
            ->assertSet('step', 'theme')
            ->call('selectTheme', 'light')
            ->assertRedirect(route('catalog-landing-pages.show', LandingPageGeneration::query()->sole()));

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('queued', $generation->status);
        $this->assertCount(6, $generation->product_urls);
        $this->assertSame('Outdoor', $generation->catalog['research']['category']);
        $this->assertNull($generation->catalog['research']['subcategory'] ?? null);
        $this->assertSame('template-2', $generation->catalog['research']['template']);
        $this->assertSame('light', $generation->catalog['research']['theme']);
        $this->assertTrue($generation->landingPages->every(fn (LandingPage $page) => $page->template === 'template-2'));
        $this->assertTrue($generation->landingPages->every(fn (LandingPage $page) => $page->theme === 'light'));
        Bus::assertDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_multiple_subcategories_are_used_for_product_fetching(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
                ['name' => 'Sleeping bags', 'url' => 'https://shop.example.com/outdoor/sleeping-bags'],
            ]],
            ['products' => $this->productRows(6)],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->assertSet('step', 'subcategory')
            ->call('toggleSubcategory', 'Tents')
            ->call('toggleSubcategory', 'Sleeping bags')
            ->call('confirmSubcategories')
            ->assertSet('step', 'template')
            ->call('selectTemplate', 'template-1')
            ->assertSet('step', 'theme')
            ->call('selectTheme', 'amazon')
            ->assertRedirect(route('catalog-landing-pages.show', LandingPageGeneration::query()->sole()));

        $generation = LandingPageGeneration::query()->sole();
        $this->assertSame('Sleeping bags', $generation->catalog['research']['subcategory']);
        $this->assertSame([
            ['name' => 'Sleeping bags', 'url' => 'https://shop.example.com/outdoor/sleeping-bags'],
        ], $generation->catalog['research']['selected_subcategories']);
        $this->assertStringContainsString('Subcategory: Sleeping bags.', (string) $generation->notes);
        Bus::assertDispatched(GenerateCatalogLandingPagesJob::class);
    }

    public function test_subcategory_step_offers_a_single_choice(): void
    {
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => [
                ['name' => 'Outdoor', 'url' => 'https://shop.example.com/outdoor'],
            ]],
            ['subcategories' => [
                ['name' => 'Tents', 'url' => 'https://shop.example.com/outdoor/tents'],
                ['name' => 'Sleeping bags', 'url' => 'https://shop.example.com/outdoor/sleeping-bags'],
            ]],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->call('selectCategory', 'Outdoor')
            ->assertSet('step', 'subcategory')
            ->assertSee('Which subcategory should we catalog?')
            ->assertDontSee('You can pick more than one')
            ->assertSee('Continue')
            ->assertSee('Tents')
            ->assertSee('Sleeping bags')
            ->assertSee('Pick another category')
            ->call('toggleSubcategory', 'Tents')
            ->assertSet('selectedSubcategories', ['Tents'])
            ->call('toggleSubcategory', 'Sleeping bags')
            ->assertSet('selectedSubcategories', ['Sleeping bags'])
            ->assertSet('step', 'subcategory')
            ->call('toggleSubcategory', 'Sleeping bags')
            ->assertSet('selectedSubcategories', []);
    }

    public function test_it_shows_category_chips_from_nav_links_when_the_model_returns_none(): void
    {
        $this->actingAs(User::factory()->create());
        Http::preventStrayRequests();
        Http::fake([
            'https://shop.example.com/robots.txt' => Http::response("User-agent: *\nDisallow: /\n"),
            'https://shop.example.com' => Http::response(
                '<html><nav><a href="/women">Women</a><a href="/men">Men</a></nav></html>'
            ),
            'https://shop.example.com/sitemap.xml' => Http::response('<urlset></urlset>'),
        ]);
        StructuredAnonymousAgent::fake([
            ['categories' => []],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'category')
            ->assertSee('Women')
            ->assertSee('Men')
            ->assertSee('I found these categories');
    }

    public function test_empty_categories_do_not_claim_that_categories_were_found(): void
    {
        Bus::fake([GenerateCatalogLandingPagesJob::class]);
        $this->actingAs(User::factory()->create());
        $this->fakeShopSite();
        StructuredAnonymousAgent::fake([
            ['categories' => []],
            ['products' => []],
        ]);

        Livewire::test(ChatWizard::class)
            ->set('brandName', 'Hoe')
            ->call('submitBrand')
            ->set('researchSource', 'website')
            ->set('brandUrl', 'https://shop.example.com')
            ->call('submitWebsite')
            ->assertSet('step', 'empty')
            ->assertSet('emptyReason', 'no_products')
            ->assertDontSee('I found these categories');
    }

    private function fakeShopSite(): void
    {
        Http::preventStrayRequests();

        $responses = [
            'https://shop.example.com/robots.txt' => Http::response("User-agent: *\nDisallow: /admin\n"),
            'https://shop.example.com' => Http::response('<html><nav>Outdoor Camping</nav></html>'),
            'https://shop.example.com/sitemap.xml' => Http::response('<urlset></urlset>'),
            'https://shop.example.com/outdoor' => Http::response(
                '<html><a href="/outdoor/tents">Tents</a><a href="/outdoor/sleeping-bags">Sleeping bags</a>'
                .$this->productAnchors(6).'</html>'
            ),
            'https://shop.example.com/outdoor/tents' => Http::response($this->listingHtmlWithProducts(6)),
            'https://shop.example.com/outdoor/sleeping-bags' => Http::response($this->listingHtmlWithProducts(6)),
        ];

        for ($i = 1; $i <= 6; $i++) {
            $responses[$this->productUrl($i)] = Http::response($this->productPageHtml($i));
        }

        Http::fake($responses);
    }

    private function listingHtmlWithProducts(int $count): string
    {
        return '<html>'.$this->productAnchors($count).'</html>';
    }

    private function productAnchors(int $count): string
    {
        $anchors = '';

        for ($i = 1; $i <= $count; $i++) {
            $path = parse_url($this->productUrl($i), PHP_URL_PATH);
            $anchors .= '<a href="'.(string) $path.'">Tent '.$i.'</a>';
        }

        return $anchors;
    }

    private function productUrl(int $id): string
    {
        return "https://shop.example.com/a/products/{$id}/tent-{$id}";
    }

    private function productImageUrl(int $id): string
    {
        return "https://cdn.example.com/products/{$id}/image.jpg";
    }

    private function productPageHtml(int $id): string
    {
        $price = number_format(19.99 + $id, 2, '.', '');

        return <<<HTML
<html>
<head>
<meta property="og:image" content="{$this->productImageUrl($id)}">
<script type="application/ld+json">{"@type":"Product","offers":{"@type":"Offer","priceCurrency":"USD","price":"{$price}"}}</script>
</head>
<body>Product {$id}</body>
</html>
HTML;
    }

    /**
     * @return list<array{name: string, url: string, popularity_note: string}>
     */
    private function productRows(int $count): array
    {
        $rows = [];

        for ($i = 1; $i <= $count; $i++) {
            $rows[] = [
                'name' => "Tent {$i}",
                'url' => $this->productUrl($i),
                'popularity_note' => 'Top rated',
            ];
        }

        return $rows;
    }

    private function importableProductsJson(int $count): string
    {
        $products = [];

        for ($i = 1; $i <= $count; $i++) {
            $products[] = [
                'name' => "Product {$i}",
                'url' => $this->productUrl($i),
                'price' => '$19.99',
                'image_url' => $this->productImageUrl($i),
                'rating' => '4.8',
                'review_count' => (string) ($i * 10),
                'keywords' => [
                    'primary' => "product {$i}",
                ],
            ];
        }

        return json_encode(['products' => $products], JSON_THROW_ON_ERROR);
    }
}
