<?php

namespace Tests\Feature;

use App\Services\AlertsManager\AlertsManagerService;
use CodeIgniter\Test\CIUnitTestCase;

/**
 * Advertiser isolation for Alerts Manager (AP-130).
 *
 * Drives the service directly with two advertisers, asserting in BOTH
 * directions. Inserts its own fixtures so it does not depend on whatever
 * dashboard_announcements holds after a drest refresh; every row it creates is
 * removed in tearDown, including rows a leaking implementation would have
 * written to the wrong advertiser.
 */
final class AlertsManagerScopingTest extends CIUnitTestCase
{
    private const ADV_A  = 13421;
    private const ADV_B  = 15273;
    private const USER   = 999001;
    private const MARKER = 'AP130-scoping-fixture';

    private AlertsManagerService $service;
    protected $db;

    protected function setUp(): void
    {
        parent::setUp();
        $this->service = new AlertsManagerService();
        $this->db      = \Config\Database::connect('default');
        $this->purge();
    }

    protected function tearDown(): void
    {
        $this->purge();
        parent::tearDown();
    }

    private function purge(): void
    {
        $this->db->table('dashboard_announcements')
            ->like('title', self::MARKER, 'after')
            ->delete();
    }

    /** Insert directly, bypassing the service, so reads are tested independently of writes. */
    private function seed(int $advId, string $label): int
    {
        $this->db->table('dashboard_announcements')->insert([
            'adv_id'             => $advId,
            'severity'           => 'info',
            'title'              => self::MARKER . ' ' . $label,
            'body'               => 'fixture',
            'starts_at'          => date('Y-m-d H:i:s', strtotime('-1 hour')),
            'expires_at'         => date('Y-m-d H:i:s', strtotime('+30 days')),
            'is_active'          => 1,
            'created_at'         => date('Y-m-d H:i:s'),
            'created_by_app'     => 'adcenter',
            'created_by_user_id' => self::USER,
        ]);

        return (int) $this->db->insertID();
    }

    private function titlesFor(int $advId): array
    {
        return array_column($this->service->getAllAlerts($advId), 'title');
    }

    private function rawAdvIdOf(int $id): int
    {
        $row = $this->db->table('dashboard_announcements')->where('id', $id)->get()->getRowArray();

        return (int) ($row['adv_id'] ?? 0);
    }

    private function rawIsActiveOf(int $id): int
    {
        $row = $this->db->table('dashboard_announcements')->where('id', $id)->get()->getRowArray();

        return (int) ($row['is_active'] ?? 0);
    }

    /* ── Reads ─────────────────────────────────────────────────── */

    public function testListingIsScopedToOneAdvertiserInBothDirections(): void
    {
        $this->seed(self::ADV_A, 'A');
        $this->seed(self::ADV_B, 'B');

        $a = $this->titlesFor(self::ADV_A);
        $b = $this->titlesFor(self::ADV_B);

        $this->assertContains(self::MARKER . ' A', $a);
        $this->assertNotContains(self::MARKER . ' B', $a, 'advertiser A must not see B alerts');

        $this->assertContains(self::MARKER . ' B', $b);
        $this->assertNotContains(self::MARKER . ' A', $b, 'advertiser B must not see A alerts');
    }

    public function testBroadcastRowsAreNotListedInAccountScope(): void
    {
        $this->seed(0, 'broadcast');

        $this->assertNotContains(
            self::MARKER . ' broadcast',
            $this->titlesFor(self::ADV_A),
            'adv_id = 0 is owned by nobody and must not be editable from one account'
        );
    }

    public function testGetAlertRefusesForeignId(): void
    {
        $bId = $this->seed(self::ADV_B, 'B');

        $this->assertNull($this->service->getAlert(self::ADV_A, $bId));
        $this->assertNotNull($this->service->getAlert(self::ADV_B, $bId), 'the owner must still read it');
    }

    public function testNoAdvertiserContextReturnsNothing(): void
    {
        $this->seed(self::ADV_A, 'A');

        $this->assertSame([], $this->service->getAllAlerts(0), 'adv_id 0 must fail closed, not widen');
    }

    /* ── Mutations ─────────────────────────────────────────────── */

    public function testCreateIgnoresPostedAdvIdAndUsesSessionAdvertiser(): void
    {
        // Forged payload aimed at B, submitted while acting for A.
        $res = $this->service->createAlert(self::ADV_A, [
            'adv_id'     => self::ADV_B,
            'severity'   => 'info',
            'title'      => self::MARKER . ' forged',
            'body'       => 'fixture',
            'starts_at'  => date('Y-m-d H:i:s', strtotime('-1 hour')),
            'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
            'is_active'  => 1,
        ], self::USER);

        $this->assertTrue($res['success'], (string) ($res['error'] ?? ''));
        $this->assertSame(
            self::ADV_A,
            $this->rawAdvIdOf((int) $res['id']),
            'the posted adv_id must be ignored in favour of the session advertiser'
        );
    }

    public function testUpdateOnForeignAlertIsRefusedAndMutatesNothing(): void
    {
        $bId    = $this->seed(self::ADV_B, 'B');
        $before = $this->service->getAlert(self::ADV_B, $bId);

        $res = $this->service->updateAlert(self::ADV_A, $bId, [
            'adv_id'     => self::ADV_A,
            'severity'   => 'critical',
            'title'      => self::MARKER . ' hijacked',
            'body'       => 'hijacked',
            'starts_at'  => date('Y-m-d H:i:s', strtotime('-1 hour')),
            'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
            'is_active'  => 1,
        ], self::USER);

        $this->assertFalse($res['success']);
        $this->assertSame($before, $this->service->getAlert(self::ADV_B, $bId), 'zero rows may change');
        $this->assertSame(self::ADV_B, $this->rawAdvIdOf($bId), 'an edit must never retarget an alert');
    }

    public function testUpdateCannotRetargetAnOwnedAlert(): void
    {
        $aId = $this->seed(self::ADV_A, 'A');

        $res = $this->service->updateAlert(self::ADV_A, $aId, [
            'adv_id'     => self::ADV_B,
            'severity'   => 'info',
            'title'      => self::MARKER . ' A edited',
            'body'       => 'fixture',
            'starts_at'  => date('Y-m-d H:i:s', strtotime('-1 hour')),
            'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
            'is_active'  => 1,
        ], self::USER);

        $this->assertTrue($res['success'], (string) ($res['error'] ?? ''));
        $this->assertSame(self::ADV_A, $this->rawAdvIdOf($aId), 'own alert must stay in own account');
    }

    public function testToggleOnForeignAlertIsRefusedAndMutatesNothing(): void
    {
        $bId = $this->seed(self::ADV_B, 'B');

        $this->assertFalse($this->service->toggleActive(self::ADV_A, $bId, self::USER)['success']);
        $this->assertSame(1, $this->rawIsActiveOf($bId), 'is_active must be untouched');
    }

    public function testDeleteOnForeignAlertIsRefusedAndMutatesNothing(): void
    {
        $bId = $this->seed(self::ADV_B, 'B');

        $this->assertFalse($this->service->softDelete(self::ADV_A, $bId, self::USER)['success']);
        $this->assertNotNull($this->service->getAlert(self::ADV_B, $bId), 'the row must survive');
    }

    public function testOwnerCanStillManageOwnAlert(): void
    {
        $aId = $this->seed(self::ADV_A, 'A');

        $this->assertTrue($this->service->toggleActive(self::ADV_A, $aId, self::USER)['success']);
        $this->assertSame(0, $this->rawIsActiveOf($aId));
        $this->assertTrue($this->service->softDelete(self::ADV_A, $aId, self::USER)['success']);
    }

    public function testActiveCountIsPerAdvertiser(): void
    {
        $baseA = $this->service->countActiveAlerts(self::ADV_A);
        $this->seed(self::ADV_B, 'B');

        $this->assertSame(
            $baseA,
            $this->service->countActiveAlerts(self::ADV_A),
            "another advertiser's alert must not consume A's active cap"
        );
    }
}
