<?php

namespace Tests\Feature;

use App\Services\Campaign\CampaignMetricTotals;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
use PHPUnit\Framework\Attributes\DataProvider;

/**
 * Campaign Management's aggregate ("Total") row must have exactly one <th> per column.
 *
 * The grid renders the row as a <tfoot> with no colspan, because the JS addresses footer
 * cells by column index. Three separate places gate the same role-conditional columns — the
 * <thead>, renderCampRow()'s <td>s, and now CampaignMetricTotals::columnCount() — and a
 * mismatch is quiet: DataTables logs an init warning and the totals line up under the wrong
 * headers. This renders the real page for every combination of the three roles and compares
 * the counts.
 *
 * Run with the feature config:
 *   php8.2 -d newrelic.enabled=0 -d newrelic.daemon.dont_launch=3 \
 *     vendor/bin/phpunit -c phpunit.feature.xml
 */
final class CampaignTotalsRowColumnsTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    /** Throwaway advertiser id — never a real account, and deliberately not Hearst. */
    private const ADV = 999999992;

    /** From HEARST_ADV_IDS: the only way auth_is_hearst() turns on. */
    private const HEARST_ADV = 13421;

    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();
    }

    /**
     * Every combination of the three gates, with the session keys each reads:
     * is_ao → auth_is_ao(), is_superuser → auth_is_mngt_user(), a Hearst adv_id →
     * auth_is_hearst().
     *
     * @return array<string,array{0:bool,1:bool,2:bool}>
     */
    public static function roleCombinations(): array
    {
        $out = [];
        foreach ([false, true] as $isAo) {
            foreach ([false, true] as $isMngt) {
                foreach ([false, true] as $isHearst) {
                    $label = implode(' + ', array_filter([
                        $isAo ? 'ao' : null,
                        $isMngt ? 'mngt' : null,
                        $isHearst ? 'hearst' : null,
                    ])) ?: 'plain advertiser';
                    $out[$label] = [$isAo, $isMngt, $isHearst];
                }
            }
        }

        return $out;
    }

    #[DataProvider('roleCombinations')]
    public function testTheTotalsRowHasOneCellPerColumn(bool $isAo, bool $isMngt, bool $isHearst): void
    {
        $body = $this->renderGrid($isAo, $isMngt, $isHearst);

        $headCount = $this->countThs($body, 'thead');
        $footCount = $this->countThs($body, 'tfoot');

        $this->assertGreaterThan(0, $headCount, 'the grid rendered no <thead> cells');
        $this->assertSame(
            $headCount,
            $footCount,
            'totals row has ' . $footCount . ' cells against ' . $headCount . ' columns'
        );
    }

    /**
     * And that the count helper the view calls agrees with the markup — so the unit test's
     * arithmetic is pinned to reality, not just to itself.
     */
    #[DataProvider('roleCombinations')]
    public function testTheColumnCountHelperMatchesTheRenderedHead(bool $isAo, bool $isMngt, bool $isHearst): void
    {
        $body = $this->renderGrid($isAo, $isMngt, $isHearst);

        $this->assertSame(
            $this->countThs($body, 'thead'),
            CampaignMetricTotals::columnCount($isAo, $isMngt, $isHearst)
        );
    }

    /**
     * The row starts hidden and empty: the JS fills it from the `metricTotals` block on the
     * AJAX envelope and only then drops d-none. Rendering values server-side would show a
     * total for rows the page has not loaded yet.
     */
    public function testTheTotalsRowStartsHiddenAndEmpty(): void
    {
        $body = $this->renderGrid(true, true, false);

        $this->assertMatchesRegularExpression(
            '/<tfoot>\s*<tr class="totals-row d-none">/',
            $body,
            'the totals row should ship hidden'
        );

        preg_match('#<tfoot>(.*?)</tfoot>#s', $body, $m);
        $this->assertNotEmpty($m, 'no <tfoot> rendered');
        $this->assertSame(
            '',
            trim(strip_tags($m[1])),
            'the totals row should ship with no values in it'
        );
    }

    private function renderGrid(bool $isAo, bool $isMngt, bool $isHearst): string
    {
        $session = [
            'logged_in' => 1,
            'adv_id'    => $isHearst ? self::HEARST_ADV : self::ADV,
            'user_id'   => $isHearst ? self::HEARST_ADV : self::ADV,
            'role'      => 'admin',
        ];
        if ($isAo) {
            $session['is_ao'] = 1;
        }
        if ($isMngt) {
            // auth_is_mngt_user() is auth_is_superuser() || auth_mngtuser() !== ''.
            $session['is_superuser'] = 1;
            $session['mngtuser']     = 'qabot';
        }

        return (string) $this->withSession($session)->get('campaigns')->response()->getBody();
    }

    /** Count <th> elements inside the grid's own thead/tfoot, ignoring other tables. */
    private function countThs(string $body, string $section): int
    {
        // Scope to #campaignMgmtTable: the page also renders the history-modal table.
        $start = strpos($body, 'id="campaignMgmtTable"');
        if ($start === false) {
            return 0;
        }
        $end   = strpos($body, '</table>', $start);
        $table = substr($body, $start, $end === false ? null : $end - $start);

        if (!preg_match('#<' . $section . '>(.*?)</' . $section . '>#s', $table, $m)) {
            return 0;
        }

        return preg_match_all('/<th\b/', $m[1]);
    }
}
