<?php

namespace Tests\Feature;

use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\SiteURI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
use Config\Services;
use PHPUnit\Framework\Attributes\DataProvider;

/**
 * The grid search boxes on the Dashboard and Campaign Performance tables.
 *
 * QA asked for a search over the values in the grid, "like in Breakdown" - the
 * topbar control is a global campaign navigator (AP-56) and only ever navigates.
 * Both tables are wired the Breakdown way: an input in the card header, bound to
 * the DataTable by `data-search-container` on the <table>.
 *
 * Asserted on the rendered markup because there is no other level that can see
 * it, and because the wiring has two silent failure modes that look identical to
 * "works" in a code review:
 *
 *   1. `data-search="false"`. common-datatable.js reads `searching` and passes it
 *      straight to DataTables; `data-search-container` does NOT override it. Both
 *      tables carried "false" before this change, so the box would render, the
 *      keyup handler would bind, and `table.search()` would be a no-op.
 *   2. A container selector that does not match any id in the same document.
 *      `wireExternalSearch` bails silently on an empty lookup.
 *
 * Neither leaves a trace at runtime, so both are pinned here.
 */
final class GridSearchWiringTest extends CIUnitTestCase
{
    private int $obLevel = 0;

    protected function setUp(): void
    {
        parent::setUp();
        $this->obLevel = ob_get_level();
    }

    protected function tearDown(): void
    {
        while (ob_get_level() > $this->obLevel) {
            ob_end_clean();
        }
        parent::tearDown();
    }

    private function render(string $view, array $data, string $uri): string
    {
        helper(['utils', 'auth']);

        $config = config(App::class);
        Services::injectMock('request', new IncomingRequest(
            $config,
            new SiteURI($config, $uri),
            null,
            new UserAgent()
        ));

        return view($view, $data);
    }

    private function dashboard(): string
    {
        return $this->render('dashboard/partials/table_section', [
            'campaignTableHeaders' => [
                ['label' => 'Campaign', 'class' => 'left-align'],
                ['label' => 'Spend', 'class' => 'right-align'],
            ],
            'campaignTableRows' => [
                [['html' => '<a href="#">Alpha</a>'], '$10.00'],
                [['html' => '<a href="#">Beta</a>'], '$20.00'],
            ],
            'periodSummary'     => ['current' => 'Aug 1 - Aug 7', 'previous' => ''],
            'dashboardFilters'  => [],
        ], 'dashboard');
    }

    private function campaignPerformance(bool $empty = false): string
    {
        return $this->render('campaign_performance/partials/table_section', [
            'cpTableEmpty'   => $empty,
            'tablePeriod'    => 'Aug 1 - Aug 7',
            'cpTableHeaders' => [
                ['label' => 'Campaign', 'class' => 'left-align'],
                ['label' => 'Spend', 'class' => 'right-align'],
            ],
            'cpTableRows' => $empty ? [] : [
                [['html' => '<a href="#">Alpha</a>'], '$10.00'],
            ],
        ], 'campaign-performance');
    }

    /**
     * @return iterable<string, array{0: string}>
     */
    public static function gridProvider(): iterable
    {
        yield 'dashboard' => ['dashboard'];
        yield 'campaign performance' => ['campaignPerformance'];
    }

    /**
     * Every `data-search-container` must point at an id that exists in the same
     * markup, or the search input is inert.
     */
    #[DataProvider('gridProvider')]
    public function testSearchContainerResolvesToAnIdInTheSameView(string $case): void
    {
        $html = $case === 'dashboard' ? $this->dashboard() : $this->campaignPerformance();

        $found = preg_match_all('/data-search-container="#([^"]+)"/', $html, $m);
        $this->assertSame(1, $found, 'expected exactly one search container on ' . $case);

        $id = $m[1][0];
        $this->assertStringContainsString('id="' . $id . '"', $html, $id . ' is referenced but never rendered');

        // The container must hold exactly one input: wireExternalSearch binds
        // `$(container).find('input').first()`, so a second control inside the
        // same wrapper would be a coin flip.
        $wrapper = $this->wrapperFor($html, $id);
        $this->assertSame(1, substr_count($wrapper, '<input'), 'search container must hold exactly one input');
        $this->assertStringContainsString('type="search"', $wrapper);
    }

    /**
     * The trap: searching must be enabled, or `table.search()` does nothing.
     */
    #[DataProvider('gridProvider')]
    public function testSearchingIsNotDisabledOnATableThatHasASearchBox(string $case): void
    {
        $html = $case === 'dashboard' ? $this->dashboard() : $this->campaignPerformance();

        $this->assertStringContainsString('data-search-container="#', $html);
        $this->assertStringNotContainsString(
            'data-search="false"',
            $html,
            'data-search="false" makes the external search box a silent no-op'
        );
        $this->assertStringContainsString('data-search="true"', $html);
    }

    /**
     * The dashboard card is capped at 10 rows by
     * DashboardTablePresenter::buildTopCampaigns(), so its search filters those 10
     * and nothing else. The placeholder has to say so, and the info line has to be
     * on, or a campaign ranked 11th reads as deleted.
     */
    public function testDashboardSearchNamesItsTenRowCorpus(): void
    {
        $html = $this->dashboard();

        $this->assertStringContainsString('placeholder="Search these 10 campaigns..."', $html);
        $this->assertStringContainsString('data-info="true"', $html);

        // `i` must be in the DOM string or DataTables never builds the info element,
        // and the footer wrapper is what gives it the app's styling.
        $this->assertMatchesRegularExpression('/data-dom="[^"]*i[^"]*"/', $html);
        $this->assertStringContainsString('data-footer-container="#dashCampaignTableFooter"', $html);
        $this->assertStringContainsString('id="dashCampaignTableFooter"', $html);
    }

    /**
     * Campaign Performance is not sliced, so its search covers every row and the
     * placeholder should not imply a subset.
     */
    public function testCampaignPerformanceSearchDoesNotClaimASubset(): void
    {
        $html = $this->campaignPerformance();

        $this->assertStringContainsString('placeholder="Search campaigns..."', $html);
        $this->assertStringNotContainsString('10 campaigns', $html);
    }

    /**
     * With no rows there is no table to bind to, so the input must not render -
     * a visibly dead search box is its own bug report.
     */
    public function testCampaignPerformanceHidesTheSearchBoxWhenThereAreNoRows(): void
    {
        $html = $this->campaignPerformance(true);

        $this->assertStringNotContainsString('id="cpKpiTableSearch"', $html);
        $this->assertStringNotContainsString('type="search"', $html);
    }

    /**
     * The topbar search is a separate, spec'd control (AP-56). Neither grid may
     * grow a second copy of it.
     */
    public function testGridsDoNotReimplementTheHeaderSearch(): void
    {
        foreach ([$this->dashboard(), $this->campaignPerformance()] as $html) {
            $this->assertStringNotContainsString('headerCampaignSearch', $html);
        }
    }

    /**
     * Narrow the markup to the element carrying $id, so an assertion about "the
     * container" cannot accidentally pass on an input elsewhere in the view.
     */
    private function wrapperFor(string $html, string $id): string
    {
        $start = strpos($html, 'id="' . $id . '"');
        $this->assertNotFalse($start, 'id ' . $id . ' not found');

        $open = strpos($html, '>', $start);
        $close = strpos($html, '</div>', $open);
        $this->assertNotFalse($close, 'unterminated container for ' . $id);

        return substr($html, $open, $close - $open);
    }
}
