<?php

namespace Tests\Feature;

use App\Models\NotesModel;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;

/**
 * Advertiser isolation for Notes (ADC-180).
 *
 * Fixtures are two real dev campaigns owned by different advertisers. The
 * ownership guard test fails loudly if a drest refresh moves them, so the
 * isolation assertions can never pass vacuously.
 */
final class NotesScopingTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    private const ADV_A      = 13421;
    private const CAMPAIGN_A = 93772;
    private const ADV_B      = 15273;
    private const CAMPAIGN_B = 93749;

    private const MNGT = 'qa.harness';

    private NotesModel $model;

    protected $db;

    private int $obLevel = 0;

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

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

    private function purge(): void
    {
        $this->db->table('adcenter_notes')->where('user', self::MNGT)->delete();
    }

    private function mngtSession(int $advId): array
    {
        return [
            'logged_in'    => 1,
            'adv_id'       => $advId,
            'user_id'      => 9999,
            'role'         => 'admin',
            'is_superuser' => false,
            'mngtuser'     => self::MNGT,
        ];
    }

    private function nativeSession(int $advId): array
    {
        return [
            'logged_in'    => 1,
            'adv_id'       => $advId,
            'user_id'      => $advId,
            'role'         => 'admin',
            'is_superuser' => false,
        ];
    }

    private function campaignNotesBody(array $session, int $campaignId): string
    {
        $res = $this->withSession($session)->get('api/notes/campaign/' . $campaignId);

        return (string) $res->response()->getBody();
    }

    /* ── Fixture guard ─────────────────────────────────────────── */

    public function testFixtureCampaignsBelongToDifferentAdvertisers(): void
    {
        $owner = function (int $campaignId): int {
            $row = $this->db->table('campaign')->select('advertiser_id')
                ->where('id', $campaignId)->get()->getRowArray();

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

        $this->assertSame(self::ADV_A, $owner(self::CAMPAIGN_A), 'fixture drift: campaign A changed owner');
        $this->assertSame(self::ADV_B, $owner(self::CAMPAIGN_B), 'fixture drift: campaign B changed owner');

        $this->assertNotEmpty(
            $this->model->getList(self::ADV_B, self::CAMPAIGN_B),
            'campaign B must carry notes, or the isolation assertions prove nothing'
        );
    }

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

    public function testGlobalListingIsScopedInBothDirections(): void
    {
        $this->model->createNote(self::ADV_A, 'note for A', self::MNGT);
        $this->model->createNote(self::ADV_B, 'note for B', self::MNGT);

        $a = array_column($this->model->getList(self::ADV_A), 'comment');
        $b = array_column($this->model->getList(self::ADV_B), 'comment');

        $this->assertContains('note for A', $a);
        $this->assertNotContains('note for B', $a, 'advertiser A must not see B notes');

        $this->assertContains('note for B', $b);
        $this->assertNotContains('note for A', $b, 'advertiser B must not see A notes');
    }

    public function testDateFilterStaysScoped(): void
    {
        $this->model->createNote(self::ADV_B, 'note for B', self::MNGT);
        $today = date('Y-m-d');

        $this->assertNotContains(
            'note for B',
            array_column($this->model->getList(self::ADV_A, null, $today, $today), 'comment'),
            'the date filter must not widen the advertiser scope'
        );
    }

    public function testCampaignNotesApiRefusesForeignCampaignForMngtUser(): void
    {
        $this->assertStringNotContainsString(
            '"comment"',
            $this->campaignNotesBody($this->mngtSession(self::ADV_A), self::CAMPAIGN_B),
            'mngt acting for A must not read B campaign notes'
        );

        $this->assertStringNotContainsString(
            '"comment"',
            $this->campaignNotesBody($this->mngtSession(self::ADV_B), self::CAMPAIGN_A),
            'and the reverse direction'
        );
    }

    public function testCampaignNotesApiIsDeniedToNativeAdvertiser(): void
    {
        $this->assertStringNotContainsString(
            '"comment"',
            $this->campaignNotesBody($this->nativeSession(self::ADV_B), self::CAMPAIGN_B),
            'the feed is mngt-only even for the campaign owner'
        );
    }

    public function testOwnerCanStillReadOwnCampaignNotes(): void
    {
        $this->assertStringContainsString(
            '"comment"',
            $this->campaignNotesBody($this->mngtSession(self::ADV_B), self::CAMPAIGN_B),
            'mngt acting for B must still read B campaign notes'
        );
    }

    /* ── Writes ────────────────────────────────────────────────── */

    public function testCreateStoresTheSessionAdvertiser(): void
    {
        $id  = $this->model->createNote(self::ADV_A, 'owned by A', self::MNGT);
        $row = $this->db->table('adcenter_notes')->where('id', $id)->get()->getRowArray();

        $this->assertSame(self::ADV_A, (int) $row['adv_id']);
    }

    public function testCreateWithNoAdvertiserContextWritesNothing(): void
    {
        $before = (int) $this->db->table('adcenter_notes')->countAllResults();

        $this->assertSame(0, $this->model->createNote(0, 'orphan', self::MNGT));
        $this->assertSame($before, (int) $this->db->table('adcenter_notes')->countAllResults());
    }

    public function testForeignNoteCannotBeReadOrDeleted(): void
    {
        $bId = $this->model->createNote(self::ADV_B, 'note for B', self::MNGT);

        $this->assertNull($this->model->get(self::ADV_A, $bId), 'A must not read B note');
        $this->assertFalse(
            $this->model->softDelete(self::ADV_A, $bId, self::MNGT),
            'A must not delete B note even as the same creator'
        );
        $this->assertNotNull($this->model->get(self::ADV_B, $bId), 'the row must survive');
    }

    public function testOwnerCanStillDeleteOwnNote(): void
    {
        $aId = $this->model->createNote(self::ADV_A, 'owned by A', self::MNGT);

        $this->assertTrue($this->model->softDelete(self::ADV_A, $aId, self::MNGT));
    }

    public function testDeleteRemainsCreatorScoped(): void
    {
        $aId = $this->model->createNote(self::ADV_A, 'owned by A', self::MNGT);

        $this->assertFalse(
            $this->model->softDelete(self::ADV_A, $aId, 'someone.else'),
            'the advertiser predicate must not replace the creator check'
        );
    }
}
