<?php

namespace Tests\Feature;

use CodeIgniter\Security\Exceptions\SecurityException;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
use Config\Services;
use PHPUnit\Framework\Attributes\DataProvider;

/**
 * Ad Groups access control: the enable_adgroup gate and CSRF on the write routes.
 *
 * Covers the two holes AdGroupsFlowTest cannot see, because every case there
 * seeds a synthetic master-less advertiser with enable_adgroup => 1 and unsets
 * the csrf filter.
 *
 * Runs against the dev Admin DB and uses real linked accounts:
 *   21126 = master, enable_adgroup = 1, 116 ad groups
 *   21179 = sub-user of 21126 (DMcCoy), own enable_adgroup = 0
 *   17237 = master, enable_adgroup = 0
 *   17306 = sub-user of 17237, own enable_adgroup = 0
 *
 * The sub-user rows carrying 0 is the point: login only stores the logging-in
 * row's own flag, so reading the session value alone hid the feature from every
 * sub-user of an enabled account while legacy showed it.
 *
 * Unlike the sibling feature tests this class does NOT unset the csrf filter —
 * testWriteRoutesRejectAPostWithNoCsrfToken depends on it being active, and no
 * case here needs a tokenless POST to succeed.
 */
final class AdGroupAccessTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    private const ENABLED_MASTER  = 21126;
    private const ENABLED_SUB     = 21179;
    private const DISABLED_MASTER = 17237;
    private const DISABLED_SUB    = 17306;

    protected function setUp(): void
    {
        parent::setUp();
        helper('auth');
    }

    /** Session as login builds it: the flag is the row's OWN value. */
    private function sessionFor(int $advId, int $ownFlag): array
    {
        return [
            'logged_in'      => 1,
            'adv_id'         => $advId,
            'user_id'        => $advId,
            'username'       => 'QA Bot',
            'role'           => 'admin',
            'enable_adgroup' => $ownFlag,
        ];
    }

    private function login(int $advId, int $ownFlag): void
    {
        $this->mockSession();
        Services::session()->set($this->sessionFor($advId, $ownFlag));
    }

    /* ── The gate reads the master's flag ─────────────────────── */

    public function testSubUserOfAnEnabledMasterPassesTheGate(): void
    {
        // Own flag 0, master's flag 1 — the regression this fixes.
        $this->login(self::ENABLED_SUB, 0);
        $this->assertTrue(auth_has_adgroup());
    }

    public function testSubUserOfADisabledMasterIsStillDenied(): void
    {
        $this->login(self::DISABLED_SUB, 0);
        $this->assertFalse(auth_has_adgroup());
    }

    public function testEnabledMasterPassesTheGate(): void
    {
        $this->login(self::ENABLED_MASTER, 1);
        $this->assertTrue(auth_has_adgroup());
    }

    public function testDisabledMasterIsDenied(): void
    {
        $this->login(self::DISABLED_MASTER, 0);
        $this->assertFalse(auth_has_adgroup());
    }

    public function testNoAdvertiserSessionIsDenied(): void
    {
        $this->mockSession();
        $this->assertFalse(auth_has_adgroup());
    }

    public function testTheGateNeverReadsRequestInput(): void
    {
        // A spoofed flag on the request must not open the gate.
        $this->login(self::DISABLED_SUB, 0);
        $_GET['enable_adgroup']  = '1';
        $_POST['enable_adgroup'] = '1';

        try {
            $this->assertFalse(auth_has_adgroup());
        } finally {
            unset($_GET['enable_adgroup'], $_POST['enable_adgroup']);
        }
    }

    /* ── …and the page/feed follow it ─────────────────────────── */

    public function testSubUserOfAnEnabledMasterReachesTheAdGroupsPage(): void
    {
        $r = $this->withSession($this->sessionFor(self::ENABLED_SUB, 0))->get('ad-groups');

        $r->assertStatus(200);
        // The filter's denial is a redirect to '/', so a 200 here is the gate
        // passing rather than the page merely existing.
        $this->assertStringContainsString('adGroupsTable', (string) $r->response()->getBody());
    }

    public function testSubUserOfADisabledMasterIsRedirectedAway(): void
    {
        $r = $this->withSession($this->sessionFor(self::DISABLED_SUB, 0))->get('ad-groups');

        $this->assertContains($r->response()->getStatusCode(), [301, 302]);
    }

    public function testSubUserSeesTheSameFeedAsItsMaster(): void
    {
        // Scoped to auth_master_adv_id(), so the sub-user's feed must be the
        // master's. Compared rather than hardcoded — dev data shifts.
        $asMaster = json_decode(
            (string) $this->withSession($this->sessionFor(self::ENABLED_MASTER, 1))
                ->get('ad-groups/json')->response()->getBody(),
            true
        );
        $asSub = json_decode(
            (string) $this->withSession($this->sessionFor(self::ENABLED_SUB, 0))
                ->get('ad-groups/json')->response()->getBody(),
            true
        );

        $this->assertGreaterThan(0, $asMaster['total'], 'fixture master should own ad groups');
        $this->assertSame($asMaster['total'], $asSub['total']);
        $this->assertSame(
            array_column($asMaster['data'], 'id'),
            array_column($asSub['data'], 'id')
        );
    }

    /* ── CSRF on the write routes ─────────────────────────────── */

    #[DataProvider('writeRoutes')]
    public function testWriteRoutesRejectAPostWithNoCsrfToken(string $uri): void
    {
        // config('Filters') must still carry 'csrf' => ad-groups/* for this to
        // mean anything; setUp deliberately leaves it in place.
        $this->assertContains('ad-groups/*', config('Filters')->filters['csrf']['before']);

        // AJAX so Security rethrows instead of redirecting back ($redirect = true).
        $this->expectException(SecurityException::class);
        $this->withSession($this->sessionFor(self::ENABLED_MASTER, 1))
            ->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
            ->post($uri, ['name' => 'csrf probe']);
    }

    public static function writeRoutes(): array
    {
        return [
            'create'      => ['ad-groups/create'],
            'edit'        => ['ad-groups/1/edit'],
            'delete'      => ['ad-groups/delete/1'],
            'bulk delete' => ['ad-groups/bulk-delete'],
        ];
    }
}
